RecyclerView 之 ItemTouchHelper 源码分析

ItemTouchHelper 与 RecyclerView 产生关联是通过 ItemTouchHelper 的 attachToRecyclerView() 方法,把 RecyclerView 当做参数传进去的,我们看看这个方法

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            final Resources resources = recyclerView.getResources();
            mSwipeEscapeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
            mMaxSwipeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
            setupCallbacks();
        }
    }


它调用了 setupCallbacks() 方法,这个是重点,我们看看它的逻辑

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        initGestureDetector();
    }

    private void initGestureDetector() {
        if (mGestureDetector != null) {
            return;
        }
        mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                new ItemTouchHelperGestureListener());
    }

这里面重点关注三行代码,分别是 addItemDecoration(this) 、 addOnItemTouchListener(mOnItemTouchListener) 和 initGestureDetector(),这三行可以说是包含了 item 横滑拖拽的逻辑,先看看 addItemDecoration(this) ,我们知道,ItemTouchHelper 继承了 ItemDecoration,通过 addItemDecoration() 方法把它添加到 RecyclerView 中,那么在 item 绘制时,会回调 ItemDecoration 中相应的方法,我们看看重点的三个方法

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        outRect.setEmpty();
    }

这个 Rect 对象里面数据皆为0,再看看 onDraw() 、onDrawOver() 方法

    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        float dx = 0, dy = 0;
        if (mSelected != null) {
            getSelectedDxDy(mTmpPosition);
            dx = mTmpPosition[0];
            dy = mTmpPosition[1];
        }
        mCallback.onDrawOver(c, parent, mSelected, mRecoverAnimations, mActionState, dx, dy);
    }

    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        // we don't know if RV changed something so we should invalidate this index.
        mOverdrawChildPosition = -1;
        float dx = 0, dy = 0;
        if (mSelected != null) {
            getSelectedDxDy(mTmpPosition);
            dx = mTmpPosition[0];
            dy = mTmpPosition[1];
        }
        mCallback.onDraw(c, parent, mSelected, mRecoverAnimations, mActionState, dx, dy);
    }

通过观察可以发现,这两个方法里面的逻辑基本一样,只是最终分别调用不同的回调方法,我们看看 getSelectedDxDy(mTmpPosition) 方法,在这个方法中,会把 item 的当前位移偏量计算出来,放到 mTmpPosition 数组中,然后赋值给 dx 和 dy,至于是如何计算的,后面再分析。获取到偏移量后,然后调用 mCallback 的回调,我们看看 mCallback 是什么,它是 ItemTouchHelper 的内部类 Callback,也是上一篇文中的 SimpleItemTouchHelperCallback,我们看看调用的方法

  private void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
            List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
            int actionState, float dX, float dY) {
    final int recoverAnimSize = recoverAnimationList.size();
    for (int i = 0; i < recoverAnimSize; i++) {
        final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
        anim.update();
        final int count = c.save();
        onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
                false);
        c.restoreToCount(count);
    }
    if (selected != null) {
        final int count = c.save();
        onChildDraw(c, parent, selected, dX, dY, actionState, true);
        c.restoreToCount(count);
    }
 }


先不管 for 循环中的 ItemTouchHelper.RecoverAnimation 对象,这是个类似辅助动画,我们看最后一行diamante,尤其是 onChildDraw() 方法,

    public void onChildDraw(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder,
        float dX, float dY, int actionState, boolean isCurrentlyActive) {
    sUICallback.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive);
    }


同理, onDrawOver() 对应的是

    public void onChildDrawOver(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder,
        float dX, float dY, int actionState, boolean isCurrentlyActive) {
    sUICallback.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, actionState, isCurrentlyActive);
    }

看看 sUICallback 这个对象又是什么,原来是 ItemTouchUIUtil,它是在静态代码块中被创建对象

 static {
    if (Build.VERSION.SDK_INT >= 21) {
        sUICallback = new ItemTouchUIUtilImpl.Lollipop();
    } else if (Build.VERSION.SDK_INT >= 11) {
        sUICallback = new ItemTouchUIUtilImpl.Honeycomb();
    } else {
        sUICallback = new ItemTouchUIUtilImpl.Gingerbread();
    }
 }

