RecyclerView notifyItemRemoved 之后的源码分析

源码版本:androidx1.3.2

分析场景:

RecyclerView使用线性布局,方向为竖直方向,布局从上到下,宽高都是 MATCH_PARENT。开始有3条数据。然后移除 position = 1 的数据。

在这里插入图片描述

流程图
在这里插入图片描述

先说下结论:

在 dispatchLayoutStep1 预布局阶段:

  • 给要被移出的 ViewHolder1 添加标记位 ViewHolder.FLAG_REMOVED
    ViewHolder1 标记为 removed,在 fill 方法中不会减去 remainingSpace。所以,fill 方法会继续布局。这个时候 position = 2,会创建一个新的ViewHolder,onBindViewHolder 然后返回。ViewHolder对应的 ItemView 会添加到RecyclerView。 RecyclerView.this.addView(child, index);

  • 现在有3个ViewHolder,RecyclerView 有3个子 View。

在 dispatchLayoutStep2 真正的布局阶段:

  • 在 detachAndScrapAttachedViews 回收 ViewHolder 的时候,Recycler.mAttachedScrap 回收了3个ViewHolder。
  • ViewHolder0 被布局到 position = 0 的位置。
  • ViewHolder2 被布局到 position = 1 的位置。
  • 缓存Recycler.mAttachedScrap 中还有一个 ViewHolder1,就是被移除的。

在 dispatchLayoutStep3 动画阶段:

  • 没有变化的ViewHolder0,没有动画效果。
  • 新创建的ViewHolder2 会执行一个移动动画,从屏幕底部进入到屏幕中。
  • 被移除的ViewHolder1 会执行一个透明度渐出动画,透明度从1变化到0。在动画开始之前,会重新把ViewHolder 对应的 ItemView 重新 attachViewToParent 到 RecyclerView 上 。在动画结束后,会把 这个 ItemView 真正移除,对应的ViewHolder 缓存到 RecycledViewPool(有 ViewHolder.FLAG_REMOVED 是不会被缓存到 Recycler.mCacheViews 中的)。

示例代码如下所示:

rv.layoutManager = LinearLayoutManager(this)
val arrayList = arrayListOf <CheckBoxModel> ()
for(i in 0 until 3) {
    arrayList.add(CheckBoxModel("Hello$i", false))
}
rv.adapter = TestRvTheoryAdapter(this, arrayList)
binding.btnNotifyItemChanged.setOnClickListener {
    testNotifyItemRemoved(arrayList)
}

private fun testNotifyItemRemoved(arrayList: ArrayList < CheckBoxModel > ) {
    arrayList.removeAt(1)
    rv.adapter?.notifyItemRemoved(1)
}

当我们调用Adapter的 notifyItemRemoved 方法的时候,会调用RecyclerView的 requestLayout 方法,然后会调用RecyclerView的 onLayout 方法,然后会调用 RecyclerView 的 dispatchLayout 方法。

void dispatchLayout() {
    //...
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        //注释1处,调用dispatchLayoutStep1方法。
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        //注释2处,调用dispatchLayoutStep2方法。
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        mLayout.setExactMeasureSpecsFrom(this);
    }
    //调用dispatchLayoutStep3方法。
    dispatchLayoutStep3();
}

RecyclerView 的 dispatchLayoutStep1 方法

