blob: 70fc0d7e233fbcc6cbaf8605c5edad464d98fa9b [file] [log] [blame]
/*
* 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 <code>mode</code>.
*
* @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 <code>values</code> 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
}