这里是做了版本兼容,我们看看 ItemTouchUIUtilImpl 代码,android版本小于11,使用 Gingerbread 对象,它里面最终调用 draw() 方法

  private void draw(Canvas c, RecyclerView parent, View view, float dX, float dY) {
    c.save();
    c.translate(dX, dY);
    parent.drawChild(c, view, 0);
    c.restore();
  }

代码意思是通过画布保存,然后位移画布,绘制后,还原画布层,这样就实现了item的位移;再看看版本 11 和 21 的两个类,Lollipop 继承了 Honeycomb,直接看 Honeycomb 代码

    @Override
    public void onDraw(Canvas c, RecyclerView recyclerView, View view,
        float dX, float dY, int actionState, boolean isCurrentlyActive) {
      ViewCompat.setTranslationX(view, dX);
      ViewCompat.setTranslationY(view, dY);
    }

版本 11 以上,添加了 setTranslationX() 位移方法,这里通过这个方法实现位移,效率更高;看看 Lollipop 中方法

  @Override
  public void onDraw(Canvas c, RecyclerView recyclerView, View view,
        float dX, float dY, int actionState, boolean isCurrentlyActive) {
    if (isCurrentlyActive) {
        Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
        if (originalElevation == null) {
            originalElevation = ViewCompat.getElevation(view);
            float newElevation = 1f + findMaxElevation(recyclerView, view);
            ViewCompat.setElevation(view, newElevation);
            view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
        }
    }
    super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
  }

这个里面在 Honeycomb 的基础上,添加了坐标轴Z轴的坐标,这样view之间会显示出层次感; onDrawOver() 方法为空,默认无功能实现。如果我们想在item横滑时,让它随着位移变的透明,可以通过重写 ItemTouchHelper.Callback 方法中 onChildDraw() 方法,如果不重写这个方法,item 只会左右位移,重写时判断当前是否是左右拖拽,然后根据位移 dx 占宽的值,来显示透明度,

    public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
                float dX, float dY, int actionState, boolean isCurrentlyActive) {
        super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            float alpha = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();
            viewHolder.itemView.setAlpha(alpha);
        }
    }


这里可能有同学就有疑问了,既然 ItemTouchHelper 是 ItemDecoration 类型,并且刚才的方法是在 onDraw() 方法中调用,是不是说只要 RecyclerView 滑动,就会调用onChildDraw()方法?我们注意 mCallback 中 onDraw() 方法,调用 onChildDraw() 有个前提 if (selected != null),当 RecyclerView 自身滑动时,这个 selected 是空的,当我们按着 item 拖拽时,满足条件  selected 才会有值。

看看 mRecyclerView.addOnItemTouchListener(mOnItemTouchListener) 代码,

    private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
        @Override
        public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            final int action = MotionEventCompat.getActionMasked(event);
            if (action == MotionEvent.ACTION_DOWN) {
                ...
            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mActivePointerId = ACTIVE_POINTER_ID_NONE;
                select(null, ACTION_STATE_IDLE);
            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId);
                if (index >= 0) {
                    checkSelectForSwipe(action, event, index);
                }
            }
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(event);
            }
            return mSelected != null;
        }

        @Override
        public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            ...
            if (activePointerIndex >= 0) {
                checkSelectForSwipe(action, event, activePointerIndex);
            }
            ViewHolder viewHolder = mSelected;
            if (viewHolder == null) {
                return;
            }
            switch (action) {
                case MotionEvent.ACTION_MOVE: {
                    if (activePointerIndex >= 0) {
                        updateDxDy(event, mSelectedFlags, activePointerIndex);
                        moveIfNecessary(viewHolder);
                        mRecyclerView.removeCallbacks(mScrollRunnable);
                        mScrollRunnable.run();
                        mRecyclerView.invalidate();
                    }
                    break;
                }
                ...
                }
            }
        }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            if (!disallowIntercept) {
                return;
            }
            select(null, ACTION_STATE_IDLE);
        }
    };

这里面是触摸事件机制,通过 checkSelectForSwipe(action, event, index) 方法 调用 select(vh, ACTION_STATE_SWIPE) 方法,开启横滑事件

    private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
        ...
        final ViewHolder vh = findSwipedView(motionEvent);
        if (vh == null) {
            return false;
        }
        final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
        final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
        if (swipeFlags == 0) {
            return false;
        }
        ...
        select(vh, ACTION_STATE_SWIPE);
        return true;
    }