private void dispatchLayoutStep1() {
    mState.assertLayoutStep(State.STEP_START);
    fillRemainingScrollValues(mState);
    mState.mIsMeasuring = false;
    startInterceptRequestLayout();
    mViewInfoStore.clear();
    onEnterLayoutOrScroll();
    //注释1处,调用processAdapterUpdatesAndSetAnimationFlags方法。处理动画标记位
    processAdapterUpdatesAndSetAnimationFlags();
    //...

    if(mState.mRunSimpleAnimations) {
        // Step 0: Find out where all non-removed items are, pre-layout
        int count = mChildHelper.getChildCount();
        for(int i = 0; i < count; ++i) {
            final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
            if(holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
                continue;
            }
            final ItemHolderInfo animationInfo = mItemAnimator
                .recordPreLayoutInformation(mState, holder,
                    ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
                    holder.getUnmodifiedPayloads());
            //注释2处,保存ViewHolder的动画信息。
            mViewInfoStore.addToPreLayout(holder, animationInfo);
            //...
        }
    }
    if(mState.mRunPredictiveAnimations) {
        // Save old positions so that LayoutManager can run its mapping logic.
        //保存ViewHolder的位置信息
        saveOldPositions();
        final boolean didStructureChange = mState.mStructureChanged;
        mState.mStructureChanged = false;
        //注释3处,布局子View
        mLayout.onLayoutChildren(mRecycler, mState);
        mState.mStructureChanged = didStructureChange;

        for(int i = 0; i < mChildHelper.getChildCount(); ++i) {
            final View child = mChildHelper.getChildAt(i);
            final ViewHolder viewHolder = getChildViewHolderInt(child);
            if(viewHolder.shouldIgnore()) {
                continue;
            }
            //注释4处,新创建的ViewHolder,满足条件,记录新创建的ViewHolder的动画信息。
            if(!mViewInfoStore.isInPreLayout(viewHolder)) {
                int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder);
                boolean wasHidden = viewHolder
                    .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                if(!wasHidden) {
                    flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
                }
                final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation(
                    mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads());
                if(wasHidden) {
                    recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
                } else {
                    mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
                }
            }
        }
        // we don't process disappearing list because they may re-appear in post layout pass.
        clearOldPositions();
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

注释1处,调用processAdapterUpdatesAndSetAnimationFlags方法。处理动画标记位。

private void processAdapterUpdatesAndSetAnimationFlags() {
    //...
    if(predictiveItemAnimationsEnabled()) {
        //注释1处
        mAdapterHelper.preProcess();
    } else {
        mAdapterHelper.consumeUpdatesInOnePass();
    }
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    
    // mState.mRunSimpleAnimations = true
    mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null && (mDataSetHasChangedAfterLayout || animationTypeSupported || mLayout.mRequestedSimpleAnimations) && (!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds());
    
    // mState.mRunPredictiveAnimations = true
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations && animationTypeSupported && !mDataSetHasChangedAfterLayout && predictiveItemAnimationsEnabled();
}

这里我们说一下这个方法的做的一些事情,就不一步一步跟了:

首先会改变 position =1 位置上的 ViewHolder,我我们看一下改变之后的 ViewHolder1 的信息。Evaluate ViewHolder1:

ViewHolder1: ViewHolder{40b6a27 position=0 id=-1, oldPos=1, pLpos:1 removed}

  • 给要被移出的 ViewHolder1 添加标记位 ViewHolder.FLAG_REMOVED
  • 保存旧的位置。 oldPos=1, pLpos:1 ,保存新的位置 position=0

回到 dispatchLayoutStep1 方法注释2处,保存ViewHolder的动画信息。

注释3处,调用 LayoutManager 的 onLayoutChildren 方法布局子View。

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //...
    //注释1处,detachAndScrapAttachedViews
    detachAndScrapAttachedViews(recycler);
    //...
    mLayoutState.mNoRecycleSpace = 0;
    if (mAnchorInfo.mLayoutFromEnd) {//正常情况为该条件不满足。我们分析else的情况。
        //...
    } else {
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForEnd;
        //注释2处,从锚点开始向end方向填充
        fill(recycler, mLayoutState, state, false);
        //...
    }
    //...
}

注释1处,detachAndScrapAttachedViews。所有的子View 会被 detachFromParent ,缓存在 Recycler.mAttachedScrap 中。

