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