findSwipedView(motionEvent) 根据手指坐标,找到对应的 item,然后返回对应的 ViewHolder;是否允许横滑,看看 mCallback.getAbsoluteMovementFlags(mRecyclerView, vh) 代码

    final int getAbsoluteMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
        final int flags = getMovementFlags(recyclerView, viewHolder);
        return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
    }

最终通过 getMovementFlags() 方法返回值,通过位运算来决定,就是上一章中重写的方法,我们可以自定义滑动允许的方向;然后就是计算滑动的距离,看是否满足滑动的要求,最终一切符合条件,调用 select() 方法,传入 ACTION_STATE_SWIPE 值。 select() 方法稍后分析,先看看 setupCallbacks() 中最后一行代码 initGestureDetector() 方法

  private void initGestureDetector() {
    if (mGestureDetector != null) {
        return;
    }
    mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
            new ItemTouchHelperGestureListener());
  }

  private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {

    @Override
    public void onLongPress(MotionEvent e) {
        View child = findChildView(e);
        if (child != null) {
            ViewHolder vh = mRecyclerView.getChildViewHolder(child);
            if (vh != null) {
                if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
                    return;
                }
                int pointerId = MotionEventCompat.getPointerId(e, 0);
                if (pointerId == mActivePointerId) {
                    ...
                    if (mCallback.isLongPressDragEnabled()) {
                        select(vh, ACTION_STATE_DRAG);
                    }
                }
            }
        }
    }
  }

这里是个手势事件,长按时生效,根据手指按下坐标找到 item 及对应的 ViewHolder,然后根据mCallback.hasDragFlag(mRecyclerView, vh) 判断是否允许拖拽,紧接着判断位移量,如果符合条件调用 select(vh, ACTION_STATE_DRAG) 方法。通过上面的代码,我们知道不论是item滑动或是拖动,有几个限制,一个是 ItemTouchHelper.Callback 中的 getMovementFlags() 方法,控制横滑和拖拽的方向;另一个是 isLongPressDragEnabled() 和 isItemViewSwipeEnabled() 方法,必须返回 true 时才会最终允许拖拽和滑动。 item 滑动或拖拽或还原,都是调用 select() 方法,出入 ACTION_STATE_IDLE、ACTION_STATE_SWIPE、ACTION_STATE_DRAG 值。

    private void select(ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        ...
        if (mSelected != null) {
            ...
                getSelectedDxDy(mTmpPosition);
                final float currentTranslateX = mTmpPosition[0];
                final float currentTranslateY = mTmpPosition[1];
                final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
                        prevActionState, currentTranslateX, currentTranslateY,
                        targetTranslateX, targetTranslateY) {
                    @Override
                    public void onAnimationEnd(ValueAnimatorCompat animation) {
                        super.onAnimationEnd(animation);
                        if (this.mOverridden) {
                            return;
                        }
                        if (swipeDir <= 0) {
                            mCallback.clearView(mRecyclerView, prevSelected);
                        } else {
                            mPendingCleanup.add(prevSelected.itemView);
                            mIsPendingCleanup = true;
                            if (swipeDir > 0) {
                                postDispatchSwipe(this, swipeDir);
                            }
                        }
                        ...
                    }
                };
                final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
                        targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
                rv.setDuration(duration);
                mRecoverAnimations.add(rv);
                rv.start();
                preventLayout = true;
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
                mCallback.clearView(mRecyclerView, prevSelected);
            }
            mSelected = null;
        }
        ...
        mRecyclerView.invalidate();
    }

此方法调用时,最终会回调 mCallback.onSelectedChanged() 方法,我们可以在这里做一些UI操作。 item 横滑,手指松开时,会使用 RecoverAnimation 这个对象;如果是拖拽,swipeDir 值为0,如果是横滑,且满足了移除的条件,默认是横滑的距离大于item宽度的一半,则 swipeDir 值大于0,否则也为0。如果为0调用 mCallback.clearView() 方法,可以在里面还原UI操作。如果 swipeDir 大于0,则会执行 postDispatchSwipe() 方法,最终执行 mCallback.onSwiped() 回调,我们肯可以在这个里面做数据删除操作。

