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