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