Visibility.java revision 8158051cdfef95fc1f22b56bba93b9c610f5ecb1
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    private 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 = -1;
416            originalVisibility = viewToKeep.getVisibility();
417            viewToKeep.setVisibility(View.VISIBLE);
418            Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
419            if (animator != null) {
420                final View finalViewToKeep = viewToKeep;
421                animator.addListener(new AnimatorListenerAdapter() {
422                    boolean mCanceled = false;
423
424                    @Override
425                    public void onAnimationPause(Animator animation) {
426                        if (!mCanceled) {
427                            //noinspection WrongConstant
428                            finalViewToKeep.setVisibility(finalVisibility);
429                        }
430                    }
431
432                    @Override
433                    public void onAnimationResume(Animator animation) {
434                        if (!mCanceled) {
435                            finalViewToKeep.setVisibility(View.VISIBLE);
436                        }
437                    }
438
439                    @Override
440                    public void onAnimationCancel(Animator animation) {
441                        mCanceled = true;
442                    }
443
444                    @Override
445                    public void onAnimationEnd(Animator animation) {
446                        if (!mCanceled) {
447                            //noinspection WrongConstant
448                            finalViewToKeep.setVisibility(finalVisibility);
449                        }
450                    }
451                });
452            } else {
453                viewToKeep.setVisibility(originalVisibility);
454            }
455            return animator;
456        }
457        return null;
458
459
460    }
461
462    /**
463     * The default implementation of this method returns a null Animator. Subclasses should
464     * override this method to make targets disappear with the desired transition. The
465     * method should only be called from
466     * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}.
467     *
468     * @param sceneRoot   The root of the transition hierarchy
469     * @param view        The View to make disappear. This will be in the target scene's View
470     *                    hierarchy or in an {@link android.view.ViewGroupOverlay} and will be
471     *                    VISIBLE.
472     * @param startValues The target values in the start scene
473     * @param endValues   The target values in the end scene
474     * @return An Animator to be started at the appropriate time in the
475     * overall transition for this scene change. A null value means no animation
476     * should be run.
477     */
478    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
479            TransitionValues endValues) {
480        return null;
481    }
482
483    @Override
484    boolean areValuesChanged(TransitionValues oldValues, TransitionValues newValues) {
485        if (oldValues == null && newValues == null) {
486            return false;
487        }
488        VisibilityInfo changeInfo = getVisibilityChangeInfo(oldValues, newValues);
489        return changeInfo.mVisibilityChange && (changeInfo.mStartVisibility == View.VISIBLE
490                || changeInfo.mEndVisibility == View.VISIBLE);
491    }
492
493    // TODO: Implement API 23; isTransitionRequired
494
495}
496