LayoutTransition关键点分析

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 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()确定动画的初始值,对于默认动画实现来说,核心是确定当前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) {
            // 标记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到了最终位置,导致闪动现象的发生。

要解决这样的闪动现象,就需要根据该原理,去抑制更高层级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 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动画会被结束掉

由于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 {
            // 标记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.GONEView.INVISIBLE来回变化的时候, child会被LayoutTransition当成从可见到不可见来处理去执行DISAPPEARING动画,进而导致这个过程会把child绘制出来。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值