注释2处,从锚点开始向end方向填充。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    //记录开始填充的时候,可用的空间
    final int start = layoutState.mAvailable;
    //...
    // 剩余的空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    //循环填充子View,只要还有剩余空间并且还有数据
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //这里会把 layoutChunkResult.mIgnoreConsumed 重置为 false
        layoutChunkResult.resetInternal();
        //获取并添加子View,然后测量、布局子View并将分割线考虑在内。
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        //如果没有更多View了,布局结束,跳出循环
        if (layoutChunkResult.mFinished) {
            break;
        }
        //增加偏移量,加上已经填充的像素
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
        //注释1处,注意这里的逻辑,如果填充的View是被移除的,就不减去remainingSpace。
        //注释1处,注意这里的逻辑,如果填充的View是被移除的,就不减去remainingSpace。
        //注释1处,注意这里的逻辑,如果填充的View是被移除的,就不减去remainingSpace。
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            //可用空间减去已经填充的像素        
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            //剩余空间,减去已经填充的像素
            remainingSpace -= layoutChunkResult.mConsumed;
        }
    }
    // 返回已经填充的空间,比如开始可用空间 start 是1920,填充完毕,可用空间 layoutState.mAvailable 是120,就返回 1800 。填充了1800像素。
    //返回结果有可能大于start,因为最后一个填充的View有一部分在屏幕外面。
    return start - layoutState.mAvailable;
}

**注释1处,注意这里的逻辑,如果填充的View是被移除的,就不减去remainingSpace。**因为在layoutChunk 方法中,将 result.mIgnoreConsumed 置为true了。

layoutChunk方法部分逻辑

//layoutChunk方法部分逻辑
if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
}

ViewHolder1 标记为 removed,在 fill 方法中不会减去 remainingSpace。所以,fill 方法会继续布局。这个时候 position = 2,会创建一个新的ViewHolder,onBindViewHolder 然后返回。ViewHolder对应的 ItemView 会添加到RecyclerView。 RecyclerView.this.addView(child, index);

//新创建的ViewHolder,
// 我们可以看到一些信息,在预布局的时候,pLpos:2,真正的位置 position=1
// 说明在后期需要执行一个动画 从 position=2 的位置,移动到 position=1 的位置。
ViewHolder{2390807 position=1 id=-1, oldPos=-1, pLpos:2 no parent}

回到 dispatchLayoutStep1 方法,注释4处,新创建的ViewHolder,满足条件,记录新创建的ViewHolder的动画信息。

dispatchLayoutStep1 结束,总结一下:

  • 在预布局阶段,有一个新创建的 ViewHolder2,对应的 ItemView 会添加到RecyclerView。 RecyclerView.this.addView(child, index);。这个时候 RecyclerView 是有3个子 View 的。
  • 记录新创建的 ViewHolder2 的动画信息。
  • 现在有3个Item。有标记位为 removed 的 ViewHolder1,是被移除的。

然后,进入 dispatchLayoutStep2 方法,内部再次调用 mLayout.onLayoutChildren(mRecycler, mState);

回收ViewHolder的时候,还是会都放进 Recycler.mAttachedScrap 中。这个时候,缓存了3个ViewHolder。

fill 的时候,获取ViewHolder,调用 Recycler 的 getScrapOrHiddenOrCachedHolderForPosition 方法。

注意 holder.getLayoutPosition() == position(mState.mInPreLayout || !holder.isRemoved()) 这两个条件。

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();

    // Try first for an exact, non-invalid match from scrap.
    for(int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        //注释1处,关注条件判断  (mState.mInPreLayout || !holder.isRemoved())
        if(!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position && !holder.isInvalid() 
        && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }
    //...
}

现在 Recycler.mAttachedScrap 中有3个 ViewHolder。

注意:这个时候,ViewHolder1的 mPosition = 0 && holder.isRemoved() = 0,不会被复用的。

position = 0 的时候,会从 mAttachedScrap 中取 ViewHolder0 出来复用的。 holder.getLayoutPosition() == position = 0
position = 1 的时候,会从 mAttachedScrap 中取 ViewHolder2 出来复用的。 holder.getLayoutPosition() == position = 1

然后这个时候,fill 没有剩余空间 remainingSpace = 0 ,就不会再继续布局了。这个时候RecyclerView中就只有2个子View。
这个时候 Recycler.mAttachedScrap 还是有一个ViewHolder的,就是被移除的那个。

