Android修炼系列(十二),自定义一个超顺滑的回弹RecyclerView

5,290 阅读5分钟

前面写了一个嵌套滑动框架和分析了ViewDragHelper的事件分发,本节主要自定义一个带有回弹效果的RecyclerView,看看事件和动画的配合,这在各大App中都比较常见了,效果如下:

实现

这是定义的回弹类:OverScrollRecyclerView,其是RecyclerView的子类,并实现了OnTouchListener方法:

public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

    public OverScrollRecyclerView(Context context) {
        this(context, null);
    }

    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initParams();
    }
}

随后会定义一些必要的属性,其中DEFAULT_TOUCH_DRAG_MOVE_RATIO表示滑动的像素数与实际view偏移量的比例,减速系数和时间也都是根据实际效果不断调整的。

```java
public class OverScrollRecyclerView extends RecyclerView implements View.OnTouchListener {

    // 下拉与上拉,move px / view Translation
    private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_FWD = 2f;
    private static final float DEFAULT_TOUCH_DRAG_MOVE_RATIO_BCK = 1f;
    // 默认减速系数
    private static final float DEFAULT_DECELERATE_FACTOR = -2f;
    // 最大反弹时间
    private static final int MAX_BOUNCE_BACK_DURATION_MS = 800;
    private static final int MIN_BOUNCE_BACK_DURATION_MS = 200;

    // 初始状态,滑动状态,回弹状态
    private IDecoratorState mCurrentState;
    private IdleState mIdleState;
    private OverScrollingState mOverScrollingState;
    private BounceBackState mBounceBackState;

    private final OverScrollStartAttributes mStartAttr = new OverScrollStartAttributes();
    private float mVelocity;
    private final RecyclerView mRecyclerView = this;
    ...
    public OverScrollRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initParams();
    }
    ...
}

这是我们的状态接口IDecoratorState,其提供了3个方法,IdleState、OverScrollingState、BounceBackState都是它的具体实现类,符合状态模式的思想:

    protected interface IDecoratorState {
        // 处理move事件
        boolean handleMoveTouchEvent(MotionEvent event);
        // 处理up事件
        boolean handleUpTouchEvent(MotionEvent event);
        // 事件结束后的动画处理
        void handleTransitionAnim(IDecoratorState fromState);
    }

初始化我们定义的变量,没有什么特殊的操作,只是一些各自属性的赋值,具体见下文:

    private void initParams() {
        mBounceBackState = new BounceBackState();
        mOverScrollingState = new OverScrollingState();
        mCurrentState = mIdleState = new IdleState();
        attach();
    }

这是我们的attach,添加触摸监听,并去掉滚动到边缘的光晕效果:

    @SuppressLint("ClickableViewAccessibility")
    public void attach() {
        mRecyclerView.setOnTouchListener(this);
        mRecyclerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
    }

核心代码就是事件的监听了,需要我们处理onTouch事件,当手指按下滑动时,此时mCurrentState还处于初始状态,其会执行相应的handleMoveTouchEvent方法:

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                return mCurrentState.handleMoveTouchEvent(event);
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                return mCurrentState.handleUpOrCancelTouchEvent(event);
        }
        return false;
    }

这是初始状态IdleState处理move的逻辑,主要做些校验工作,如果移动不满足要求,就将事件透出去,具体见下:

    @Override
    public boolean handleMoveTouchEvent(MotionEvent event) {
        // 是否符合move要求,不符合不拦截事件
        if (!initMotionAttributes(mRecyclerView, mMoveAttr, event)) {
            return false;
        }
        // 在RecyclerView顶部但不能下拉 或 在RecyclerView底部但不能上拉
        if (!((isInAbsoluteStart(mRecyclerView) && mMoveAttr.mDir) ||
                (isInAbsoluteEnd(mRecyclerView) && !mMoveAttr.mDir))) {
            return false;
        }
        // 保存当前Motion信息
        mStartAttr.mPointerId = event.getPointerId(0);
        mStartAttr.mAbsOffset = mMoveAttr.mAbsOffset;
        mStartAttr.mDir = mMoveAttr.mDir;
        // 初始状态->滑动状态
        issueStateTransition(mOverScrollingState);
        return mOverScrollingState.handleMoveTouchEvent(event);
    }

这是initMotionAttributes方法,会计算Y方向偏移量,如果满足要求,则为MotionAttributes赋值:

    private boolean initMotionAttributes(View view, MotionAttributes attributes, MotionEvent event) {
        if (event.getHistorySize() == 0) {
            return false;
        }
        // 像素偏移量
        final float dy = event.getY(0) - event.getHistoricalY(0, 0);
        final float dx = event.getX(0) - event.getHistoricalX(0, 0);
        if (Math.abs(dy) < Math.abs(dx)) {
            return false;
        }
        attributes.mAbsOffset = view.getTranslationY();
        attributes.mDeltaOffset = dy;
        attributes.mDir = attributes.mDeltaOffset > 0;
        return true;
    }

这里的isInAbsoluteStart方法用来判断,当前RecyclerView是否不能向下滑动,另一个isInAbsoluteEnd是否不能向上滑动,代码就不展示了:

    private boolean isInAbsoluteStart(View view) {
        return !view.canScrollVertically(-1);
    }

当move事件通过初始状态的校验,则改变状态为滑动态OverScrollingState,正式处理滑动逻辑,其方法见下:

    @Override
    public boolean handleMoveTouchEvent(MotionEvent event) {
        final OverScrollStartAttributes startAttr = mStartAttr;
        // 不是一个触摸点事件,则直接切到回弹状态
        if (startAttr.mPointerId != event.getPointerId(0)) {
            issueStateTransition(mBounceBackState);
            return true;
        }

        final View view = mRecyclerView;

        // 是否符合move要求
        if (!initMotionAttributes(view, mMoveAttr, event)) {
            return true;
        }

        // mDeltaOffset: 实际要移动的像素,可以为下拉和上拉设置不同移动比
        float deltaOffset = mMoveAttr.mDeltaOffset / (mMoveAttr.mDir == startAttr.mDir
                ? mTouchDragRatioFwd : mTouchDragRatioBck);
        // 计算偏移
        float newOffset = mMoveAttr.mAbsOffset + deltaOffset;

        // 上拉下拉状态与滑动方向不符,则回到初始状态,并将视图归位
        if ((startAttr.mDir && !mMoveAttr.mDir && (newOffset <= startAttr.mAbsOffset)) ||
                (!startAttr.mDir && mMoveAttr.mDir && (newOffset >= startAttr.mAbsOffset))) {
            translateViewAndEvent(view, startAttr.mAbsOffset, event);
            issueStateTransition(mIdleState);
            return true;
        }

        // 不让父类截获move事件
        if (view.getParent() != null) {
            view.getParent().requestDisallowInterceptTouchEvent(true);
        }

        // 计算速度
        long dt = event.getEventTime() - event.getHistoricalEventTime(0);
        if (dt > 0) {
            mVelocity = deltaOffset / dt;
        }

        // 改变控件位置
        translateView(view, newOffset);
        return true;
    }

这是translateView方法,改变view相对父布局的偏移量:

    private void translateView(View view, float offset) {
        view.setTranslationY(offset);
    }

当滑动事件结束,手指抬起时,会将状态由滑动状态切换为回弹状态:

    @Override
    public boolean handleUpTouchEvent(MotionEvent event) {
        // 事件up切换状态,有滑动态-回弹态
        issueStateTransition(mBounceBackState);
        return false;
    }

上文提到的issueStateTransition方法,只是说切换了状态,但实际上它还会执行handleTransitionAnim的操作,只不过初始状态和滑动状态此接口都是空实现,只有回弹状态才会去处理动画效果罢了:

    protected void issueStateTransition(IDecoratorState state) {
        IDecoratorState oldState = mCurrentState;
        mCurrentState = state;
        // 处理回弹动画效果
        mCurrentState.handleTransitionAnim(oldState);
    }

这是我们处理动画效果的方法,核心方法createAnimator具体看下,之后添加了动画监听,并开启动画:

    @Override
    public void handleTransitionAnim(IDecoratorState fromState) {
        Animator bounceBackAnim = createAnimator();
        bounceBackAnim.addListener(this);
        bounceBackAnim.start();
    }

这是动画创建的核心类,使用了属性动画,先由当前速度mVelocity->0,随后回弹slowdownEndOffset->mStartAttr.mAbsOffset,具体代码见下:

    private Animator createAnimator() {
        initAnimationAttributes(view, mAnimAttributes);

        // 速度为0了或手势记录的状态与mDir不符合,直接回弹
        if (mVelocity == 0f || (mVelocity < 0 && mStartAttr.mDir) || (mVelocity > 0 && !mStartAttr.mDir)) {
            return createBounceBackAnimator(mAnimAttributes.mAbsOffset);
        }

        // 速度减到0,即到达最大距离时,需要的动画事件
        float slowdownDuration = (0 - mVelocity) / mDecelerateFactor;
        slowdownDuration = (slowdownDuration < 0 ? 0 : slowdownDuration);

        // 速度减到0,动画的距离,dx = (Vt^2 - Vo^2) / 2a
        float slowdownDistance = -mVelocity * mVelocity / mDoubleDecelerateFactor;
        float slowdownEndOffset = mAnimAttributes.mAbsOffset + slowdownDistance;

        // 开始动画,减速->回弹
        ObjectAnimator slowdownAnim = createSlowdownAnimator(view, (int) slowdownDuration, slowdownEndOffset);
        ObjectAnimator bounceBackAnim = createBounceBackAnimator(slowdownEndOffset);
        AnimatorSet wholeAnim = new AnimatorSet();
        wholeAnim.playSequentially(slowdownAnim, bounceBackAnim);
        return wholeAnim;
    }

这是具体的减速动画方法,设置时间和差值器,就不细说了,不是本文的重点,直接见代码吧:

    private ObjectAnimator createSlowdownAnimator(View view, int slowdownDuration, float slowdownEndOffset) {
        ObjectAnimator slowdownAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, slowdownEndOffset);
        slowdownAnim.setDuration(slowdownDuration);
        slowdownAnim.setInterpolator(mBounceBackInterpolator);
        slowdownAnim.addUpdateListener(this);
        return slowdownAnim;
    }

同样这是回弹动画的方法,设置时间和差值器,添加监听等,代码见下:

    private ObjectAnimator createBounceBackAnimator(float startOffset) {
        float bounceBackDuration = (Math.abs(startOffset) / mAnimAttributes.mMaxOffset) * MAX_BOUNCE_BACK_DURATION_MS;
        ObjectAnimator bounceBackAnim = ObjectAnimator.ofFloat(view, mAnimAttributes.mProperty, mStartAttr.mAbsOffset);
        bounceBackAnim.setDuration(Math.max((int) bounceBackDuration, MIN_BOUNCE_BACK_DURATION_MS));
        bounceBackAnim.setInterpolator(mBounceBackInterpolator);
        bounceBackAnim.addUpdateListener(this);
        return bounceBackAnim;
    }

当动画结束的时候,会将状态由回弹模式切换为初始状态,代码见下:

    @Override
    public void onAnimationEnd(Animator animation) {
        // 动画结束改变状态
        issueStateTransition(mIdleState);
    }

好了,到这里核心逻辑就结束啦,应该不难理解吧。如果讲的不好,博客的栗子我都上传到了gitHub上,感兴趣的可以直接下载看下。

本文到这里,关于回弹效果的实现就结束了,希望本文对你有用。