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