带阻尼回弹效果的RecyclerView

一、前提

接到新需求,要求列表滑动过程增加阻尼回弹效果,且即使列表不能填充一整个屏幕的情况下也支持滑动。

有人说,给RecyclerView加上 android:overScrollMode="always" 就行了,事实证明,NO!这个东西只是在滑动到边缘是多了个水波阴影而已,没有阻尼回弹。

又有人说,给ListView加上 android:overScrollMode="always" 就行了,经过尝试,貌似可以,但是有bug,还相当严重。况且我还要把RecyclerView改成ListView,太麻烦了。

本着不重复造轮子的前提,搜索了一大波。github上面也有很多现成的框架,不过由于项目要求,不能随便引入框架,所以不敢直接depend,本来想copy源码改吧改吧,后来发现这些框架做的都很“大”,冗余功能代码量多,索性放弃。后来终于找到一个简单的实现方式。借鉴作者思路,继续修改使之符合我的功能要求。站在巨人的肩膀上会让成功来的更快!

二、直接上代码:

package com.zzz.test.view.widget;

import android.content.Context;
import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;

/**
 * @author xxx
 * @date 20-4-29
 */
public class OverScrollLayout extends LinearLayout {

    private static final int ANIM_TIME = 400;

    private RecyclerView childView;

    private Rect original = new Rect();

    private boolean isMoved = false;

    private float startYpos;

    /**
     * 阻尼系数
     */
    private static final float DAMPING_COEFFICIENT = 0.3f;

    private boolean isSuccess = false;

    private ScrollListener mScrollListener;

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

    public OverScrollLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        childView = (RecyclerView) getChildAt(0);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        original.set(childView.getLeft(), childView.getTop(), childView.getRight(), childView.getBottom());
    }

    public void setScrollListener(ScrollListener listener) {
        mScrollListener = listener;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float touchYpos = ev.getY();
        if (touchYpos >= original.bottom || touchYpos <= original.top) {
            if (isMoved) {
                recoverLayout();
            }
            return true;
        }
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startYpos = ev.getY();
            case MotionEvent.ACTION_MOVE:
                int scrollYpos = (int) (ev.getY() - startYpos);
                boolean pullDown = scrollYpos > 0 && canPullDown();
                boolean pullUp = scrollYpos < 0 && canPullUp();
                if (pullDown || pullUp) {
                    cancelChild(ev);
                    int offset = (int) (scrollYpos * DAMPING_COEFFICIENT);
                    childView.layout(original.left, original.top + offset, original.right, original.bottom + offset);
                    if (mScrollListener != null) {
                        mScrollListener.onScroll();
                    }
                    isMoved = true;
                    isSuccess = false;
                    return true;
                } else {
                    startYpos = ev.getY();
                    isMoved = false;
                    isSuccess = true;
                    return super.dispatchTouchEvent(ev);
                }
            case MotionEvent.ACTION_UP:
                if (isMoved) {
                    recoverLayout();
                }
                return !isSuccess || super.dispatchTouchEvent(ev);
            default:
                return true;
        }
    }

    /**
     * 取消子view已经处理的事件
     *
     * @param ev event
     */
    private void cancelChild(MotionEvent ev) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        super.dispatchTouchEvent(ev);
    }

    /**
     * 位置还原
     */
    private void recoverLayout() {
        TranslateAnimation anim = new TranslateAnimation(0, 0, childView.getTop() - original.top, 0);
        anim.setDuration(ANIM_TIME);
        childView.startAnimation(anim);
        childView.layout(original.left, original.top, original.right, original.bottom);
        isMoved = false;
    }

    /**
     * 判断是否可以下拉
     *
     * @return true:可以,false:不可以
     */
    private boolean canPullDown() {
        final int firstVisiblePosition = ((LinearLayoutManager) childView.getLayoutManager()).findFirstVisibleItemPosition();
        if (firstVisiblePosition != 0 && childView.getAdapter().getItemCount() != 0) {
            return false;
        }
        int mostTop = (childView.getChildCount() > 0) ? childView.getChildAt(0).getTop() : 0;
        return mostTop >= 0;
    }

    /**
     * 判断是否可以上拉
     *
     * @return true:可以,false:不可以
     */
    private boolean canPullUp() {
        final int lastItemPosition = childView.getAdapter().getItemCount() - 1;
        final int lastVisiblePosition = ((LinearLayoutManager) childView.getLayoutManager()).findLastVisibleItemPosition();
        if (lastVisiblePosition >= lastItemPosition) {
            final int childIndex = lastVisiblePosition - ((LinearLayoutManager) childView.getLayoutManager()).findFirstVisibleItemPosition();
            final int childCount = childView.getChildCount();
            final int index = Math.min(childIndex, childCount - 1);
            final View lastVisibleChild = childView.getChildAt(index);
            if (lastVisibleChild != null) {
                return lastVisibleChild.getBottom() <= childView.getBottom() - childView.getTop();
            }
        }
        return false;
    }


    public interface ScrollListener {
        /**
         * 滚动事件回调
         */
        void onScroll();
    }

}

使RecyclerView支持阻尼回弹的话,把RecyclerView放到OverScrollView中,其他照旧。要想监听阻尼滑动,直接设置scrollListener。

    

        

    

 

你可能感兴趣的:(android开发,android)