dispatchLayoutStep2 结束。

dispatchLayoutStep3 阶段

没有变化的ViewHolder,没有动画效果。

被移除的ViewHolder 的动画。是一个透明度渐出动画,透明度从1变化到0。

首先看添加移除动画的逻辑。

RecyclerView 的 animateDisappearance 方法。

void animateDisappearance(@NonNull ViewHolder holder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
    //注释1处,调用addAnimatingView 方法
    addAnimatingView(holder);
    holder.setIsRecyclable(false);
    //注释2处,调用 SimpleItemAnimator 的 animateDisappearance 方法。添加消失动画。
    if(mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

注释1处,调用addAnimatingView 方法。被移除的 ViewHolder 在 dispatchLayoutStep2 阶段 detachViewFromParent以后,在 fill 方法中,不会重新 attachViewToParent。这里在移除动画的开始之前,会调用 addAnimatingView, 把ViewHolder 对应的 ItemView 重新 attachViewToParent 到 RecyclerView 上 RecyclerView.this.attachViewToParent(child, index, layoutParams);注意,这里的index是1哟。

然后动画结束之后,会把这个ViewHolder 从 RecyclerView 中移除。并且会把这个 ViewHolder 缓存到 RecycledViewPool(有 ViewHolder.FLAG_REMOVED 是不会被缓存到 Recycler.mCacheViews 中的)。

注释2处,调用 SimpleItemAnimator 的 animateDisappearance 方法。添加消失动画。

@Override
public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
    int oldLeft = preLayoutInfo.left;
    int oldTop = preLayoutInfo.top;
    View disappearingItemView = viewHolder.itemView;
    int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
    int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
    if(!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
        
        disappearingItemView.layout(newLeft, newTop,
            newLeft + disappearingItemView.getWidth(),
            newTop + disappearingItemView.getHeight());
        //注释1处,不是被移出的ViewHolder才会执行 animateMove
        return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
    } else {
        //注释2处,被移出的ViewHolder才会执行 animateRemove
        return animateRemove(viewHolder);
    }
}

注释2处,被移出的ViewHolder才会执行 animateRemove。

DefaultItemAnimator 的 animateRemove 方法。

public boolean animateRemove(final RecyclerView.ViewHolder holder) {
    resetAnimation(holder);
    mPendingRemovals.add(holder);
    return true;
}

这个方法,就是向 mPendingRemovals 添加了一个等待执行的移除动画,返回true。

新创建的ViewHolder会执行一个 移动动画,从屏幕底部进入到屏幕中。

新创建的 ViewHolder2 现在已经在 position = 1 的位置上了。为了实现从屏幕外移动到屏幕中的 translationY 动画。 在动画开始之初,给 ViewHolder2 对应的 ItemView 设置 translationY >0 。在我们的例子中就是一个ItemView的高度,例如1200px。

RecyclerView 的 animateAppearance 方法。

void animateAppearance(@NonNull ViewHolder itemHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    itemHolder.setIsRecyclable(false);
    //注释1处,调用 SimpleItemAnimator 的 animateAppearance 方法。
    if(mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) {
        postAnimationRunner();
    }
}

注释1处,调用 SimpleItemAnimator 的 animateAppearance 方法。

@Override
public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
    if(preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top)) {
        //注释1处,调用 DefaultItemAnimator 的 animateAppearance 方法。
        return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
            postLayoutInfo.left, postLayoutInfo.top);
    } else {
        return animateAdd(viewHolder);
    }
}

注释1处,调用 DefaultItemAnimator 的 animateAppearance 方法。

public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
    int toX, int toY) {
    final View view = holder.itemView;
    fromX += (int) holder.itemView.getTranslationX();
    fromY += (int) holder.itemView.getTranslationY();
    resetAnimation(holder);
    int deltaX = toX - fromX;
    int deltaY = toY - fromY;
    if(deltaX == 0 && deltaY == 0) {
        dispatchMoveFinished(holder);
        return false;
    }
    if(deltaX != 0) {
        view.setTranslationX(-deltaX);
    }
    //注释1处,给View设置 translationY >0 
    if(deltaY != 0) {
        view.setTranslationY(-deltaY);
    }
    mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
    return true;
}

