• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 androidx.transition;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.content.res.XmlResourceParser;
26 import android.util.AttributeSet;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import androidx.annotation.IntDef;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.RestrictTo;
34 import androidx.core.content.res.TypedArrayUtils;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 
39 /**
40  * This transition tracks changes to the visibility of target views in the
41  * start and end scenes. Visibility is determined not just by the
42  * {@link View#setVisibility(int)} state of views, but also whether
43  * views exist in the current view hierarchy. The class is intended to be a
44  * utility for subclasses such as {@link Fade}, which use this visibility
45  * information to determine the specific animations to run when visibility
46  * changes occur. Subclasses should implement one or both of the methods
47  * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)},
48  * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or
49  * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)},
50  * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}.
51  */
52 public abstract class Visibility extends Transition {
53 
54     static final String PROPNAME_VISIBILITY = "android:visibility:visibility";
55     private static final String PROPNAME_PARENT = "android:visibility:parent";
56     private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation";
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     /** @hide */
73     @RestrictTo(LIBRARY_GROUP)
74     @IntDef(flag = true, value = {MODE_IN, MODE_OUT})
75     @Retention(RetentionPolicy.SOURCE)
76     public @interface Mode {
77     }
78 
79     private static final String[] sTransitionProperties = {
80             PROPNAME_VISIBILITY,
81             PROPNAME_PARENT,
82     };
83 
84     private static class VisibilityInfo {
85         boolean mVisibilityChange;
86         boolean mFadeIn;
87         int mStartVisibility;
88         int mEndVisibility;
89         ViewGroup mStartParent;
90         ViewGroup mEndParent;
91     }
92 
93     private int mMode = MODE_IN | MODE_OUT;
94 
Visibility()95     public Visibility() {
96     }
97 
Visibility(Context context, AttributeSet attrs)98     public Visibility(Context context, AttributeSet attrs) {
99         super(context, attrs);
100         TypedArray a = context.obtainStyledAttributes(attrs, Styleable.VISIBILITY_TRANSITION);
101         @Mode
102         int mode = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs,
103                 "transitionVisibilityMode",
104                 Styleable.VisibilityTransition.TRANSITION_VISIBILITY_MODE, 0);
105         a.recycle();
106         if (mode != 0) {
107             setMode(mode);
108         }
109     }
110 
111     /**
112      * Changes the transition to support appearing and/or disappearing Views, depending
113      * on <code>mode</code>.
114      *
115      * @param mode The behavior supported by this transition, a combination of
116      *             {@link #MODE_IN} and {@link #MODE_OUT}.
117      */
setMode(@ode int mode)118     public void setMode(@Mode 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      * @return whether appearing and/or disappearing Views are supported. A combination of
129      * {@link #MODE_IN} and {@link #MODE_OUT}.
130      */
131     @Mode
getMode()132     public int getMode() {
133         return mMode;
134     }
135 
136     @Nullable
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(@onNull TransitionValues transitionValues)152     public void captureStartValues(@NonNull TransitionValues transitionValues) {
153         captureValues(transitionValues);
154     }
155 
156     @Override
captureEndValues(@onNull TransitionValues transitionValues)157     public void captureEndValues(@NonNull 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 VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues,
187             TransitionValues endValues) {
188         final VisibilityInfo visInfo = new VisibilityInfo();
189         visInfo.mVisibilityChange = false;
190         visInfo.mFadeIn = false;
191         if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) {
192             visInfo.mStartVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY);
193             visInfo.mStartParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT);
194         } else {
195             visInfo.mStartVisibility = -1;
196             visInfo.mStartParent = null;
197         }
198         if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) {
199             visInfo.mEndVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY);
200             visInfo.mEndParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT);
201         } else {
202             visInfo.mEndVisibility = -1;
203             visInfo.mEndParent = null;
204         }
205         if (startValues != null && endValues != null) {
206             if (visInfo.mStartVisibility == visInfo.mEndVisibility
207                     && visInfo.mStartParent == visInfo.mEndParent) {
208                 return visInfo;
209             } else {
210                 if (visInfo.mStartVisibility != visInfo.mEndVisibility) {
211                     if (visInfo.mStartVisibility == View.VISIBLE) {
212                         visInfo.mFadeIn = false;
213                         visInfo.mVisibilityChange = true;
214                     } else if (visInfo.mEndVisibility == View.VISIBLE) {
215                         visInfo.mFadeIn = true;
216                         visInfo.mVisibilityChange = true;
217                     }
218                     // no visibilityChange if going between INVISIBLE and GONE
219                 } else /* if (visInfo.mStartParent != visInfo.mEndParent) */ {
220                     if (visInfo.mEndParent == null) {
221                         visInfo.mFadeIn = false;
222                         visInfo.mVisibilityChange = true;
223                     } else if (visInfo.mStartParent == null) {
224                         visInfo.mFadeIn = true;
225                         visInfo.mVisibilityChange = true;
226                     }
227                 }
228             }
229         } else if (startValues == null && visInfo.mEndVisibility == View.VISIBLE) {
230             visInfo.mFadeIn = true;
231             visInfo.mVisibilityChange = true;
232         } else if (endValues == null && visInfo.mStartVisibility == View.VISIBLE) {
233             visInfo.mFadeIn = false;
234             visInfo.mVisibilityChange = true;
235         }
236         return visInfo;
237     }
238 
239     @Nullable
240     @Override
createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)241     public Animator createAnimator(@NonNull ViewGroup sceneRoot,
242             @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) {
243         VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues);
244         if (visInfo.mVisibilityChange
245                 && (visInfo.mStartParent != null || visInfo.mEndParent != null)) {
246             if (visInfo.mFadeIn) {
247                 return onAppear(sceneRoot, startValues, visInfo.mStartVisibility,
248                         endValues, visInfo.mEndVisibility);
249             } else {
250                 return onDisappear(sceneRoot, startValues, visInfo.mStartVisibility,
251                         endValues, visInfo.mEndVisibility
252                 );
253             }
254         }
255         return null;
256     }
257 
258     /**
259      * The default implementation of this method does nothing. Subclasses
260      * should override 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     @SuppressWarnings("UnusedParameters")
onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)274     public Animator onAppear(ViewGroup sceneRoot, 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             View endParent = (View) endValues.view.getParent();
281             TransitionValues startParentValues = getMatchedTransitionValues(endParent,
282                     false);
283             TransitionValues endParentValues = getTransitionValues(endParent, false);
284             VisibilityInfo parentVisibilityInfo =
285                     getVisibilityChangeInfo(startParentValues, endParentValues);
286             if (parentVisibilityInfo.mVisibilityChange) {
287                 return null;
288             }
289         }
290         return onAppear(sceneRoot, endValues.view, startValues, endValues);
291     }
292 
293     /**
294      * The default implementation of this method returns a null Animator. Subclasses should
295      * override this method to make targets appear with the desired transition. The
296      * method should only be called from
297      * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
298      *
299      * @param sceneRoot   The root of the transition hierarchy
300      * @param view        The View to make appear. This will be in the target scene's View
301      *                    hierarchy
302      *                    and
303      *                    will be VISIBLE.
304      * @param startValues The target values in the start scene
305      * @param endValues   The target values in the end scene
306      * @return An Animator to be started at the appropriate time in the
307      * overall transition for this scene change. A null value means no animation
308      * should be run.
309      */
onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)310     public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
311             TransitionValues endValues) {
312         return null;
313     }
314 
315     /**
316      * The default implementation of this method does nothing. Subclasses
317      * should override 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      *
321      * @param sceneRoot       The root of the transition hierarchy
322      * @param startValues     The target values in the start scene
323      * @param startVisibility The target visibility in the start scene
324      * @param endValues       The target values in the end scene
325      * @param endVisibility   The target visibility in the end scene
326      * @return An Animator to be started at the appropriate time in the
327      * overall transition for this scene change. A null value means no animation
328      * should be run.
329      */
330     @SuppressWarnings("UnusedParameters")
onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility)331     public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues,
332             int startVisibility, TransitionValues endValues, int endVisibility) {
333         if ((mMode & MODE_OUT) != MODE_OUT) {
334             return null;
335         }
336 
337         View startView = (startValues != null) ? startValues.view : null;
338         View endView = (endValues != null) ? endValues.view : null;
339         View overlayView = null;
340         View viewToKeep = null;
341         if (endView == null || endView.getParent() == null) {
342             if (endView != null) {
343                 // endView was removed from its parent - add it to the overlay
344                 overlayView = endView;
345             } else if (startView != null) {
346                 // endView does not exist. Use startView only under certain
347                 // conditions, because placing a view in an overlay necessitates
348                 // it being removed from its current parent
349                 if (startView.getParent() == null) {
350                     // no parent - safe to use
351                     overlayView = startView;
352                 } else if (startView.getParent() instanceof View) {
353                     View startParent = (View) startView.getParent();
354                     TransitionValues startParentValues = getTransitionValues(startParent, true);
355                     TransitionValues endParentValues = getMatchedTransitionValues(startParent,
356                             true);
357                     VisibilityInfo parentVisibilityInfo =
358                             getVisibilityChangeInfo(startParentValues, endParentValues);
359                     if (!parentVisibilityInfo.mVisibilityChange) {
360                         overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
361                                 startParent);
362                     } else if (startParent.getParent() == null) {
363                         int id = startParent.getId();
364                         if (id != View.NO_ID && sceneRoot.findViewById(id) != null
365                                 && mCanRemoveViews) {
366                             // no parent, but its parent is unparented  but the parent
367                             // hierarchy has been replaced by a new hierarchy with the same id
368                             // and it is safe to un-parent startView
369                             overlayView = startView;
370                         }
371                     }
372                 }
373             }
374         } else {
375             // visibility change
376             if (endVisibility == View.INVISIBLE) {
377                 viewToKeep = endView;
378             } else {
379                 // Becoming GONE
380                 if (startView == endView) {
381                     viewToKeep = endView;
382                 } else if (mCanRemoveViews) {
383                     overlayView = startView;
384                 } else {
385                     overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
386                             (View) startView.getParent());
387                 }
388             }
389         }
390         final int finalVisibility = endVisibility;
391 
392         if (overlayView != null && startValues != null) {
393             // TODO: Need to do this for general case of adding to overlay
394             int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION);
395             int screenX = screenLoc[0];
396             int screenY = screenLoc[1];
397             int[] loc = new int[2];
398             sceneRoot.getLocationOnScreen(loc);
399             overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft());
400             overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop());
401             final ViewGroupOverlayImpl overlay = ViewGroupUtils.getOverlay(sceneRoot);
402             overlay.add(overlayView);
403             Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues);
404             if (animator == null) {
405                 overlay.remove(overlayView);
406             } else {
407                 final View finalOverlayView = overlayView;
408                 animator.addListener(new AnimatorListenerAdapter() {
409                     @Override
410                     public void onAnimationEnd(Animator animation) {
411                         overlay.remove(finalOverlayView);
412                     }
413                 });
414             }
415             return animator;
416         }
417 
418         if (viewToKeep != null) {
419             int originalVisibility = viewToKeep.getVisibility();
420             ViewUtils.setTransitionVisibility(viewToKeep, View.VISIBLE);
421             Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
422             if (animator != null) {
423                 DisappearListener disappearListener = new DisappearListener(viewToKeep,
424                         finalVisibility, true);
425                 animator.addListener(disappearListener);
426                 AnimatorUtils.addPauseListener(animator, disappearListener);
427                 addListener(disappearListener);
428             } else {
429                 ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility);
430             }
431             return animator;
432         }
433         return null;
434     }
435 
436     /**
437      * The default implementation of this method returns a null Animator. Subclasses should
438      * override this method to make targets disappear with the desired transition. The
439      * method should only be called from
440      * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
441      *
442      * @param sceneRoot   The root of the transition hierarchy
443      * @param view        The View to make disappear. This will be in the target scene's View
444      *                    hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
445      *                    VISIBLE.
446      * @param startValues The target values in the start scene
447      * @param endValues   The target values in the end scene
448      * @return An Animator to be started at the appropriate time in the
449      * overall transition for this scene change. A null value means no animation
450      * should be run.
451      */
onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)452     public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
453             TransitionValues endValues) {
454         return null;
455     }
456 
457     @Override
isTransitionRequired(TransitionValues startValues, TransitionValues newValues)458     public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) {
459         if (startValues == null && newValues == null) {
460             return false;
461         }
462         if (startValues != null && newValues != null
463                 && newValues.values.containsKey(PROPNAME_VISIBILITY)
464                 != startValues.values.containsKey(PROPNAME_VISIBILITY)) {
465             // The transition wasn't targeted in either the start or end, so it couldn't
466             // have changed.
467             return false;
468         }
469         VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues);
470         return changeInfo.mVisibilityChange && (changeInfo.mStartVisibility == View.VISIBLE
471                 || changeInfo.mEndVisibility == View.VISIBLE);
472     }
473 
474     private static class DisappearListener extends AnimatorListenerAdapter
475             implements TransitionListener, AnimatorUtils.AnimatorPauseListenerCompat {
476 
477         private final View mView;
478         private final int mFinalVisibility;
479         private final ViewGroup mParent;
480         private final boolean mSuppressLayout;
481 
482         private boolean mLayoutSuppressed;
483         boolean mCanceled = false;
484 
DisappearListener(View view, int finalVisibility, boolean suppressLayout)485         DisappearListener(View view, int finalVisibility, boolean suppressLayout) {
486             mView = view;
487             mFinalVisibility = finalVisibility;
488             mParent = (ViewGroup) view.getParent();
489             mSuppressLayout = suppressLayout;
490             // Prevent a layout from including mView in its calculation.
491             suppressLayout(true);
492         }
493 
494         // This overrides both AnimatorListenerAdapter and
495         // AnimatorUtilsApi14.AnimatorPauseListenerCompat
496         @Override
onAnimationPause(Animator animation)497         public void onAnimationPause(Animator animation) {
498             if (!mCanceled) {
499                 ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
500             }
501         }
502 
503         // This overrides both AnimatorListenerAdapter and
504         // AnimatorUtilsApi14.AnimatorPauseListenerCompat
505         @Override
onAnimationResume(Animator animation)506         public void onAnimationResume(Animator animation) {
507             if (!mCanceled) {
508                 ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
509             }
510         }
511 
512         @Override
onAnimationCancel(Animator animation)513         public void onAnimationCancel(Animator animation) {
514             mCanceled = true;
515         }
516 
517         @Override
onAnimationRepeat(Animator animation)518         public void onAnimationRepeat(Animator animation) {
519         }
520 
521         @Override
onAnimationStart(Animator animation)522         public void onAnimationStart(Animator animation) {
523         }
524 
525         @Override
onAnimationEnd(Animator animation)526         public void onAnimationEnd(Animator animation) {
527             hideViewWhenNotCanceled();
528         }
529 
530         @Override
onTransitionStart(@onNull Transition transition)531         public void onTransitionStart(@NonNull Transition transition) {
532             // Do nothing
533         }
534 
535         @Override
onTransitionEnd(@onNull Transition transition)536         public void onTransitionEnd(@NonNull Transition transition) {
537             hideViewWhenNotCanceled();
538             transition.removeListener(this);
539         }
540 
541         @Override
onTransitionCancel(@onNull Transition transition)542         public void onTransitionCancel(@NonNull Transition transition) {
543         }
544 
545         @Override
onTransitionPause(@onNull Transition transition)546         public void onTransitionPause(@NonNull Transition transition) {
547             suppressLayout(false);
548         }
549 
550         @Override
onTransitionResume(@onNull Transition transition)551         public void onTransitionResume(@NonNull Transition transition) {
552             suppressLayout(true);
553         }
554 
hideViewWhenNotCanceled()555         private void hideViewWhenNotCanceled() {
556             if (!mCanceled) {
557                 // Recreate the parent's display list in case it includes mView.
558                 ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
559                 if (mParent != null) {
560                     mParent.invalidate();
561                 }
562             }
563             // Layout is allowed now that the View is in its final state
564             suppressLayout(false);
565         }
566 
suppressLayout(boolean suppress)567         private void suppressLayout(boolean suppress) {
568             if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) {
569                 mLayoutSuppressed = suppress;
570                 ViewGroupUtils.suppressLayout(mParent, suppress);
571             }
572         }
573     }
574 
575     // TODO: Implement API 23; isTransitionRequired
576 
577 }
578