Visibility.java revision cba01b2310e78fc2114ceed285f4e134469ae62b
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.transition;
18
19import com.android.internal.R;
20
21import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.drawable.BitmapDrawable;
28import android.util.AttributeSet;
29import android.view.View;
30import android.view.ViewGroup;
31/**
32 * This transition tracks changes to the visibility of target views in the
33 * start and end scenes. Visibility is determined not just by the
34 * {@link View#setVisibility(int)} state of views, but also whether
35 * views exist in the current view hierarchy. The class is intended to be a
36 * utility for subclasses such as {@link Fade}, which use this visibility
37 * information to determine the specific animations to run when visibility
38 * changes occur. Subclasses should implement one or both of the methods
39 * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
40 * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
41 * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
42 * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
43 */
44public abstract class Visibility extends Transition {
45
46    static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
47    private static final String PROPNAME_PARENT = "android:visibility:parent";
48    private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
49
50    /**
51     * Mode used in {@link #setMode(int)} to make the transition
52     * operate on targets that are appearing. Maybe be combined with
53     * {@link #MODE_OUT} to target Visibility changes both in and out.
54     */
55    public static final int MODE_IN = 0x1;
56
57    /**
58     * Mode used in {@link #setMode(int)} to make the transition
59     * operate on targets that are disappearing. Maybe be combined with
60     * {@link #MODE_IN} to target Visibility changes both in and out.
61     */
62    public static final int MODE_OUT = 0x2;
63
64    private static final String[] sTransitionProperties = {
65            PROPNAME_VISIBILITY,
66    };
67
68    private static class VisibilityInfo {
69        boolean visibilityChange;
70        boolean fadeIn;
71        int startVisibility;
72        int endVisibility;
73        ViewGroup startParent;
74        ViewGroup endParent;
75    }
76
77    private int mMode = MODE_IN | MODE_OUT;
78
79    private int mForcedStartVisibility = -1;
80    private int mForcedEndVisibility = -1;
81
82    public Visibility() {}
83
84    public Visibility(Context context, AttributeSet attrs) {
85        super(context, attrs);
86        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VisibilityTransition);
87        int mode = a.getInt(R.styleable.VisibilityTransition_transitionVisibilityMode, 0);
88        a.recycle();
89        if (mode != 0) {
90            setMode(mode);
91        }
92    }
93
94    /**
95     * Changes the transition to support appearing and/or disappearing Views, depending
96     * on <code>mode</code>.
97     *
98     * @param mode The behavior supported by this transition, a combination of
99     *             {@link #MODE_IN} and {@link #MODE_OUT}.
100     * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
101     */
102    public void setMode(int mode) {
103        if ((mode & ~(MODE_IN | MODE_OUT)) != 0) {
104            throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed");
105        }
106        mMode = mode;
107    }
108
109    /**
110     * Returns whether appearing and/or disappearing Views are supported.
111     *
112     * Returns whether appearing and/or disappearing Views are supported. A combination of
113     *         {@link #MODE_IN} and {@link #MODE_OUT}.
114     * @attr ref android.R.styleable#VisibilityTransition_transitionVisibilityMode
115     */
116    public int getMode() {
117        return mMode;
118    }
119
120    @Override
121    public String[] getTransitionProperties() {
122        return sTransitionProperties;
123    }
124
125    private void captureValues(TransitionValues transitionValues, int forcedVisibility) {
126        int visibility;
127        if (forcedVisibility != -1) {
128            visibility = forcedVisibility;
129        } else {
130            visibility = transitionValues.view.getVisibility();
131        }
132        transitionValues.values.put(PROPNAME_VISIBILITY, visibility);
133        transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent());
134        int[] loc = new int[2];
135        transitionValues.view.getLocationOnScreen(loc);
136        transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc);
137    }
138
139    @Override
140    public void captureStartValues(TransitionValues transitionValues) {
141        captureValues(transitionValues, mForcedStartVisibility);
142    }
143
144    @Override
145    public void captureEndValues(TransitionValues transitionValues) {
146        captureValues(transitionValues, mForcedEndVisibility);
147    }
148
149    /** @hide */
150    @Override
151    public void forceVisibility(int visibility, boolean isStartValue) {
152        if (isStartValue) {
153            mForcedStartVisibility = visibility;
154        } else {
155            mForcedEndVisibility = visibility;
156        }
157    }
158
159    /**
160     * Returns whether the view is 'visible' according to the given values
161     * object. This is determined by testing the same properties in the values
162     * object that are used to determine whether the object is appearing or
163     * disappearing in the {@link
164     * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)}
165     * method. This method can be called by, for example, subclasses that want
166     * to know whether the object is visible in the same way that Visibility
167     * determines it for the actual animation.
168     *
169     * @param values The TransitionValues object that holds the information by
170     * which visibility is determined.
171     * @return True if the view reference by <code>values</code> is visible,
172     * false otherwise.
173     */
174    public boolean isVisible(TransitionValues values) {
175        if (values == null) {
176            return false;
177        }
178        int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY);
179        View parent = (View) values.values.get(PROPNAME_PARENT);
180
181        return visibility == View.VISIBLE && parent != null;
182    }
183
184    private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues,
185            TransitionValues endValues) {
186        final VisibilityInfo visInfo = new VisibilityInfo();
187        visInfo.visibilityChange = false;
188        visInfo.fadeIn = false;
189        if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) {
190            visInfo.startVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY);
191            visInfo.startParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
192        } else {
193            visInfo.startVisibility = -1;
194            visInfo.startParent = null;
195        }
196        if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) {
197            visInfo.endVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY);
198            visInfo.endParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
199        } else {
200            visInfo.endVisibility = -1;
201            visInfo.endParent = null;
202        }
203        if (startValues != null && endValues != null) {
204            if (visInfo.startVisibility == visInfo.endVisibility &&
205                    visInfo.startParent == visInfo.endParent) {
206                return visInfo;
207            } else {
208                if (visInfo.startVisibility != visInfo.endVisibility) {
209                    if (visInfo.startVisibility == View.VISIBLE) {
210                        visInfo.fadeIn = false;
211                        visInfo.visibilityChange = true;
212                    } else if (visInfo.endVisibility == View.VISIBLE) {
213                        visInfo.fadeIn = true;
214                        visInfo.visibilityChange = true;
215                    }
216                    // no visibilityChange if going between INVISIBLE and GONE
217                } else if (visInfo.startParent != visInfo.endParent) {
218                    if (visInfo.endParent == null) {
219                        visInfo.fadeIn = false;
220                        visInfo.visibilityChange = true;
221                    } else if (visInfo.startParent == null) {
222                        visInfo.fadeIn = true;
223                        visInfo.visibilityChange = true;
224                    }
225                }
226            }
227        } else if (startValues == null && visInfo.endVisibility == View.VISIBLE) {
228            visInfo.fadeIn = true;
229            visInfo.visibilityChange = true;
230        } else if (endValues == null && visInfo.startVisibility == View.VISIBLE) {
231            visInfo.fadeIn = false;
232            visInfo.visibilityChange = true;
233        }
234        return visInfo;
235    }
236
237    @Override
238    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
239            TransitionValues endValues) {
240        VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues);
241        if (visInfo.visibilityChange
242                && (visInfo.startParent != null || visInfo.endParent != null)) {
243            if (visInfo.fadeIn) {
244                return onAppear(sceneRoot, startValues, visInfo.startVisibility,
245                        endValues, visInfo.endVisibility);
246            } else {
247                return onDisappear(sceneRoot, startValues, visInfo.startVisibility,
248                        endValues, visInfo.endVisibility
249                );
250            }
251        }
252        return null;
253    }
254
255    /**
256     * The default implementation of this method calls
257     * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
258     * Subclasses should override this method or
259     * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}.
260     * if they need to create an Animator when targets appear.
261     * The method should only be called by the Visibility class; it is
262     * not intended to be called from external classes.
263     *
264     * @param sceneRoot The root of the transition hierarchy
265     * @param startValues The target values in the start scene
266     * @param startVisibility The target visibility in the start scene
267     * @param endValues The target values in the end scene
268     * @param endVisibility The target visibility in the end scene
269     * @return An Animator to be started at the appropriate time in the
270     * overall transition for this scene change. A null value means no animation
271     * should be run.
272     */
273    public Animator onAppear(ViewGroup sceneRoot,
274            TransitionValues startValues, int startVisibility,
275            TransitionValues endValues, int endVisibility) {
276        if ((mMode & MODE_IN) != MODE_IN || endValues == null) {
277            return null;
278        }
279        if (startValues == null) {
280            VisibilityInfo parentVisibilityInfo = null;
281            View endParent = (View) endValues.view.getParent();
282            TransitionValues startParentValues = getMatchedTransitionValues(endParent,
283                                                                            false);
284            TransitionValues endParentValues = getTransitionValues(endParent, false);
285            parentVisibilityInfo =
286                getVisibilityChangeInfo(startParentValues, endParentValues);
287            if (parentVisibilityInfo.visibilityChange) {
288                return null;
289            }
290        }
291        return onAppear(sceneRoot, endValues.view, startValues, endValues);
292    }
293
294    /**
295     * The default implementation of this method returns a null Animator. Subclasses should
296     * override this method to make targets appear with the desired transition. The
297     * method should only be called from
298     * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
299     *
300     * @param sceneRoot The root of the transition hierarchy
301     * @param view The View to make appear. This will be in the target scene's View hierarchy and
302     *             will be VISIBLE.
303     * @param startValues The target values in the start scene
304     * @param endValues The target values in the end scene
305     * @return An Animator to be started at the appropriate time in the
306     * overall transition for this scene change. A null value means no animation
307     * should be run.
308     */
309    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
310            TransitionValues endValues) {
311        return null;
312    }
313
314    /**
315     * Subclasses should override this method or
316     * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}
317     * if they need to create an Animator when targets disappear.
318     * The method should only be called by the Visibility class; it is
319     * not intended to be called from external classes.
320     * <p>
321     * The default implementation of this method attempts to find a View to use to call
322     * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)},
323     * based on the situation of the View in the View hierarchy. For example,
324     * if a View was simply removed from its parent, then the View will be added
325     * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code>
326     * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
327     * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE},
328     * then it can be used as the <code>view</code> and the visibility will be changed
329     * to {@link View#VISIBLE} for the duration of the animation. However, if a View
330     * is in a hierarchy which is also altering its visibility, the situation can be
331     * more complicated. In general, if a view that is no longer in the hierarchy in
332     * the end scene still has a parent (so its parent hierarchy was removed, but it
333     * was not removed from its parent), then it will be left alone to avoid side-effects from
334     * improperly removing it from its parent. The only exception to this is if
335     * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int,
336     * android.content.Context) created from a layout resource file}, then it is considered
337     * safe to un-parent the starting scene view in order to make it disappear.</p>
338     *
339     * @param sceneRoot The root of the transition hierarchy
340     * @param startValues The target values in the start scene
341     * @param startVisibility The target visibility in the start scene
342     * @param endValues The target values in the end scene
343     * @param endVisibility The target visibility in the end scene
344     * @return An Animator to be started at the appropriate time in the
345     * overall transition for this scene change. A null value means no animation
346     * should be run.
347     */
348    public Animator onDisappear(ViewGroup sceneRoot,
349            TransitionValues startValues, int startVisibility,
350            TransitionValues endValues, int endVisibility) {
351        if ((mMode & MODE_OUT) != MODE_OUT) {
352            return null;
353        }
354
355        View startView = (startValues != null) ? startValues.view : null;
356        View endView = (endValues != null) ? endValues.view : null;
357        View overlayView = null;
358        View viewToKeep = null;
359        if (endView == null || endView.getParent() == null) {
360            if (endView != null) {
361                // endView was removed from its parent - add it to the overlay
362                overlayView = endView;
363            } else if (startView != null) {
364                // endView does not exist. Use startView only under certain
365                // conditions, because placing a view in an overlay necessitates
366                // it being removed from its current parent
367                if (startView.getParent() == null) {
368                    // no parent - safe to use
369                    overlayView = startView;
370                } else if (startView.getParent() instanceof View) {
371                    View startParent = (View) startView.getParent();
372                    TransitionValues startParentValues = getTransitionValues(startParent, true);
373                    TransitionValues endParentValues = getMatchedTransitionValues(startParent,
374                            true);
375                    VisibilityInfo parentVisibilityInfo =
376                            getVisibilityChangeInfo(startParentValues, endParentValues);
377                    if (!parentVisibilityInfo.visibilityChange) {
378                        overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
379                                startParent);
380                    } else if (startParent.getParent() == null) {
381                        int id = startParent.getId();
382                        if (id != View.NO_ID && sceneRoot.findViewById(id) != null
383                                && mCanRemoveViews) {
384                            // no parent, but its parent is unparented  but the parent
385                            // hierarchy has been replaced by a new hierarchy with the same id
386                            // and it is safe to un-parent startView
387                            overlayView = startView;
388                        }
389                    }
390                }
391            }
392        } else {
393            // visibility change
394            if (endVisibility == View.INVISIBLE) {
395                viewToKeep = endView;
396            } else {
397                // Becoming GONE
398                if (startView == endView) {
399                    viewToKeep = endView;
400                } else {
401                    overlayView = startView;
402                }
403            }
404        }
405        final int finalVisibility = endVisibility;
406        final ViewGroup finalSceneRoot = sceneRoot;
407
408        if (overlayView != null) {
409            // TODO: Need to do this for general case of adding to overlay
410            int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
411            int screenX = screenLoc[0];
412            int screenY = screenLoc[1];
413            int[] loc = new int[2];
414            sceneRoot.getLocationOnScreen(loc);
415            overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
416            overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
417            sceneRoot.getOverlay().add(overlayView);
418            Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
419            if (animator == null) {
420                sceneRoot.getOverlay().remove(overlayView);
421            } else {
422                final View finalOverlayView = overlayView;
423                animator.addListener(new AnimatorListenerAdapter() {
424                    @Override
425                    public void onAnimationEnd(Animator animation) {
426                        finalSceneRoot.getOverlay().remove(finalOverlayView);
427                    }
428                });
429            }
430            return animator;
431        }
432
433        if (viewToKeep != null) {
434            int originalVisibility = -1;
435            final boolean isForcedVisibility = mForcedStartVisibility != -1 ||
436                    mForcedEndVisibility != -1;
437            if (!isForcedVisibility) {
438                originalVisibility = viewToKeep.getVisibility();
439                viewToKeep.setVisibility(View.VISIBLE);
440            }
441            Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
442            if (animator != null) {
443                final View finalViewToKeep = viewToKeep;
444                animator.addListener(new AnimatorListenerAdapter() {
445                    boolean mCanceled = false;
446
447                    @Override
448                    public void onAnimationPause(Animator animation) {
449                        if (!mCanceled && !isForcedVisibility) {
450                            finalViewToKeep.setVisibility(finalVisibility);
451                        }
452                    }
453
454                    @Override
455                    public void onAnimationResume(Animator animation) {
456                        if (!mCanceled && !isForcedVisibility) {
457                            finalViewToKeep.setVisibility(View.VISIBLE);
458                        }
459                    }
460
461                    @Override
462                    public void onAnimationCancel(Animator animation) {
463                        mCanceled = true;
464                    }
465
466                    @Override
467                    public void onAnimationEnd(Animator animation) {
468                        if (!mCanceled) {
469                            if (isForcedVisibility) {
470                                finalViewToKeep.setTransitionAlpha(0);
471                            } else {
472                                finalViewToKeep.setVisibility(finalVisibility);
473                            }
474                        }
475                    }
476                });
477            } else if (!isForcedVisibility) {
478                viewToKeep.setVisibility(originalVisibility);
479            }
480            return animator;
481        }
482        return null;
483    }
484
485    @Override
486    boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) {
487        if (oldValues == null && newValues == null) {
488            return false;
489        }
490        if (oldValues != null && newValues != null &&
491                newValues.values.containsKey(PROPNAME_VISIBILITY) !=
492                        oldValues.values.containsKey(PROPNAME_VISIBILITY)) {
493            // The transition wasn't targeted in either the start or end, so it couldn't
494            // have changed.
495            return false;
496        }
497        VisibilityInfo changeInfo = getVisibilityChangeInfo(oldValues, newValues);
498        return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE ||
499                changeInfo.endVisibility == View.VISIBLE);
500    }
501
502    /**
503     * The default implementation of this method returns a null Animator. Subclasses should
504     * override this method to make targets disappear with the desired transition. The
505     * method should only be called from
506     * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
507     *
508     * @param sceneRoot The root of the transition hierarchy
509     * @param view The View to make disappear. This will be in the target scene's View
510     *             hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
511     *             VISIBLE.
512     * @param startValues The target values in the start scene
513     * @param endValues The target values in the end scene
514     * @return An Animator to be started at the appropriate time in the
515     * overall transition for this scene change. A null value means no animation
516     * should be run.
517     */
518    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
519            TransitionValues endValues) {
520        return null;
521    }
522}
523