注释1处,给View设置 translationY >0 。注意,这里 deltaY小于0,所以 **-deltaY >0 **,在我们的例子中是1200。

然后在动画过程中,从 translationY = 1200 移动到 translationY 为0 的位置,就会向上移动1200像素。实现从屏幕下方进入屏幕的效果。最后是在 DefaultItemAnimator 的 animateMoveImpl 方法中执行的。

现在动画添加完毕,看看动画的执行过程。

@Override
public void runPendingAnimations() {
    //移除动画
    boolean removalsPending = !mPendingRemovals.isEmpty();
    boolean movesPending = !mPendingMoves.isEmpty();
    boolean changesPending = !mPendingChanges.isEmpty();
    boolean additionsPending = !mPendingAdditions.isEmpty();
    
    // First, remove stuff
    for(RecyclerView.ViewHolder holder: mPendingRemovals) {
        //注释1处,执行移除动画
        animateRemoveImpl(holder);
    }
    mPendingRemovals.clear();
    // 注释2处,执行移动动画
    if(movesPending) {
        final ArrayList < MoveInfo > moves = new ArrayList < > ();
        moves.addAll(mPendingMoves);
        mMovesList.add(moves);
        mPendingMoves.clear();
        Runnable mover = new Runnable() {
        
            @Override
            public void run() {
                for(MoveInfo moveInfo: moves) {
                    animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
                        moveInfo.toX, moveInfo.toY);
                }
                moves.clear();
                mMovesList.remove(moves);
            }
        };
        if(removalsPending) {
            View view = moves.get(0).holder.itemView;
            ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
        } else {
            mover.run();
        }
    }
    // Next, change stuff, to run in parallel with move animations
    if(changesPending) {
        final ArrayList < ChangeInfo > changes = new ArrayList < > ();
        changes.addAll(mPendingChanges);
        mChangesList.add(changes);
        mPendingChanges.clear();
        Runnable changer = new Runnable() {@
            Override
            public void run() {
                for(ChangeInfo change: changes) {
                    animateChangeImpl(change);
                }
                changes.clear();
                mChangesList.remove(changes);
            }
        };
        if(removalsPending) {
            RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
            ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
        } else {
            changer.run();
        }
    }
    //...
}

注释1处,执行移除动画

private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
        final View view = holder.itemView;
        final ViewPropertyAnimator animation = view.animate();
        mRemoveAnimations.add(holder);
        //注释1处,这里透明度变化到0,变为不可见。
        animation.setDuration(getRemoveDuration()).alpha(0).setListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchRemoveStarting(holder);
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {
                        animation.setListener(null);
                        view.setAlpha(1);
                        //注释2处,动画结束
                        dispatchRemoveFinished(holder);
                        mRemoveAnimations.remove(holder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }

注释1处,移除动画,这里透明度变化到0,变为不可见。

注释2处,动画结束。会把这个ViewHolder2 从 RecyclerView 中移除。并且会把这个 ViewHolder 缓存到 RecycledViewPool(有 ViewHolder.FLAG_REMOVED 是不会被缓存到 Recycler.mCacheViews 中的)。

回到 runPendingAnimations 方法的注释2处,执行移动动画。

void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
    final View view = holder.itemView;
    final int deltaX = toX - fromX;
    final int deltaY = toY - fromY;
    if(deltaX != 0) {
        view.animate().translationX(0);
    }
    if(deltaY != 0) {
        //注释1处,移动动画的结束的时候的translationY设置为0,回到原来的位置上。
        view.animate().translationY(0);
    }
    // TODO: make EndActions end listeners instead, since end actions aren't called when
    // vpas are canceled (and can't end them. why?)
    // need listener functionality in VPACompat for this. Ick.
    final ViewPropertyAnimator animation = view.animate();
    mMoveAnimations.add(holder);
    animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {

        @Override
        public void onAnimationEnd(Animator animator) {
            animation.setListener(null);
            dispatchMoveFinished(holder);
            mMoveAnimations.remove(holder);
            dispatchFinishedWhenDone();
        }
    }).start();
}