swip 和 drag 的过程在哪呢?还得看 onTouchEvent() 中 ACTION_MOVE 部分的代码,updateDxDy() 中更新 mDx mDy 的值;moveIfNecessary() 这个会做拖拽的判断,会不停执行,最终满足条件的话会执行 mCallback.onMove() 回调,我们可以在里面做集合中数据的交换及刷新UI。至于item的横滑,记得 onDraw() 方法中有个获取位移的方法 getSelectedDxDy()

    private void getSelectedDxDy(float[] outPosition) {
        if ((mSelectedFlags & (LEFT | RIGHT)) != 0) {
            outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft();
        } else {
            outPosition[0] = ViewCompat.getTranslationX(mSelected.itemView);
        }
        if ((mSelectedFlags & (UP | DOWN)) != 0) {
            outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop();
        } else {
            outPosition[1] = ViewCompat.getTranslationY(mSelected.itemView);
        }
    }


这里面会去计算偏移量,然后通过 ItemTouchUIUtilImpl 对象中方法去位移。我们在调用 CallBack 中 onDraw() 方法,也会用到 RecoverAnimation

    private void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
            List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
            int actionState, float dX, float dY) {
        final int recoverAnimSize = recoverAnimationList.size();
        for (int i = 0; i < recoverAnimSize; i++) {
            final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
            anim.update();
            final int count = c.save();
            onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
                    false);
            c.restoreToCount(count);
        }
        if (selected != null) {
            final int count = c.save();
            onChildDraw(c, parent, selected, dX, dY, actionState, true);
            c.restoreToCount(count);
        }
    }

mRecoverAnimations 这个集合是在 select() 中添加对象,所以在 onDraw() 中就有 RecoverAnimation 对象了,但这个执行是手指头松开后才会执行。

最后稍微分析一下 RecoverAnimation 这个类。看看它的构造方法 

    public RecoverAnimation(ViewHolder viewHolder, int animationType,
            int actionState, float startDx, float startDy, float targetX, float targetY) {
        ...
        mValueAnimator = AnimatorCompatHelper.emptyValueAnimator();
        mValueAnimator.addUpdateListener(
                new AnimatorUpdateListenerCompat() {
                    @Override
                    public void onAnimationUpdate(ValueAnimatorCompat animation) {
                        setFraction(animation.getAnimatedFraction());
                    }
                });
        mValueAnimator.setTarget(viewHolder.itemView);
        mValueAnimator.addListener(this);
        setFraction(0f);
    }


重点关注 mValueAnimator = AnimatorCompatHelper.emptyValueAnimator() 这行代码,

public final class AnimatorCompatHelper {

    private final static AnimatorProvider IMPL;

    static {
        if (Build.VERSION.SDK_INT >= 12) {
            IMPL = new HoneycombMr1AnimatorCompatProvider();
        } else {
            IMPL = new DonutAnimatorCompatProvider();
        }
    }

    public static ValueAnimatorCompat emptyValueAnimator() {
        return IMPL.emptyValueAnimator();
    }

    private AnimatorCompatHelper() {}

    public static void clearInterpolator(View view) {
        IMPL.clearInterpolator(view);
    }
}

原来是个类似工厂模式,做了版本兼容,看返回的对象类型是 ValueAnimatorCompat ,说明两个类都实现了 AnimatorProvider 接口,都先看高版本的,

class HoneycombMr1AnimatorCompatProvider implements AnimatorProvider {

    private TimeInterpolator mDefaultInterpolator;

    @Override
    public ValueAnimatorCompat emptyValueAnimator() {
        return new HoneycombValueAnimatorCompat(ValueAnimator.ofFloat(0f, 1f));
    }

    static class HoneycombValueAnimatorCompat implements ValueAnimatorCompat {

        final Animator mWrapped;

