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 
17 package android.transition;
18 
19 import com.android.internal.R;
20 
21 import android.animation.Animator;
22 import android.animation.Animator.AnimatorListener;
23 import android.animation.Animator.AnimatorPauseListener;
24 import android.content.Context;
25 import android.content.res.TypedArray;
26 import android.util.AttributeSet;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 /**
31  * This transition tracks changes to the visibility of target views in the
32  * start and end scenes. Visibility is determined not just by the
33  * {@link View#setVisibility(int)} state of views, but also whether
34  * views exist in the current view hierarchy. The class is intended to be a
35  * utility for subclasses such as {@link Fade}, which use this visibility
36  * information to determine the specific animations to run when visibility
37  * changes occur. Subclasses should implement one or both of the methods
38  * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
39  * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
40  * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
41  * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
42  */
43 public abstract class Visibility extends Transition {
44 
45     static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
46     private static final String PROPNAME_PARENT = "android:visibility:parent";
47     private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
48 
49     /**
50      * Mode used in {@link #setMode(int)} to make the transition
51      * operate on targets that are appearing. Maybe be combined with
52      * {@link #MODE_OUT} to target Visibility changes both in and out.
53      */
54     public static final int MODE_IN = 0x1;
55 
56     /**
57      * Mode used in {@link #setMode(int)} to make the transition
58      * operate on targets that are disappearing. Maybe be combined with
59      * {@link #MODE_IN} to target Visibility changes both in and out.
60      */
61     public static final int MODE_OUT = 0x2;
62 
63     private static final String[] sTransitionProperties = {
64             PROPNAME_VISIBILITY,
65             PROPNAME_PARENT,
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 
Visibility()82     public Visibility() {}
83 
Visibility(Context context, AttributeSet attrs)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      */
setMode(int mode)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      */
getMode()116     public int getMode() {
117         return mMode;
118     }
119 
120     @Override
getTransitionProperties()121     public String[] getTransitionProperties() {
122         return sTransitionProperties;
123     }
124 
captureValues(TransitionValues transitionValues, int forcedVisibility)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
captureStartValues(TransitionValues transitionValues)140     public void captureStartValues(TransitionValues transitionValues) {
141         captureValues(transitionValues, mForcedStartVisibility);
142     }
143 
144     @Override
captureEndValues(TransitionValues transitionValues)145     public void captureEndValues(TransitionValues transitionValues) {
146         captureValues(transitionValues, mForcedEndVisibility);
147     }
148 
149     /** @hide */
150     @Override
forceVisibility(int visibility, boolean isStartValue)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      */
isVisible(TransitionValues values)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 
getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues)184     private static 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
createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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      */
onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)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         final boolean isForcedVisibility = mForcedStartVisibility != -1 ||
292                 mForcedEndVisibility != -1;
293         if (isForcedVisibility) {
294             // Make sure that we reverse the effect of onDisappear's setTransitionAlpha(0)
295             endValues.view.setTransitionAlpha(1);
296         }
297         return onAppear(sceneRoot, endValues.view, startValues, endValues);
298     }
299 
300     /**
301      * The default implementation of this method returns a null Animator. Subclasses should
302      * override this method to make targets appear with the desired transition. The
303      * method should only be called from
304      * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
305      *
306      * @param sceneRoot The root of the transition hierarchy
307      * @param view The View to make appear. This will be in the target scene's View hierarchy and
308      *             will be VISIBLE.
309      * @param startValues The target values in the start scene
310      * @param endValues The target values in the end scene
311      * @return An Animator to be started at the appropriate time in the
312      * overall transition for this scene change. A null value means no animation
313      * should be run.
314      */
onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)315     public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
316             TransitionValues endValues) {
317         return null;
318     }
319 
320     /**
321      * Subclasses should override this method or
322      * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}
323      * if they need to create an Animator when targets disappear.
324      * The method should only be called by the Visibility class; it is
325      * not intended to be called from external classes.
326      * <p>
327      * The default implementation of this method attempts to find a View to use to call
328      * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)},
329      * based on the situation of the View in the View hierarchy. For example,
330      * if a View was simply removed from its parent, then the View will be added
331      * into a {@link android.view.ViewGroupOverlay} and passed as the <code>view</code>
332      * parameter in {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
333      * If a visible View is changed to be {@link View#GONE} or {@link View#INVISIBLE},
334      * then it can be used as the <code>view</code> and the visibility will be changed
335      * to {@link View#VISIBLE} for the duration of the animation. However, if a View
336      * is in a hierarchy which is also altering its visibility, the situation can be
337      * more complicated. In general, if a view that is no longer in the hierarchy in
338      * the end scene still has a parent (so its parent hierarchy was removed, but it
339      * was not removed from its parent), then it will be left alone to avoid side-effects from
340      * improperly removing it from its parent. The only exception to this is if
341      * the previous {@link Scene} was {@link Scene#getSceneForLayout(ViewGroup, int,
342      * android.content.Context) created from a layout resource file}, then it is considered
343      * safe to un-parent the starting scene view in order to make it disappear.</p>
344      *
345      * @param sceneRoot The root of the transition hierarchy
346      * @param startValues The target values in the start scene
347      * @param startVisibility The target visibility in the start scene
348      * @param endValues The target values in the end scene
349      * @param endVisibility The target visibility in the end scene
350      * @return An Animator to be started at the appropriate time in the
351      * overall transition for this scene change. A null value means no animation
352      * should be run.
353      */
onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)354     public Animator onDisappear(ViewGroup sceneRoot,
355             TransitionValues startValues, int startVisibility,
356             TransitionValues endValues, int endVisibility) {
357         if ((mMode & MODE_OUT) != MODE_OUT) {
358             return null;
359         }
360 
361         View startView = (startValues != null) ? startValues.view : null;
362         View endView = (endValues != null) ? endValues.view : null;
363         View overlayView = null;
364         View viewToKeep = null;
365         if (endView == null || endView.getParent() == null) {
366             if (endView != null) {
367                 // endView was removed from its parent - add it to the overlay
368                 overlayView = endView;
369             } else if (startView != null) {
370                 // endView does not exist. Use startView only under certain
371                 // conditions, because placing a view in an overlay necessitates
372                 // it being removed from its current parent
373                 if (startView.getParent() == null) {
374                     // no parent - safe to use
375                     overlayView = startView;
376                 } else if (startView.getParent() instanceof View) {
377                     View startParent = (View) startView.getParent();
378                     TransitionValues startParentValues = getTransitionValues(startParent, true);
379                     TransitionValues endParentValues = getMatchedTransitionValues(startParent,
380                             true);
381                     VisibilityInfo parentVisibilityInfo =
382                             getVisibilityChangeInfo(startParentValues, endParentValues);
383                     if (!parentVisibilityInfo.visibilityChange) {
384                         overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
385                                 startParent);
386                     } else if (startParent.getParent() == null) {
387                         int id = startParent.getId();
388                         if (id != View.NO_ID && sceneRoot.findViewById(id) != null
389                                 && mCanRemoveViews) {
390                             // no parent, but its parent is unparented  but the parent
391                             // hierarchy has been replaced by a new hierarchy with the same id
392                             // and it is safe to un-parent startView
393                             overlayView = startView;
394                         }
395                     }
396                 }
397             }
398         } else {
399             // visibility change
400             if (endVisibility == View.INVISIBLE) {
401                 viewToKeep = endView;
402             } else {
403                 // Becoming GONE
404                 if (startView == endView) {
405                     viewToKeep = endView;
406                 } else {
407                     overlayView = startView;
408                 }
409             }
410         }
411         final int finalVisibility = endVisibility;
412         final ViewGroup finalSceneRoot = sceneRoot;
413 
414         if (overlayView != null) {
415             // TODO: Need to do this for general case of adding to overlay
416             int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
417             int screenX = screenLoc[0];
418             int screenY = screenLoc[1];
419             int[] loc = new int[2];
420             sceneRoot.getLocationOnScreen(loc);
421             overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
422             overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
423             sceneRoot.getOverlay().add(overlayView);
424             Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
425             if (animator == null) {
426                 sceneRoot.getOverlay().remove(overlayView);
427             } else {
428                 final View finalOverlayView = overlayView;
429                 addListener(new TransitionListenerAdapter() {
430                     @Override
431                     public void onTransitionEnd(Transition transition) {
432                         finalSceneRoot.getOverlay().remove(finalOverlayView);
433                     }
434                 });
435             }
436             return animator;
437         }
438 
439         if (viewToKeep != null) {
440             int originalVisibility = -1;
441             final boolean isForcedVisibility = mForcedStartVisibility != -1 ||
442                     mForcedEndVisibility != -1;
443             if (!isForcedVisibility) {
444                 originalVisibility = viewToKeep.getVisibility();
445                 viewToKeep.setTransitionVisibility(View.VISIBLE);
446             }
447             Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
448             if (animator != null) {
449                 DisappearListener disappearListener = new DisappearListener(viewToKeep,
450                         finalVisibility, isForcedVisibility);
451                 animator.addListener(disappearListener);
452                 animator.addPauseListener(disappearListener);
453                 addListener(disappearListener);
454             } else if (!isForcedVisibility) {
455                 viewToKeep.setTransitionVisibility(originalVisibility);
456             }
457             return animator;
458         }
459         return null;
460     }
461 
462     @Override
isTransitionRequired(TransitionValues startValues, TransitionValues newValues)463     public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) {
464         if (startValues == null && newValues == null) {
465             return false;
466         }
467         if (startValues != null && newValues != null &&
468                 newValues.values.containsKey(PROPNAME_VISIBILITY) !=
469                         startValues.values.containsKey(PROPNAME_VISIBILITY)) {
470             // The transition wasn't targeted in either the start or end, so it couldn't
471             // have changed.
472             return false;
473         }
474         VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues);
475         return changeInfo.visibilityChange && (changeInfo.startVisibility == View.VISIBLE ||
476                 changeInfo.endVisibility == View.VISIBLE);
477     }
478 
479     /**
480      * The default implementation of this method returns a null Animator. Subclasses should
481      * override this method to make targets disappear with the desired transition. The
482      * method should only be called from
483      * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
484      *
485      * @param sceneRoot The root of the transition hierarchy
486      * @param view The View to make disappear. This will be in the target scene's View
487      *             hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
488      *             VISIBLE.
489      * @param startValues The target values in the start scene
490      * @param endValues The target values in the end scene
491      * @return An Animator to be started at the appropriate time in the
492      * overall transition for this scene change. A null value means no animation
493      * should be run.
494      */
onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)495     public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
496             TransitionValues endValues) {
497         return null;
498     }
499 
500     private static class DisappearListener
501             extends TransitionListenerAdapter implements AnimatorListener, AnimatorPauseListener {
502         private final boolean mIsForcedVisibility;
503         private final View mView;
504         private final int mFinalVisibility;
505         private final ViewGroup mParent;
506 
507         private boolean mLayoutSuppressed;
508         private boolean mFinalVisibilitySet = false;
509         boolean mCanceled = false;
510 
DisappearListener(View view, int finalVisibility, boolean isForcedVisibility)511         public DisappearListener(View view, int finalVisibility, boolean isForcedVisibility) {
512             this.mView = view;
513             this.mIsForcedVisibility = isForcedVisibility;
514             this.mFinalVisibility = finalVisibility;
515             this.mParent = (ViewGroup) view.getParent();
516             // Prevent a layout from including mView in its calculation.
517             suppressLayout(true);
518         }
519 
520         @Override
onAnimationPause(Animator animation)521         public void onAnimationPause(Animator animation) {
522             if (!mCanceled && !mIsForcedVisibility) {
523                 mView.setTransitionVisibility(mFinalVisibility);
524             }
525         }
526 
527         @Override
onAnimationResume(Animator animation)528         public void onAnimationResume(Animator animation) {
529             if (!mCanceled && !mIsForcedVisibility) {
530                 mView.setTransitionVisibility(View.VISIBLE);
531             }
532         }
533 
534         @Override
onAnimationCancel(Animator animation)535         public void onAnimationCancel(Animator animation) {
536             mCanceled = true;
537         }
538 
539         @Override
onAnimationRepeat(Animator animation)540         public void onAnimationRepeat(Animator animation) {
541         }
542 
543         @Override
onAnimationStart(Animator animation)544         public void onAnimationStart(Animator animation) {
545         }
546 
547         @Override
onAnimationEnd(Animator animation)548         public void onAnimationEnd(Animator animation) {
549             hideViewWhenNotCanceled();
550         }
551 
552         @Override
onTransitionEnd(Transition transition)553         public void onTransitionEnd(Transition transition) {
554             hideViewWhenNotCanceled();
555         }
556 
557         @Override
onTransitionPause(Transition transition)558         public void onTransitionPause(Transition transition) {
559             suppressLayout(false);
560         }
561 
562         @Override
onTransitionResume(Transition transition)563         public void onTransitionResume(Transition transition) {
564             suppressLayout(true);
565         }
566 
hideViewWhenNotCanceled()567         private void hideViewWhenNotCanceled() {
568             if (!mCanceled) {
569                 if (mIsForcedVisibility) {
570                     mView.setTransitionAlpha(0);
571                 } else if (!mFinalVisibilitySet) {
572                     // Recreate the parent's display list in case it includes mView.
573                     mView.setTransitionVisibility(mFinalVisibility);
574                     if (mParent != null) {
575                         mParent.invalidate();
576                     }
577                     mFinalVisibilitySet = true;
578                 }
579             }
580             // Layout is allowed now that the View is in its final state
581             suppressLayout(false);
582         }
583 
suppressLayout(boolean suppress)584         private void suppressLayout(boolean suppress) {
585             if (mLayoutSuppressed != suppress && mParent != null && !mIsForcedVisibility) {
586                 mLayoutSuppressed = suppress;
587                 mParent.suppressLayout(suppress);
588             }
589         }
590     }
591 }
592