注释1处,移动动画的结束的时候的translationY设置为0,回到原来的位置上。然后执行动画。移动动画结束后在本例中没有做额外的操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/485444.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

24. UE5 RPG制作属性面板(二)

在上一篇中&#xff0c;我们创建属性面板的大部分样式&#xff0c;这一篇里面接着制作。 在这一篇里我们需要有以下几个方面&#xff1a; 在界面增加一个属性按钮。属性按钮增加事件&#xff0c;点击时可以打开属性面板&#xff0c;属性面板打开时无法再次点击按钮。点击属性面…

操作系统究竟是什么?在计算机体系中扮演什么角色?

操作系统究竟是什么&#xff1f;在计算机体系中扮演什么角色&#xff1f; 一、操作系统概念二、操作系统如何管理软硬件资源2.1 何为管理者2.2 操作系统如何管理硬件 三、系统调用接口作用四、用户操作接口五、广义操作系统和狭义操作系统 一、操作系统概念 下面是来自百度百科…

动态规划Dynamic Programming

上篇文章我们简单入门了动态规划&#xff08;一般都是简单的上楼梯&#xff0c;分析数据等问题&#xff09;点我跳转&#xff0c;今天给大家带来的是路径问题&#xff0c;相对于上一篇在一维中摸爬滚打&#xff0c;这次就要上升到二维解决问题&#xff0c;但都用的是动态规划思…

STM32微控制器中,如何处理多个同时触发的中断请求?

在STM32微控制器中&#xff0c;处理多个同时触发的中断请求需要一个明确的中断优先级策略&#xff0c;以确保关键任务能够及时得到响应。STM32的中断控制器&#xff08;NVIC&#xff09;支持优先级分组&#xff0c;允许开发者为不同的中断设置抢占优先级和子优先级。本文将详细…

Matlab|【免费】智能配电网的双时间尺度随机优化调度

目录 1 主要内容 基础模型 2 部分代码 3 部分程序结果 4 下载链接 1 主要内容 该程序为文章《Two-Timescale Stochastic Dispatch of Smart Distribution Grids》的源代码&#xff0c;主要做的是主动配电网的双时间尺度随机优化调度&#xff0c;该模型考虑配电网的高效和安…

JAVA面向对象编程 JAVA语言入门基础

类与对象的概念 类 (Class) 和对象 (Object) 是面向对象程序设计方法中最核心的概念。 类是对某一类事物的描述(共性)&#xff0c;是抽象的、概念上的定义&#xff1b;而对象则是实际存在的属该类事物的具体的个体&#xff08;个性&#xff09;&#xff0c;因而也称为实例(In…

网络协议栈--传输层--UDP/TCP协议

目录 本节重点一、再谈端口号1.1 再谈端口号1.2 端口号范围划分1.3 认识知名端口号(Well-Know Port Number)1.4 回答两个问题1.5 netstat1.6 pidof 二、UDP协议2.1 UDP协议段格式2.2 UDP的特点2.3 面向数据报2.4 UDP的缓冲区2.5 UDP使用注意事项2.6 基于UDP的应用层协议2.7 UDP…

知攻善防应急靶场-Linux(2)

前言&#xff1a; 堕落了三个月&#xff0c;现在因为被找实习而困扰&#xff0c;着实自己能力不足&#xff0c;从今天开始 每天沉淀一点点 &#xff0c;准备秋招 加油 注意&#xff1a; 本文章参考qax的网络安全应急响应和知攻善防实验室靶场&#xff0c;记录自己的学习过程&am…

JAVA学习笔记20(面向对象编程)

1.3 方法递归调用 ​ *阶乘 public int factorial(int n) {if(n 1){return 1;}else{return factorial(n-1)*n;} }1.递归重要规则 1.执行一个方法时&#xff0c;就创建一个新的受保护的独立空间&#xff08;栈空间&#xff09; 2.方法的局部变量是独立的&#xff0c;不会相互…