        public HoneycombValueAnimatorCompat(Animator wrapped) {
            mWrapped = wrapped;
        }
        ...
        @Override
        public void addUpdateListener(final AnimatorUpdateListenerCompat animatorUpdateListener) {
            if (mWrapped instanceof ValueAnimator) {
                ((ValueAnimator) mWrapped).addUpdateListener(
                        new ValueAnimator.AnimatorUpdateListener() {
                            @Override
                            public void onAnimationUpdate(ValueAnimator animation) {
                                animatorUpdateListener
                                        .onAnimationUpdate(HoneycombValueAnimatorCompat.this);
                            }
                        });
            }
        }
    }

    static class AnimatorListenerCompatWrapper implements Animator.AnimatorListener {

        public AnimatorListenerCompatWrapper(
                AnimatorListenerCompat wrapped, ValueAnimatorCompat valueAnimatorCompat) {
            mWrapped = wrapped;
            mValueAnimatorCompat = valueAnimatorCompat;
        }
        ...
    }    
}

这个明显是个包装类,把 AnimatorListenerCompat 包装到 AnimatorListenerCompatWrapper 里面掉用,用 HoneycombValueAnimatorCompat 把 ValueAnimator 封装了一层,通过 ValueAnimator 的从 0 到 1 的变化,监听变化的方法 addUpdateListener() 回调中,调用 AnimatorUpdateListenerCompat 的 onAnimationUpdate() 方法,至于
AnimatorUpdateListenerCompat 回调,就是RecoverAnimation构造方法中的回调,animation.getAnimatedFraction() 获取的实际上是 ValueAnimator.getAnimatedFraction(),这个值可以理解为进度条,从0开始,到1结束。所以 RecoverAnimation 构造中的 addUpdateListener() 方法就是实时更新 setFraction() 中 mFraction 的值,此时看

    public void update() {
        if (mStartDx == mTargetX) {
            mX = ViewCompat.getTranslationX(mViewHolder.itemView);
        } else {
            mX = mStartDx + mFraction * (mTargetX - mStartDx);
        }
        if (mStartDy == mTargetY) {
            mY = ViewCompat.getTranslationY(mViewHolder.itemView);
        } else {
            mY = mStartDy + mFraction * (mTargetY - mStartDy);
        }
    }

如果开始和结束值不一样,则计算公式是 开始值 + (差值 * 进度),这一看就是匀速的变化,功能类似插值器 LinearInterpolator;当取消动画时 setFraction(1f)。再分析分析 DonutAnimatorCompatProvider 这个类,由于 11 之前系统源码中没有 ValueAnimator 这个类,所以在这里用 Handler + Runnable 来实现相同效果,

class DonutAnimatorCompatProvider implements AnimatorProvider {

    @Override
    public ValueAnimatorCompat emptyValueAnimator() {
        return new DonutFloatValueAnimator();
    }

    private static class DonutFloatValueAnimator implements ValueAnimatorCompat {
        ...
        private Runnable mLoopRunnable = new Runnable() {
            @Override
            public void run() {
                long dt = getTime() - mStartTime;
                float fraction = dt * 1f / mDuration;
                if (fraction > 1f || mTarget.getParent() == null) {
                    fraction = 1f;
                }
                mFraction = fraction;
                notifyUpdateListeners();
                if (mFraction >= 1f) {
                    dispatchEnd();
                } else {
                    mTarget.postDelayed(mLoopRunnable, 16);
                }
            }
        };

        @Override
        public void start() {
            if (mStarted) {
                return;
            }
            mStarted = true;
            dispatchStart();
            mFraction = 0f;
            mStartTime = getTime();
            mTarget.postDelayed(mLoopRunnable, 16);
        }
        ...
    }

}

这里是封装了各种监听,比如 start() 方法中,触发 dispatchStart() 方法,触发开始监听,初始化 mFraction 为0,mStartTime 记录当前时间,然后用 View 的 postDelayed() 方法,延迟 16 毫米执行,它的原理还是借助 Handler 来执行,看看 mLoopRunnable 这里面逻辑:获取当前距离开始的时间差 dt,计算出进度值 fraction,调用 notifyUpdateListeners()方法,在这里里面遍历 AnimatorUpdateListenerCompat 监听即可,回调 onAnimationUpdate() 方法,进度条没超过1时,继续执行 mTarget.postDelayed(mLoopRunnable, 16) 方法,循环执行;到 1 时,执行 dispatchEnd() 方法,动画结束回调。
 

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/101225100