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
17package androidx.transition;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.animation.Animator;
22import android.animation.AnimatorListenerAdapter;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.util.AttributeSet;
27import android.view.View;
28import android.view.ViewGroup;
29
30import androidx.annotation.IntDef;
31import androidx.annotation.NonNull;
32import androidx.annotation.Nullable;
33import androidx.annotation.RestrictTo;
34import androidx.core.content.res.TypedArrayUtils;
35
36import java.lang.annotation.Retention;
37import 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 */
52public 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
95    public Visibility() {
96    }
97
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     */
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
132    public int getMode() {
133        return mMode;
134    }
135
136    @Nullable
137    @Override
138    public String[] getTransitionProperties() {
139        return sTransitionProperties;
140    }
141
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
152    public void captureStartValues(@NonNull TransitionValues transitionValues) {
153        captureValues(transitionValues);
154    }
155
156    @Override
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     */
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
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
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")
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     */
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")
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     */
452    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
453            TransitionValues endValues) {
454        return null;
455    }
456
457    @Override
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
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
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
506        public void onAnimationResume(Animator animation) {
507            if (!mCanceled) {
508                ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
509            }
510        }
511
512        @Override
513        public void onAnimationCancel(Animator animation) {
514            mCanceled = true;
515        }
516
517        @Override
518        public void onAnimationRepeat(Animator animation) {
519        }
520
521        @Override
522        public void onAnimationStart(Animator animation) {
523        }
524
525        @Override
526        public void onAnimationEnd(Animator animation) {
527            hideViewWhenNotCanceled();
528        }
529
530        @Override
531        public void onTransitionStart(@NonNull Transition transition) {
532            // Do nothing
533        }
534
535        @Override
536        public void onTransitionEnd(@NonNull Transition transition) {
537            hideViewWhenNotCanceled();
538            transition.removeListener(this);
539        }
540
541        @Override
542        public void onTransitionCancel(@NonNull Transition transition) {
543        }
544
545        @Override
546        public void onTransitionPause(@NonNull Transition transition) {
547            suppressLayout(false);
548        }
549
550        @Override
551        public void onTransitionResume(@NonNull Transition transition) {
552            suppressLayout(true);
553        }
554
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
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