文章目录
LayoutTransition关键点分析
关于LayoutTransition
的具体使用,本文不赘述了。本文描述的关键点都是基于默认的动画实现,而非自定义动画。
LayoutTransition
定义的5种动画类型:
-
CHANGE_APPEARING
:有View
进行APPEARING
动画时,其它兄弟View
或View
树上的所有Parent
进行关联变化 -
CHANGE_DISAPPEARING
:有View
进行DISAPPEARING
动画时,其它兄弟View
或View
树上的所有Parent
进行关联变化 -
APPEARING
:出现动画 -
DISAPPEARING
:消失动画 -
CHANGING
:布局变化时(layoutChange()
触发,由ViewGroup.layout()
调用)
以上动画类型除了
CHANGING
都是默认开启的,需要CHANGING
动画需要手动开启
关键点1:当有View
发生消失或隐藏时,关联变化动画除了作用到其它View
,也默认作用到View
树上的所有Parent
看下核心代码:
/**
* This function sets up animations on all of the views that change during layout.
* For every child in the parent, we create a change animation of the appropriate
* type (appearing, disappearing, or changing) and ask it to populate its start values from its
* target view. We add layout listeners to all child views and listen for changes. For
* those views that change, we populate the end values for those animations and start them.
* Animations are not run on unchanging views.
*
* @param parent The container which is undergoing a change.
* @param newView The view being added to or removed from the parent. May be null if the
* changeReason is CHANGING.
* @param changeReason A value of APPEARING, DISAPPEARING, or CHANGING, indicating whether the
* transition is occurring because an item is being added to or removed from the parent, or
* if it is running in response to a layout operation (that is, if the value is CHANGING).
*/
private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) {
Animator baseAnimator = null;
Animator parentAnimator = null;
final long duration;
switch (changeReason) {
case APPEARING:
baseAnimator = mChangingAppearingAnim;
duration = mChangingAppearingDuration;
parentAnimator = defaultChangeIn;
break;
case DISAPPEARING:
baseAnimator = mChangingDisappearingAnim;
duration = mChangingDisappearingDuration;
parentAnimator = defaultChangeOut;
break;
case CHANGING:
baseAnimator = mChangingAnim;
duration = mChangingDuration;
parentAnimator = defaultChange;
break;
default:
// Shouldn't reach here
duration = 0;
break;
}
// If the animation is null, there's nothing to do
if (baseAnimator == null) {
return;
}
// reset the inter-animation delay, in case we use it later
staggerDelay = 0;
final ViewTreeObserver observer = parent.getViewTreeObserver();
if (!observer.isAlive()) {
// If the observer's not in a good state, skip the transition
return;
}
int numChildren = parent.getChildCount();
for (int i = 0; i < numChildren; ++i) {
final View child = parent.getChildAt(i);
// 标记1
// only animate the views not being added or removed
if (child != newView) {
setupChangeAnimation(parent, changeReason, baseAnimator, duration, child);
}
}
// 标记2
if (mAnimateParentHierarchy) {
ViewGroup tempParent = parent;
while (tempParent != null) {
ViewParent parentParent = tempParent.getParent();
if (parentParent instanceof ViewGroup) {
setupChangeAnimation((ViewGroup)parentParent, changeReason, parentAnimator,
duration, tempParent);
tempParent = (ViewGroup) parentParent;
} else {
tempParent = null;
}
}
}
// This is the cleanup step. When we get this rendering event, we know that all of
// the appropriate animations have been set up and run. Now we can clear out the
// layout listeners.
CleanupCallback callback = new CleanupCallback(layoutChangeListenerMap, parent);
observer.addOnPreDrawListener(callback);
parent.addOnAttachStateChangeListener(callback);
}
-
标记1:会遍历当前
ViewGroup
的所有直接child
,调用setupChangeAnimation
-
标记2:如果
mAnimateParentHierarchy == true
,会找到当前child
的所有直接parent
,然后执行setupChangeAnimation
,直到ViewRootImpl
才停止
mAnimateParentHierarchy
默认为true
,所以关联变化动画除了作用到其它View
,也默认作用到View
树上的所有parent
。
-
Q:考虑下为什么要设计成当前
View
的所有直接parent
也要响应变化动画呢? -
A:因为
ViewGroup
的宽高可能是非固定,当发生child
显示隐藏的时候,ViewGroup
本身的大小可能也发生变化,而此时就需要ViewGroup
也进行变化动画,同样的,ViewGroup
的大小变化需要一级级的传导到更高层级的parent
来响应变化动画。假设你页面是这样的层级关系:
DecorView->ScrollView(高度铺满)->LinearLayout(竖直方向,并设置LayoutTransition)->n个child(总计大小超过屏幕空间)
,然后现在是滑动到最后一个child
的状态,此时需要隐藏最后一个child
,此时LineayLayout
的高度就变小,需要执行变化动画,而ScrollView
高度虽然不变,但是由于最后一个child
隐藏,竖直滚动位置scrollY
发生改变,所以也需要进行变化动画,最终动画效果就是:child
渐隐消失,整体也渐渐向上滑动,达到一个比较好的布局变化过渡效果。
关键点2:关联变化动画触发的条件
接下来分析一下setupChangeAnimation
的实现:
/**
* Utility function called by runChangingTransition for both the children and the parent
* hierarchy.
*/
private void setupChangeAnimation(final ViewGroup parent, final int changeReason,
Animator baseAnimator, final long duration, final View child) {
// If we already have a listener for this child, then we've already set up the
// changing animation we need. Multiple calls for a child may occur when several
// add/remove operations are run at once on a container; each one will trigger
// changes for the existing children in the container.
if (layoutChangeListenerMap.get(child) != null) {
return;
}
// Don't animate items up from size(0,0); this is likely because the objects
// were offscreen/invisible or otherwise measured to be infinitely small. We don't
// want to see them animate into their real size; just ignore animation requests
// on these views
if (child.getWidth() == 0 && child.getHeight() == 0) {
return;
}
// Make a copy of the appropriate animation
final Animator anim = baseAnimator.clone();
// Set the target object for the animation
anim.setTarget(child);
// 标记1
// A ObjectAnimator (or AnimatorSet of them) can extract start values from
// its target object
anim.setupStartValues();
// If there's an animation running on this view already, cancel it
Animator currentAnimation = pendingAnimations.get(child);
if (currentAnimation != null) {
currentAnimation.cancel();
pendingAnimations.remove(child);
}
// Cache the animation in case we need to cancel it later
pendingAnimations.put(child, anim);
// For the animations which don't get started, we have to have a means of
// removing them from the cache, lest we leak them and their target objects.
// We run an animator for the default duration+100 (an arbitrary time, but one
// which should far surpass the delay between setting them up here and
// handling layout events which start them.
ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f).
setDuration(duration + 100);
pendingAnimRemover.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
pendingAnimations.remove(child);
}
});
pendingAnimRemover.start();
// Add a listener to track layout changes on this view. If we don't get a callback,
// then there's nothing to animate.
final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() {
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
// 标记3
// Tell the animation to extract end values from the changed object
anim.setupEndValues();
if (anim instanceof ValueAnimator) {
boolean valuesDiffer = false;
ValueAnimator valueAnim = (ValueAnimator)anim;
PropertyValuesHolder[] oldValues = valueAnim.getValues();
for (int i = 0; i < oldValues.length; ++i) {
PropertyValuesHolder pvh = oldValues[i];
if (pvh.mKeyframes instanceof KeyframeSet) {
KeyframeSet keyframeSet = (KeyframeSet) pvh.mKeyframes;
if (keyframeSet.mFirstKeyframe == null ||
keyframeSet.mLastKeyframe == null ||
!keyframeSet.mFirstKeyframe.getValue().equals(
keyframeSet.mLastKeyframe.getValue())) {
valuesDiffer = true;
}
} else if (!pvh.mKeyframes.getValue(0).equals(pvh.mKeyframes.getValue(1))) {
valuesDiffer = true;
}
}
if (!valuesDiffer) {
return;
}
}
long startDelay = 0;
switch (changeReason) {
case APPEARING:
startDelay = mChangingAppearingDelay + staggerDelay;
staggerDelay += mChangingAppearingStagger;
if (mChangingAppearingInterpolator != sChangingAppearingInterpolator) {
anim.setInterpolator(mChangingAppearingInterpolator);
}
break;
case DISAPPEARING:
startDelay = mChangingDisappearingDelay + staggerDelay;
staggerDelay += mChangingDisappearingStagger;
if (mChangingDisappearingInterpolator !=
sChangingDisappearingInterpolator) {
anim.setInterpolator(mChangingDisappearingInterpolator);
}
break;
case CHANGING:
startDelay = mChangingDelay + staggerDelay;
staggerDelay += mChangingStagger;
if (mChangingInterpolator != sChangingInterpolator) {
anim.setInterpolator(mChangingInterpolator);
}
break;
}
anim.setStartDelay(startDelay);
anim.setDuration(duration);
Animator prevAnimation = currentChangingAnimations.get(child);
if (prevAnimation != null) {
prevAnimation.cancel();
}
Animator pendingAnimation = pendingAnimations.get(child);
if (pendingAnimation != null) {
pendingAnimations.remove(child);
}
// 标记4
// Cache the animation in case we need to cancel it later
currentChangingAnimations.put(child, anim);
// 标记5
parent.requestTransitionStart(LayoutTransition.this);
// this only removes listeners whose views changed - must clear the
// other listeners later
child.removeOnLayoutChangeListener(this);
layoutChangeListenerMap.remove(child);
}
};
// Remove the animation from the cache when it ends
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
if (hasListeners()) {
ArrayList<TransitionListener> listeners =
(ArrayList<TransitionListener>) mListeners.clone();
for (TransitionListener listener : listeners) {
listener.startTransition(LayoutTransition.this, parent, child,
changeReason == APPEARING ?
CHANGE_APPEARING : changeReason == DISAPPEARING ?
CHANGE_DISAPPEARING : CHANGING);
}
}
}
@Override
public void onAnimationCancel(Animator animator) {
child.removeOnLayoutChangeListener(listener);
layoutChangeListenerMap.remove(child);
}
@Override
public void onAnimationEnd(Animator animator) {
currentChangingAnimations.remove(child);
if (hasListeners()) {
ArrayList<TransitionListener> listeners =
(ArrayList<TransitionListener>) mListeners.clone();
for (TransitionListener listener : listeners) {
listener.endTransition(LayoutTransition.this, parent, child,
changeReason == APPEARING ?
CHANGE_APPEARING : changeReason == DISAPPEARING ?
CHANGE_DISAPPEARING : CHANGING);
}
}
}
});
// 标记2
child.addOnLayoutChangeListener(listener);
// cache the listener for later removal
layoutChangeListenerMap.put(child, listener);
}
/**
* Constructs a LayoutTransition object. By default, the object will listen to layout
* events on any ViewGroup that it is set on and will run default animations for each
* type of layout event.
*/
public LayoutTransition() {
if (defaultChangeIn == null) {
// 标记0
// "left" is just a placeholder; we'll put real properties/values in when needed
PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1);
PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1);
PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1);
PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1);
PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1);
defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object)null,
pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY);
defaultChangeIn.setDuration(DEFAULT_DURATION);
defaultChangeIn.setStartDelay(mChangingAppearingDelay);
defaultChangeIn.setInterpolator(mChangingAppearingInterpolator);
defaultChangeOut = defaultChangeIn.clone();
defaultChangeOut.setStartDelay(mChangingDisappearingDelay);
defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator);
defaultChange = defaultChangeIn.clone();
defaultChange.setStartDelay(mChangingDelay);
defaultChange.setInterpolator(mChangingInterpolator);
defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f);
defaultFadeIn.setDuration(DEFAULT_DURATION);
defaultFadeIn.setStartDelay(mAppearingDelay);
defaultFadeIn.setInterpolator(mAppearingInterpolator);
defaultFadeOut = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f);
defaultFadeOut.setDuration(DEFAULT_DURATION);
defaultFadeOut.setStartDelay(mDisappearingDelay);
defaultFadeOut.setInterpolator(mDisappearingInterpolator);
}
mChangingAppearingAnim = defaultChangeIn;
mChangingDisappearingAnim = defaultChangeOut;
mChangingAnim = defaultChange;
mAppearingAnim = defaultFadeIn;
mDisappearingAnim = defaultFadeOut;
}
-
标记1:首先会调用
anim.setupStartValues()
确定动画的初始值,对于默认动画实现来说,核心是确定当前child
的left,top,right,bottom,scrollX,scrollY
,默认动画的初始化代码在标记0
注意这边函数参数虽然叫
child
,但实际可能是布局树上的各个parent
-
标记2:调用
child.addOnLayoutChangeListener(listener)
监听当前child
的布局变化通知,以便确定动画的结束值,这里隐藏了一个条件,就是child
如果可见性如果也设置为View.GONE
,那变化监听可能并不会得到回调(系统实现的ViewGroup
控件,onLayout
过程都过滤掉可见性为View.GONE
的child
) -
标记3:调用
anim.setupEndValues()
确定动画的结束值,然后判断动画关键帧的首帧和结束帧是否一致,如果一致,表示当前child
并未产生位置上的实际变化,无需执行关联动画,直接结束代码 -
标记4:调用
currentChangingAnimations.put(child, anim)
,把当前动画添加到currentChangingAnimations
集合,以便后续统一调度(开始、结束、取消),同时还有一个作用,执行变化动画过程中禁用ViewGroup.layout()
执行,下文会介绍 -
标记5:调用
parent.requestTransitionStart(LayoutTransition.this)
,把当前LayoutTransition
通过ViewGroup
间接调用,添加到当前ViewRootImpl
中,而变化动画的开始将由当前帧的绘制调度触发,变化动画开始将会把child
相关属性进行修改,这样就达到了从初始位置过渡到最终位置的动画效果public abstract class ViewGroup extends View implements ViewParent, ViewManager { /** * This method is called by LayoutTransition when there are 'changing' animations that need * to start after the layout/setup phase. The request is forwarded to the ViewAncestor, who * starts all pending transitions prior to the drawing phase in the current traversal. * * @param transition The LayoutTransition to be started on the next traversal. * * @hide */ public void requestTransitionStart(LayoutTransition transition) { ViewRootImpl viewAncestor = getViewRootImpl(); if (viewAncestor != null) { viewAncestor.requestTransitionStart(transition); } } } public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks { /** * Add LayoutTransition to the list of transitions to be started in the next traversal. * This list will be cleared after the transitions on the list are start()'ed. These * transitionsa re added by LayoutTransition itself when it sets up animations. The setup * happens during the layout phase of traversal, which we want to complete before any of the * animations are started (because those animations may side-effect properties that layout * depends upon, like the bounding rectangles of the affected views). So we add the transition * to the list and it is started just prior to starting the drawing phase of traversal. * * @param transition The LayoutTransition to be started on the next traversal. * * @hide */ public void requestTransitionStart(LayoutTransition transition) { if (mPendingTransitions == null || !mPendingTransitions.contains(transition)) { if (mPendingTransitions == null) { mPendingTransitions = new ArrayList<LayoutTransition>(); } mPendingTransitions.add(transition); } } } public class LayoutTransition { /** * Starts the animations set up for a CHANGING transition. We separate the setup of these * animations from actually starting them, to avoid side-effects that starting the animations * may have on the properties of the affected objects. After setup, we tell the affected parent * that this transition should be started. The parent informs its ViewAncestor, which then * starts the transition after the current layout/measurement phase, just prior to drawing * the view hierarchy. * * @hide */ public void startChangingAnimations() { LinkedHashMap<View, Animator> currentAnimCopy = (LinkedHashMap<View, Animator>) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { if (anim instanceof ObjectAnimator) { ((ObjectAnimator) anim).setCurrentPlayTime(0); } anim.start(); } } }
由以上分析可以知道,setupChangeAnimation
的作用就是找到所有布局位置发生实际变化且可见性不为View.GONE
的View
,并执行相应的变化动画。
关键点3:变化动画过程中,抑制ViewGroup.layout
的执行防止出现变化动画过程发生闪动
当ViewGroup
有变化动画在执行,当此时由于某些原因(比如有其它非当前ViewGroup
的child
发生显示隐藏)导致布局树产生了layout
调度,此时可能会出现layout
调度所在的当前帧绘制,画面为最终位置。
产生原因也很容易理解,对属性动画原理有了解的应该知道,动画调度在整个Choreographer
的管理中,属性动画的调度执行比ViewRootImpl.doTraversal()
的调度执行来得早,当某一帧调度需要layout
时,这一帧的属性动画执行已经把View
的属性设置为动画中说应该出现的位置,而随后的layout
执行又把该View
的位置覆盖为实际布局后应该出现的位置,这样在紧接着的draw
调度,绘制的位置就是最终的实际位置,而不是动画中应该出现的位置,也就会产生动画过程中发生闪动现象。
LayoutTransition
在设计之初其实已经考虑到了该情况,对动画过程中的layout
进行抑制,来看下ViewGroup
的相关源码:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
/**
* Sets the LayoutTransition object for this ViewGroup. If the LayoutTransition object is
* not null, changes in layout which occur because of children being added to or removed from
* the ViewGroup will be animated according to the animations defined in that LayoutTransition
* object. By default, the transition object is null (so layout changes are not animated).
*
* <p>Replacing a non-null transition will cause that previous transition to be
* canceled, if it is currently running, to restore this container to
* its correct post-transition state.</p>
*
* @param transition The LayoutTransition object that will animated changes in layout. A value
* of <code>null</code> means no transition will run on layout changes.
* @attr ref android.R.styleable#ViewGroup_animateLayoutChanges
*/
public void setLayoutTransition(LayoutTransition transition) {
if (mTransition != null) {
LayoutTransition previousTransition = mTransition;
previousTransition.cancel();
previousTransition.removeTransitionListener(mLayoutTransitionListener);
}
mTransition = transition;
if (mTransition != null) {
// 标记1
mTransition.addTransitionListener(mLayoutTransitionListener);
}
}
@Override
public final void layout(int l, int t, int r, int b) {
// 标记2
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// 标记3
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
private LayoutTransition.TransitionListener mLayoutTransitionListener =
new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
// We only care about disappearing items, since we need special logic to keep
// those items visible after they've been 'removed'
if (transitionType == LayoutTransition.DISAPPEARING) {
startViewTransition(view);
}
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container,
View view, int transitionType) {
// 标记4
if (mLayoutCalledWhileSuppressed && !transition.isChangingLayout()) {
requestLayout();
mLayoutCalledWhileSuppressed = false;
}
if (transitionType == LayoutTransition.DISAPPEARING && mTransitioningViews != null) {
endViewTransition(view);
}
}
};
}
- 标记1:设置
LayoutTransition
时添加一个LayoutTransition.TransitionListener
监听 - 标记2:当前
ViewGroup
重写了layout
,当mTransition.isChangingLayout()
为false
时,执行super.layout()
,进行正常layout
分发,反之,仅执行标记3,抑制了layout
执行 - 标记3:
mLayoutCalledWhileSuppressed = true
更新抑制标记 - 标记4:当
mLayoutCalledWhileSuppressed
为true
,且transition.isChangingLayout()
为false
,表示所有关联的变化动画执行结束,需要请求layout
调度,恢复之前被抑制的layout
调度
可见LayoutTransition
设计考虑到了执行变化动画过程中抑制layout
调度防止动画过程闪动,但为什么还是可能发生闪动现象?
从源码分析中可知道,抑制layout
仅仅作用于当前设置了LayoutTransition
且有变化动画正在执行的ViewGroup
,假设当前ViewGroup
虽然有执行变化动画并且抑制了layout
,但是ViewGroup
的直接parent
,以及更高层级的parent
也可能都有变化动画,但是当执行动画过程中发生layout
调度,这些parent
并不会抑制layout
执行(除非本身也满足抑制条件),导致这些parent
在动画过程中layout
到了最终位置,导致闪动现象的发生。
要解决这样的闪动现象,就需要根据该原理,去抑制更高层级parent
的layout
调度。
从
Android Q
开始,可以主动调用ViewGroup.suppressLayout()
来抑制layout
执行
关键点4:多个child
同时进行显示隐藏,最后执行的可见性改变动画才生效
比如ViewGroup
有两个child
,设置一个显示另一个隐藏,此时两个child
并不会分别执行渐隐渐显动画,而是只会执行最后设置可见性发生变化的相应动画,原因如下:
/**
* This method is called by ViewGroup when a child view is about to be removed from the
* container. This callback starts the process of a transition; we grab the starting
* values, listen for changes to all of the children of the container, and start appropriate
* animations.
*
* @param parent The ViewGroup from which the View is being removed.
* @param child The View being removed from the ViewGroup.
* @param changesLayout Whether the removal will cause changes in the layout of other views
* in the container. Views becoming INVISIBLE will not cause changes and thus will not
* affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations.
*/
private void removeChild(ViewGroup parent, View child, boolean changesLayout) {
if (parent.getWindowVisibility() != View.VISIBLE) {
return;
}
if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
// 标记1
// Want appearing animations to finish up before proceeding
cancel(APPEARING);
}
if (changesLayout &&
(mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
// Also, cancel changing animations so that we start fresh ones from current locations
cancel(CHANGE_DISAPPEARING);
cancel(CHANGING);
}
if (hasListeners() && (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
ArrayList<TransitionListener> listeners = (ArrayList<TransitionListener>) mListeners
.clone();
for (TransitionListener listener : listeners) {
listener.startTransition(this, parent, child, DISAPPEARING);
}
}
if (changesLayout &&
(mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {
runChangeTransition(parent, child, DISAPPEARING);
}
if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) {
// 标记2
runDisappearingTransition(parent, child);
}
}
/**
* This method runs the animation that makes a removed item disappear.
*
* @param parent The ViewGroup from which the View is being removed.
* @param child The View being removed from the ViewGroup.
*/
private void runDisappearingTransition(final ViewGroup parent, final View child) {
Animator currentAnimation = currentAppearingAnimations.get(child);
if (currentAnimation != null) {
currentAnimation.cancel();
}
if (mDisappearingAnim == null) {
if (hasListeners()) {
ArrayList<TransitionListener> listeners =
(ArrayList<TransitionListener>) mListeners.clone();
for (TransitionListener listener : listeners) {
listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING);
}
}
return;
}
Animator anim = mDisappearingAnim.clone();
anim.setStartDelay(mDisappearingDelay);
anim.setDuration(mDisappearingDuration);
if (mDisappearingInterpolator != sDisappearingInterpolator) {
anim.setInterpolator(mDisappearingInterpolator);
}
anim.setTarget(child);
final float preAnimAlpha = child.getAlpha();
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator anim) {
currentDisappearingAnimations.remove(child);
child.setAlpha(preAnimAlpha);
if (hasListeners()) {
ArrayList<TransitionListener> listeners =
(ArrayList<TransitionListener>) mListeners.clone();
for (TransitionListener listener : listeners) {
listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING);
}
}
}
});
if (anim instanceof ObjectAnimator) {
((ObjectAnimator) anim).setCurrentPlayTime(0);
}
// 标记3
currentDisappearingAnimations.put(child, anim);
anim.start();
}
/**
* Cancels the specified type of transition. Note that we cancel() the changing animations
* but end() the visibility animations. This is because this method is currently called
* in the context of starting a new transition, so we want to move things from their mid-
* transition positions, but we want them to have their end-transition visibility.
*
* @hide
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
public void cancel(int transitionType) {
switch (transitionType) {
case CHANGE_APPEARING:
case CHANGE_DISAPPEARING:
case CHANGING:
if (currentChangingAnimations.size() > 0) {
LinkedHashMap<View, Animator> currentAnimCopy =
(LinkedHashMap<View, Animator>) currentChangingAnimations.clone();
for (Animator anim : currentAnimCopy.values()) {
anim.cancel();
}
currentChangingAnimations.clear();
}
break;
case APPEARING:
// 标记4
if (currentAppearingAnimations.size() > 0) {
LinkedHashMap<View, Animator> currentAnimCopy =
(LinkedHashMap<View, Animator>) currentAppearingAnimations.clone();
for (Animator anim : currentAnimCopy.values()) {
anim.end();
}
currentAppearingAnimations.clear();
}
break;
case DISAPPEARING:
if (currentDisappearingAnimations.size() > 0) {
LinkedHashMap<View, Animator> currentAnimCopy =
(LinkedHashMap<View, Animator>) currentDisappearingAnimations.clone();
for (Animator anim : currentAnimCopy.values()) {
anim.end();
}
currentDisappearingAnimations.clear();
}
break;
}
}
- 标记1:当前为
removeChild
调用,并且有启用DISAPPEARING
动画,就调用cancel(APPEARING)
,执行标记4 - 标记2:调用
runDisappearingTransition
给当前child
添加DISAPPEARING
动画 - 标记3:添加到
currentDisappearingAnimations
集合中 - 标记4:对于
cancel(APPEARING)
来说,会结束掉当前列表中的APPEARING
动画,如果此时已经先执行了addChild
调用,那么刚被添加到currentAppearingAnimations
集合中的APPERAING
动画会被结束掉
由于addChild
和removeChild
是相反,这边就不贴代码了,可见,在连续调用多个child
的可见性变化时,且多个child
的可见性变化不相同,会导致先调用的可见性变化创建的动画被立刻结束掉。
所以对于常见的ViewGroup
中两个child
,想同时执行一个显示一个隐藏的动画需求,LayoutTransition
并不支持。
关键点5:View.GONE
和View.INVISIBLE
之间变化导致View
也执行DISAPPEARING
动画
这个问题有人提
bug
给官方,但是回复说不修复???相关链接:https://issuetracker.google.com/issues/62078636
看下ViewGroup
源码分析为何会产生这个问题:
/**
* Called when a view's visibility has changed. Notify the parent to take any appropriate
* action.
*
* @param child The view whose visibility has changed
* @param oldVisibility The previous visibility value (GONE, INVISIBLE, or VISIBLE).
* @param newVisibility The new visibility value (GONE, INVISIBLE, or VISIBLE).
* @hide
*/
@UnsupportedAppUsage
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
if (mTransition != null) {
if (newVisibility == VISIBLE) {
mTransition.showChild(this, child, oldVisibility);
} else {
// 标记1
mTransition.hideChild(this, child, newVisibility);
if (mTransitioningViews != null && mTransitioningViews.contains(child)) {
// Only track this on disappearing views - appearing views are already visible
// and don't need special handling during drawChild()
if (mVisibilityChangingChildren == null) {
mVisibilityChangingChildren = new ArrayList<View>();
}
mVisibilityChangingChildren.add(child);
addDisappearingView(child);
}
}
}
// in all cases, for drags
if (newVisibility == VISIBLE && mCurrentDragStartEvent != null) {
if (!mChildrenInterestedInDrag.contains(child)) {
notifyChildOfDragStart(child);
}
}
}
/**
* Add a view which is removed from mChildren but still needs animation
*
* @param v View to add
*/
private void addDisappearingView(View v) {
ArrayList<View> disappearingChildren = mDisappearingChildren;
if (disappearingChildren == null) {
disappearingChildren = mDisappearingChildren = new ArrayList<View>();
}
// 标记2
disappearingChildren.add(v);
}
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
}
}
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
controller.start();
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int clipSaveCount = 0;
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
int transientIndex = transientCount != 0 ? 0 : -1;
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList<View> preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
while (transientIndex >= 0) {
// there may be additional transient views after the normal views
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
break;
}
}
if (preorderedList != null) preorderedList.clear();
// 标记3
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList<View> disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
if (usingRenderNodeProperties) canvas.insertInorderBarrier();
if (isShowingLayoutBounds()) {
onDebugDraw(canvas);
}
if (clipToPadding) {
canvas.restoreToCount(clipSaveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
// We want to erase the drawing cache and notify the listener after the
// next frame is drawn because one extra invalidate() is caused by
// drawChild() after the animation is over
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
@Override
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}
- 标记1:当
child
的可见性变为非VISIBLE
时,调用mTransition.hideChild(this, child, newVisibility)
,内部会对该child
添加DISAPPEARING
动画,进而导致mTransitioningViews.contains(child)
满足条件执行addDisappearingView(child)
- 标记2:把
child
添加到mDisappearingChildren
集合 - 标记3:重点来了,在
ViewGroup
的绘制分发中,正常来说对于不可见的控件是跳过绘制的,但是对于有动画的child
,还是会去分发绘制,此时会对所有mDisappearingChildren
中的child
分发绘制
可见该问题的产生,是因为在View.GONE
和View.INVISIBLE
来回变化的时候, child
会被LayoutTransition
当成从可见到不可见来处理去执行DISAPPEARING
动画,进而导致这个过程会把child
绘制出来。