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