/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.transition; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.content.res.TypedArrayUtils; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * This transition tracks changes to the visibility of target views in the * start and end scenes. Visibility is determined not just by the * {@link View#setVisibility(int)} state of views, but also whether * views exist in the current view hierarchy. The class is intended to be a * utility for subclasses such as {@link Fade}, which use this visibility * information to determine the specific animations to run when visibility * changes occur. Subclasses should implement one or both of the methods * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}, * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)} or * {@link #onAppear(ViewGroup, View, TransitionValues, TransitionValues)}, * {@link #onDisappear(ViewGroup, View, TransitionValues, TransitionValues)}. */ public abstract class Visibility extends Transition { static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; private static final String PROPNAME_SCREEN_LOCATION = "android:visibility:screenLocation"; /** * Mode used in {@link #setMode(int)} to make the transition * operate on targets that are appearing. Maybe be combined with * {@link #MODE_OUT} to target Visibility changes both in and out. */ public static final int MODE_IN = 0x1; /** * Mode used in {@link #setMode(int)} to make the transition * operate on targets that are disappearing. Maybe be combined with * {@link #MODE_IN} to target Visibility changes both in and out. */ public static final int MODE_OUT = 0x2; /** @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(flag = true, value = {MODE_IN, MODE_OUT}) @Retention(RetentionPolicy.SOURCE) public @interface Mode { } private static final String[] sTransitionProperties = { PROPNAME_VISIBILITY, PROPNAME_PARENT, }; private static class VisibilityInfo { boolean mVisibilityChange; boolean mFadeIn; int mStartVisibility; int mEndVisibility; ViewGroup mStartParent; ViewGroup mEndParent; } private int mMode = MODE_IN | MODE_OUT; public Visibility() { } public Visibility(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, Styleable.VISIBILITY_TRANSITION); @Mode int mode = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs, "transitionVisibilityMode", Styleable.VisibilityTransition.TRANSITION_VISIBILITY_MODE, 0); a.recycle(); if (mode != 0) { setMode(mode); } } /** * Changes the transition to support appearing and/or disappearing Views, depending * on mode. * * @param mode The behavior supported by this transition, a combination of * {@link #MODE_IN} and {@link #MODE_OUT}. */ public void setMode(@Mode int mode) { if ((mode & ~(MODE_IN | MODE_OUT)) != 0) { throw new IllegalArgumentException("Only MODE_IN and MODE_OUT flags are allowed"); } mMode = mode; } /** * Returns whether appearing and/or disappearing Views are supported. * * @return whether appearing and/or disappearing Views are supported. A combination of * {@link #MODE_IN} and {@link #MODE_OUT}. */ @Mode public int getMode() { return mMode; } @Nullable @Override public String[] getTransitionProperties() { return sTransitionProperties; } private void captureValues(TransitionValues transitionValues) { int visibility = transitionValues.view.getVisibility(); transitionValues.values.put(PROPNAME_VISIBILITY, visibility); transitionValues.values.put(PROPNAME_PARENT, transitionValues.view.getParent()); int[] loc = new int[2]; transitionValues.view.getLocationOnScreen(loc); transitionValues.values.put(PROPNAME_SCREEN_LOCATION, loc); } @Override public void captureStartValues(@NonNull TransitionValues transitionValues) { captureValues(transitionValues); } @Override public void captureEndValues(@NonNull TransitionValues transitionValues) { captureValues(transitionValues); } /** * Returns whether the view is 'visible' according to the given values * object. This is determined by testing the same properties in the values * object that are used to determine whether the object is appearing or * disappearing in the {@link * Transition#createAnimator(ViewGroup, TransitionValues, TransitionValues)} * method. This method can be called by, for example, subclasses that want * to know whether the object is visible in the same way that Visibility * determines it for the actual animation. * * @param values The TransitionValues object that holds the information by * which visibility is determined. * @return True if the view reference by values is visible, * false otherwise. */ public boolean isVisible(TransitionValues values) { if (values == null) { return false; } int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); View parent = (View) values.values.get(PROPNAME_PARENT); return visibility == View.VISIBLE && parent != null; } private VisibilityInfo getVisibilityChangeInfo(TransitionValues startValues, TransitionValues endValues) { final VisibilityInfo visInfo = new VisibilityInfo(); visInfo.mVisibilityChange = false; visInfo.mFadeIn = false; if (startValues != null && startValues.values.containsKey(PROPNAME_VISIBILITY)) { visInfo.mStartVisibility = (Integer) startValues.values.get(PROPNAME_VISIBILITY); visInfo.mStartParent = (ViewGroup) startValues.values.get(PROPNAME_PARENT); } else { visInfo.mStartVisibility = -1; visInfo.mStartParent = null; } if (endValues != null && endValues.values.containsKey(PROPNAME_VISIBILITY)) { visInfo.mEndVisibility = (Integer) endValues.values.get(PROPNAME_VISIBILITY); visInfo.mEndParent = (ViewGroup) endValues.values.get(PROPNAME_PARENT); } else { visInfo.mEndVisibility = -1; visInfo.mEndParent = null; } if (startValues != null && endValues != null) { if (visInfo.mStartVisibility == visInfo.mEndVisibility && visInfo.mStartParent == visInfo.mEndParent) { return visInfo; } else { if (visInfo.mStartVisibility != visInfo.mEndVisibility) { if (visInfo.mStartVisibility == View.VISIBLE) { visInfo.mFadeIn = false; visInfo.mVisibilityChange = true; } else if (visInfo.mEndVisibility == View.VISIBLE) { visInfo.mFadeIn = true; visInfo.mVisibilityChange = true; } // no visibilityChange if going between INVISIBLE and GONE } else /* if (visInfo.mStartParent != visInfo.mEndParent) */ { if (visInfo.mEndParent == null) { visInfo.mFadeIn = false; visInfo.mVisibilityChange = true; } else if (visInfo.mStartParent == null) { visInfo.mFadeIn = true; visInfo.mVisibilityChange = true; } } } } else if (startValues == null && visInfo.mEndVisibility == View.VISIBLE) { visInfo.mFadeIn = true; visInfo.mVisibilityChange = true; } else if (endValues == null && visInfo.mStartVisibility == View.VISIBLE) { visInfo.mFadeIn = false; visInfo.mVisibilityChange = true; } return visInfo; } @Nullable @Override public Animator createAnimator(@NonNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues) { VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); if (visInfo.mVisibilityChange && (visInfo.mStartParent != null || visInfo.mEndParent != null)) { if (visInfo.mFadeIn) { return onAppear(sceneRoot, startValues, visInfo.mStartVisibility, endValues, visInfo.mEndVisibility); } else { return onDisappear(sceneRoot, startValues, visInfo.mStartVisibility, endValues, visInfo.mEndVisibility ); } } return null; } /** * The default implementation of this method does nothing. Subclasses * should override if they need to create an Animator when targets appear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. * * @param sceneRoot The root of the transition hierarchy * @param startValues The target values in the start scene * @param startVisibility The target visibility in the start scene * @param endValues The target values in the end scene * @param endVisibility The target visibility in the end scene * @return An Animator to be started at the appropriate time in the * overall transition for this scene change. A null value means no animation * should be run. */ @SuppressWarnings("UnusedParameters") public Animator onAppear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { if ((mMode & MODE_IN) != MODE_IN || endValues == null) { return null; } if (startValues == null) { View endParent = (View) endValues.view.getParent(); TransitionValues startParentValues = getMatchedTransitionValues(endParent, false); TransitionValues endParentValues = getTransitionValues(endParent, false); VisibilityInfo parentVisibilityInfo = getVisibilityChangeInfo(startParentValues, endParentValues); if (parentVisibilityInfo.mVisibilityChange) { return null; } } return onAppear(sceneRoot, endValues.view, startValues, endValues); } /** * The default implementation of this method returns a null Animator. Subclasses should * override this method to make targets appear with the desired transition. The * method should only be called from * {@link #onAppear(ViewGroup, TransitionValues, int, TransitionValues, int)}. * * @param sceneRoot The root of the transition hierarchy * @param view The View to make appear. This will be in the target scene's View * hierarchy * and * will be VISIBLE. * @param startValues The target values in the start scene * @param endValues The target values in the end scene * @return An Animator to be started at the appropriate time in the * overall transition for this scene change. A null value means no animation * should be run. */ public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { return null; } /** * The default implementation of this method does nothing. Subclasses * should override if they need to create an Animator when targets disappear. * The method should only be called by the Visibility class; it is * not intended to be called from external classes. * * @param sceneRoot The root of the transition hierarchy * @param startValues The target values in the start scene * @param startVisibility The target visibility in the start scene * @param endValues The target values in the end scene * @param endVisibility The target visibility in the end scene * @return An Animator to be started at the appropriate time in the * overall transition for this scene change. A null value means no animation * should be run. */ @SuppressWarnings("UnusedParameters") public Animator onDisappear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, TransitionValues endValues, int endVisibility) { if ((mMode & MODE_OUT) != MODE_OUT) { return null; } View startView = (startValues != null) ? startValues.view : null; View endView = (endValues != null) ? endValues.view : null; View overlayView = null; View viewToKeep = null; if (endView == null || endView.getParent() == null) { if (endView != null) { // endView was removed from its parent - add it to the overlay overlayView = endView; } else if (startView != null) { // endView does not exist. Use startView only under certain // conditions, because placing a view in an overlay necessitates // it being removed from its current parent if (startView.getParent() == null) { // no parent - safe to use overlayView = startView; } else if (startView.getParent() instanceof View) { View startParent = (View) startView.getParent(); TransitionValues startParentValues = getTransitionValues(startParent, true); TransitionValues endParentValues = getMatchedTransitionValues(startParent, true); VisibilityInfo parentVisibilityInfo = getVisibilityChangeInfo(startParentValues, endParentValues); if (!parentVisibilityInfo.mVisibilityChange) { overlayView = TransitionUtils.copyViewImage(sceneRoot, startView, startParent); } else if (startParent.getParent() == null) { int id = startParent.getId(); if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { // no parent, but its parent is unparented but the parent // hierarchy has been replaced by a new hierarchy with the same id // and it is safe to un-parent startView overlayView = startView; } } } } } else { // visibility change if (endVisibility == View.INVISIBLE) { viewToKeep = endView; } else { // Becoming GONE if (startView == endView) { viewToKeep = endView; } else { overlayView = startView; } } } final int finalVisibility = endVisibility; if (overlayView != null && startValues != null) { // TODO: Need to do this for general case of adding to overlay int[] screenLoc = (int[]) startValues.values.get(PROPNAME_SCREEN_LOCATION); int screenX = screenLoc[0]; int screenY = screenLoc[1]; int[] loc = new int[2]; sceneRoot.getLocationOnScreen(loc); overlayView.offsetLeftAndRight((screenX - loc[0]) - overlayView.getLeft()); overlayView.offsetTopAndBottom((screenY - loc[1]) - overlayView.getTop()); final ViewGroupOverlayImpl overlay = ViewGroupUtils.getOverlay(sceneRoot); overlay.add(overlayView); Animator animator = onDisappear(sceneRoot, overlayView, startValues, endValues); if (animator == null) { overlay.remove(overlayView); } else { final View finalOverlayView = overlayView; animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { overlay.remove(finalOverlayView); } }); } return animator; } if (viewToKeep != null) { int originalVisibility = viewToKeep.getVisibility(); ViewUtils.setTransitionVisibility(viewToKeep, View.VISIBLE); Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues); if (animator != null) { DisappearListener disappearListener = new DisappearListener(viewToKeep, finalVisibility, true); animator.addListener(disappearListener); AnimatorUtils.addPauseListener(animator, disappearListener); addListener(disappearListener); } else { ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility); } return animator; } return null; } /** * The default implementation of this method returns a null Animator. Subclasses should * override this method to make targets disappear with the desired transition. The * method should only be called from * {@link #onDisappear(ViewGroup, TransitionValues, int, TransitionValues, int)}. * * @param sceneRoot The root of the transition hierarchy * @param view The View to make disappear. This will be in the target scene's View * hierarchy or in an {@link android.view.ViewGroupOverlay} and will be * VISIBLE. * @param startValues The target values in the start scene * @param endValues The target values in the end scene * @return An Animator to be started at the appropriate time in the * overall transition for this scene change. A null value means no animation * should be run. */ public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues) { return null; } @Override public boolean isTransitionRequired(TransitionValues startValues, TransitionValues newValues) { if (startValues == null && newValues == null) { return false; } if (startValues != null && newValues != null && newValues.values.containsKey(PROPNAME_VISIBILITY) != startValues.values.containsKey(PROPNAME_VISIBILITY)) { // The transition wasn't targeted in either the start or end, so it couldn't // have changed. return false; } VisibilityInfo changeInfo = getVisibilityChangeInfo(startValues, newValues); return changeInfo.mVisibilityChange && (changeInfo.mStartVisibility == View.VISIBLE || changeInfo.mEndVisibility == View.VISIBLE); } private static class DisappearListener extends AnimatorListenerAdapter implements TransitionListener, AnimatorUtilsApi14.AnimatorPauseListenerCompat { private final View mView; private final int mFinalVisibility; private final ViewGroup mParent; private final boolean mSuppressLayout; private boolean mLayoutSuppressed; boolean mCanceled = false; DisappearListener(View view, int finalVisibility, boolean suppressLayout) { mView = view; mFinalVisibility = finalVisibility; mParent = (ViewGroup) view.getParent(); mSuppressLayout = suppressLayout; // Prevent a layout from including mView in its calculation. suppressLayout(true); } // This overrides both AnimatorListenerAdapter and // AnimatorUtilsApi14.AnimatorPauseListenerCompat @Override public void onAnimationPause(Animator animation) { if (!mCanceled) { ViewUtils.setTransitionVisibility(mView, mFinalVisibility); } } // This overrides both AnimatorListenerAdapter and // AnimatorUtilsApi14.AnimatorPauseListenerCompat @Override public void onAnimationResume(Animator animation) { if (!mCanceled) { ViewUtils.setTransitionVisibility(mView, View.VISIBLE); } } @Override public void onAnimationCancel(Animator animation) { mCanceled = true; } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { hideViewWhenNotCanceled(); } @Override public void onTransitionStart(@NonNull Transition transition) { // Do nothing } @Override public void onTransitionEnd(@NonNull Transition transition) { hideViewWhenNotCanceled(); transition.removeListener(this); } @Override public void onTransitionCancel(@NonNull Transition transition) { } @Override public void onTransitionPause(@NonNull Transition transition) { suppressLayout(false); } @Override public void onTransitionResume(@NonNull Transition transition) { suppressLayout(true); } private void hideViewWhenNotCanceled() { if (!mCanceled) { // Recreate the parent's display list in case it includes mView. ViewUtils.setTransitionVisibility(mView, mFinalVisibility); if (mParent != null) { mParent.invalidate(); } } // Layout is allowed now that the View is in its final state suppressLayout(false); } private void suppressLayout(boolean suppress) { if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) { mLayoutSuppressed = suppress; ViewGroupUtils.suppressLayout(mParent, suppress); } } } // TODO: Implement API 23; isTransitionRequired }