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