文章目录

  • LayoutTransition关键点分析
    • 关键点1:当有`View`发生消失或隐藏时,关联变化动画除了作用到其它`View`,也默认作用到`View`树上的所有`Parent`
    • 关键点2:关联变化动画触发的条件
    • 关键点3:变化动画过程中,抑制`ViewGroup.layout`的执行防止出现变化动画过程发生闪动
    • 关键点4:多个`child`同时进行显示隐藏,最后执行的可见性改变动画才生效
    • 关键点5:`View.GONE`和`View.INVISIBLE`之间变化导致`View`也执行`DISAPPEARING`动画

LayoutTransition关键点分析

关于LayoutTransition的具体使用,本文不赘述了。本文描述的关键点都是基于默认的动画实现,而非自定义动画。

LayoutTransition定义的5种动画类型:

  • CHANGE_APPEARING:有View进行APPEARING动画时,其它兄弟ViewView树上的所有Parent进行关联变化

  • CHANGE_DISAPPEARING:有View进行DISAPPEARING动画时,其它兄弟ViewView树上的所有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 hereduration = 0;break;}// If the animation is null, there's nothing to doif (baseAnimator == null) {return;}// reset the inter-animation delay, in case we use it laterstaggerDelay = 0;final ViewTreeObserver observer = parent.getViewTreeObserver();if (!observer.isAlive()) {// If the observer's not in a good state, skip the transitionreturn;}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 removedif (child != newView) {setupChangeAnimation(parent, changeReason, baseAnimator, duration, child);}}// 标记2if (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 viewsif (child.getWidth() == 0 && child.getHeight() == 0) {return;}// Make a copy of the appropriate animationfinal Animator anim = baseAnimator.clone();// Set the target object for the animationanim.setTarget(child);// 标记1// A ObjectAnimator (or AnimatorSet of them) can extract start values from// its target objectanim.setupStartValues();// If there's an animation running on this view already, cancel itAnimator currentAnimation = pendingAnimations.get(child);if (currentAnimation != null) {currentAnimation.cancel();pendingAnimations.remove(child);}// Cache the animation in case we need to cancel it laterpendingAnimations.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() {@Overridepublic 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 objectanim.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 latercurrentChangingAnimations.put(child, anim);// 标记5parent.requestTransitionStart(LayoutTransition.this);// this only removes listeners whose views changed - must clear the// other listeners laterchild.removeOnLayoutChangeListener(this);layoutChangeListenerMap.remove(child);}};// Remove the animation from the cache when it endsanim.addListener(new AnimatorListenerAdapter() {@Overridepublic 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);}}}@Overridepublic void onAnimationCancel(Animator animator) {child.removeOnLayoutChangeListener(listener);layoutChangeListenerMap.remove(child);}@Overridepublic 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);}}}});// 标记2child.addOnLayoutChangeListener(listener);// cache the listener for later removallayoutChangeListenerMap.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 neededPropertyValuesHolder 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()确定动画的初始值,对于默认动画实现来说,核心是确定当前childleft,top,right,bottom,scrollX,scrollY,默认动画的初始化代码在标记0

    注意这边函数参数虽然叫child,但实际可能是布局树上的各个parent

  • 标记2:调用child.addOnLayoutChangeListener(listener)监听当前child的布局变化通知,以便确定动画的结束值,这里隐藏了一个条件,就是child如果可见性如果也设置为View.GONE,那变化监听可能并不会得到回调(系统实现的ViewGroup控件,onLayout过程都过滤掉可见性为View.GONEchild

  • 标记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.GONEView,并执行相应的变化动画。

关键点3:变化动画过程中,抑制ViewGroup.layout的执行防止出现变化动画过程发生闪动

ViewGroup有变化动画在执行,当此时由于某些原因(比如有其它非当前ViewGroupchild发生显示隐藏)导致布局树产生了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) {// 标记1mTransition.addTransitionListener(mLayoutTransitionListener);}}@Overridepublic final void layout(int l, int t, int r, int b) {// 标记2if (!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 finishesmLayoutCalledWhileSuppressed = true;}}private LayoutTransition.TransitionListener mLayoutTransitionListener =new LayoutTransition.TransitionListener() {@Overridepublic 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);}}@Overridepublic void endTransition(LayoutTransition transition, ViewGroup container,View view, int transitionType) {// 标记4if (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:当mLayoutCalledWhileSuppressedtrue,且transition.isChangingLayout()false,表示所有关联的变化动画执行结束,需要请求layout调度,恢复之前被抑制的layout调度

可见LayoutTransition设计考虑到了执行变化动画过程中抑制layout调度防止动画过程闪动,但为什么还是可能发生闪动现象?

从源码分析中可知道,抑制layout仅仅作用于当前设置了LayoutTransition且有变化动画正在执行的ViewGroup,假设当前ViewGroup虽然有执行变化动画并且抑制了layout,但是ViewGroup的直接parent,以及更高层级的parent也可能都有变化动画,但是当执行动画过程中发生layout调度,这些parent并不会抑制layout执行(除非本身也满足抑制条件),导致这些parent在动画过程中layout到了最终位置,导致闪动现象的发生。

要解决这样的闪动现象,就需要根据该原理,去抑制更高层级parentlayout调度。

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 proceedingcancel(APPEARING);}if (changesLayout &&(mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) {// Also, cancel changing animations so that we start fresh ones from current locationscancel(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) {// 标记2runDisappearingTransition(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() {@Overridepublic 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);}// 标记3currentDisappearingAnimations.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:// 标记4if (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动画会被结束掉

由于addChildremoveChild是相反,这边就不贴代码了,可见,在连续调用多个child的可见性变化时,且多个child的可见性变化不相同,会导致先调用的可见性变化创建的动画被立刻结束掉。

所以对于常见的ViewGroup中两个child,想同时执行一个显示一个隐藏的动画需求,LayoutTransition并不支持。

关键点5:View.GONEView.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 {// 标记1mTransition.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 dragsif (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>();}// 标记2disappearingChildren.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 flagmPrivateFlags &= ~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 internallyfinal 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 viewsfinal 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 animationsif (mDisappearingChildren != null) {final ArrayList<View> disappearingChildren = mDisappearingChildren;final int disappearingCount = disappearingChildren.size() - 1;// Go backwards -- we may delete as animations finishfor (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 overmGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;final Runnable end = new Runnable() {@Overridepublic 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.GONEView.INVISIBLE来回变化的时候, child会被LayoutTransition当成从可见到不可见来处理去执行DISAPPEARING动画,进而导致这个过程会把child绘制出来。

LayoutTransition关键点分析相关推荐

  1. Python字符串关键点分析介绍

    Python字符串关键点有下面几点: 1.一些引号分隔的字符 你可以把字符串看出是Python的一种数据类型,在Python单引号或者双引号之间的字符数组或者连续的字符集合.在python中最常用的引 ...

  2. 参考平面及其高度_施工现场平面布置关键点分析

    施工现场平面布置图是在拟建工程的建筑平面上(包括周围环境),为施工服务的各种临时建筑.临时设施及材料.施工机械等,是施工方案在现场的空间体现.它反映已有建筑与拟建工程间.临时建筑与临时设施间的相互空间 ...

  3. SSD(Single shot multibox detector)目标检测模型架构和设计细节分析

    先给出论文链接:SSD: Single Shot MultiBox Detector 本文将对SSD中一些难以理解的细节做仔细分析,包括了default box和ground truth的结合,def ...

  4. 产品经理的竞品分析报告入门

    先说明一下,这不是一篇讲竞品分析报告的方法论和标准步骤的文章,这里只是汇总了一些关于做竞品分析的思路和方法,并对此做了梳理和归纳,意在为从事产品相关工作做竞品分析时提供更全面.更合理的建议. 前期准备 ...

  5. spd软件系统的发展-医用耗材管理系统功能优势发展及分析

    1.制度保障需不断强化 SPD模式涉及到医院.SPD运营服务单位以及耗材供应商等多个主体,且在医院内部也需要多部门共同参与协作.在初期探索阶段,容易出现权限不清.责任不明的问题,如耗材出入库的监管.问 ...

  6. 新闻稿发布效果的关键点,如何发软文 品牌营销推广怎么做

    在营销界,有人把新闻传播看得很重,几乎每天都发布新闻稿:但也有人对新闻营销嗤之以鼻,几乎不怎么做新闻稿发布.企业经营者何以有如此大的差别? 企业自古就有市场和销售之争,销售有固定的指标,市场宣传可以说 ...

  7. 今日头条的推荐算法原理分析(转)

    链接:https://www.jianshu.com/p/b564c19567b7 今日头条发布了后台的算法原理,不过用词比较考究.说的比较深奥,让人感觉云里雾里不知何处,本篇尽量用通俗语言进行解析, ...

  8. 2022-2028年中国在线旅行预订市场投资分析及前景预测报告

    [报告类型]产业研究 [出版时间]即时更新(交付时间约3个工作日) [发布机构]智研瞻产业研究院 [报告格式]PDF版 本报告介绍了在线旅行行业相关概述.中国在线旅行行业运行环境.分析了中国在线旅行行 ...

  9. 2022-2028年中国硅质原料行业全景调研及投资前景展望报告

    [报告类型]产业研究 [出版时间]即时更新(交付时间约3个工作日) [发布机构]智研瞻产业研究院 [报告格式]PDF版 本报告介绍了硅质原料行业相关概述.中国硅质原料行业运行环境.分析了中国硅质原料行 ...

最新文章

  1. 【青少年编程】【一级】 奔跑的马
  2. DeepMind 的2017:有 AlphaGo,更有社会责任
  3. mysql gtid 搭建主从_MySQL5.7 - 基于GTID复制模式搭建主从复制
  4. linux下使用cat打开文件乱码
  5. 滑雪(信息学奥赛一本通-T1280)
  6. linux执行shell过程日志,Android之在linux终端执行shell脚本直接打印当前运行app的日志...
  7. 计算机研究生可以参加哪些比赛?
  8. salt returner mysql_saltstack mysql returner
  9. 54 小明的存钱计划
  10. 安卓逆向-new-sec6-4 Java反射相关知识以及平头哥框架hook构造函数 | App发布测试版本感染
  11. XXXX is not in the sudoers file. This incident will be reported解决方法
  12. poi2009 切题记
  13. 教师资格证面试 计算机应用,请问下,中职类教师资格证,科目是计算机应用。可..._教师招聘考试_帮考网...
  14. uva11942 Lumberjack Sequencing
  15. Flutter 不容错过的 7 大亮点 | Google I/O 精彩回顾
  16. inn之CTS debug小技巧(1)
  17. Eclipse之jar包修改
  18. uni-app IOS的threeJS本地obj、mtl文件的读取
  19. 盘点7款应用最广泛的 Linux 桌面环境
  20. 每个优秀的人,都有一段沉默的时光

热门文章

  1. Linux通过编程获取CPU核数
  2. 基于poi的动态导出excel表头以及统计行列数据(全网最全)
  3. python 按照excel表头字符串读取单元格数据
  4. php tp5生成条形码,thinkPHP框架实现生成条形码的方法
  5. Python简单爬虫项目
  6. 2022年蓝牙耳机哪款好?公认音质最好的蓝牙耳机
  7. 计算机最快的算法,史上14个最快速算法:孩子的计算能力爆表!大脑堪比计算机!...
  8. android修改桌面app图标的问题,android修改桌面app图标的有关问题
  9. matlab帮助入口
  10. 计算机主机闪烁显示器黑屏,win7系统电脑显示器一闪一闪黑屏的几种解决方法...