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