反序列化漏洞简单知识

目录&#xff1a; 一、概念&#xff1a; 二、反序列化漏洞原因 三、序列化漏洞的魔术方法&#xff1a; 四、反序列化漏洞防御&#xff1a; 一、概念&#xff1a; 序列化&#xff1a; Web服务器将HttpSession对象保存到文件系统或数据库中&#xff0c;需要采用序列化的…

Cobalt Strike -- 各种beacon

今天来讲一下cs里面的beacon 其实cs真的功能很强大&#xff0c;自带代理创建&#xff0c;自带beacon通信&#xff01;&#xff01;&#xff01; 一张图&#xff0c;就能说明beacon的工作原理 1.Beacon 每当有一台机器上线之后&#xff0c;我们都会选择sleep时间&#xff0c;…

代码随想录算法训练营Day56 ||leetCode 583. 两个字符串的删除操作 || 72. 编辑距离

647. 回文子串 dp[i][j]表示第i位开始&#xff0c;第j位结束的字符串是否为回文串 class Solution { public:int countSubstrings(string s) {vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));int result 0;for (int i s.size() - 1…

Redis 教程系列之Redis PHP 使用 Redis(十二)

PHP 使用 Redis 安装 开始在 PHP 中使用 Redis 前&#xff0c; 我们需要确保已经安装了 redis 服务及 PHP redis 驱动&#xff0c;且你的机器上能正常使用 PHP。 接下来让我们安装 PHP redis 驱动&#xff1a;下载地址为:https://github.com/phpredis/phpredis/releases。 P…

Java微服务分布式分库分表ShardingSphere - ShardingSphere-JDBC

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 往期热门专栏回顾 专栏…

垂直起降机场:飞行基础设施的未来是绿色的

电动垂直起降&#xff08;eVTOL&#xff09;飞机的日益发展为建立一个新的网络来支持它们提供了理由&#xff0c;这将推动开发绿色基础设施新模式的机会。这些电气化的“短途”客运和货运飞机通常被描述为飞行汽车&#xff0c;是区域飞行和城市出租车的未来&#xff0c;有可能提…

为什么 Hashtable 不允许插入 null 键 和 null 值?

1、典型回答 浅层次的来回答这个问题的答案是&#xff0c;JDK 源码不支持 Hashtable 插入 value 值为 null&#xff0c;如以下JDK 源码所示&#xff1a; 也就是JDK 源码规定了&#xff0c;如果你给 Hashtable 插入 value 值为 null 就会抛出空指针异常 并目看上面的JDK 源码可…

2024全新多语言海外抢单刷单系统源码 订单自动匹配 支持分组 代理后台

2024全新多语言海外抢单刷单系统源码 订单自动匹配 支持分组 代理后台 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88948076 更多资源下载&#xff1a;关注我。

蓝桥杯基础练习详细解析一(代码实现、解题思路、Python)

试题 基础练习 数列排序 资源限制 内存限制&#xff1a;512.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 给定一个长度为n的数列&#xff0c;将这个数列按从小到大的顺序排列。1<n<200 输入格式 第…

吴恩达2022机器学习专项课程(一) 3.6 可视化样例

问题预览 1.本节课主要讲的是什么&#xff1f; 2.不同的w和b&#xff0c;如何影响线性回归和等高线图&#xff1f; 3.一般用哪种方式&#xff0c;可以找到最佳的w和b&#xff1f; 解读 1.课程内容 设置不同的w和b&#xff0c;观察模型拟合数据&#xff0c;成本函数J的等高线…

MQ领消息丢失方案

⼀、哪些场景会丢失消 业务场景&#xff1a;下单⽀付成功后、给⽤户发送消费 ⽤户反馈&#xff1a;⽀付成功以后&#xff0c;没有收到优惠券。原因&#xff1a;⽀付成功的消息丢失了 ⼆、可能丢失消息的环节&#xff1a; 1、订单系统&#xff08;⽣产者&#xff09;向MQ推送…