RecyclerView封装--添加下拉刷新和上拉加载更多

1 前言

关于RecyclerView 添加上拉加载更多和下拉刷新的封装很多,例如有自定义ViewGroup来实现的,也有使用SwipeRefreshLayout来实现的,我觉得都不是太好,因为对于在项目中,需要各种下拉刷新和上拉加载更多的效果,甚至同一项目中都有不同的效果,网上的大多耦合性太严重,无法解耦,按需定制性较差。现在提供一种对RecyclerView的封装,设计如下:

2 下拉刷新和上拉加载更多实现思路

首先来看下拉刷新的效果,如下:
这里写图片描述
首先我们分析下,有以下几点:

1. 有一个下拉刷新头,即这里的下拉显示“下拉即可刷新”,这个可以用上一节的addHeaderView来实现
2. 下拉时,一共有这几种状态:

  • 正常状态:这种状态就是没有下拉时的状态
  • 下拉状态:正在下拉,但是拉动的距离小于下拉刷新头的高度,或者其他值,此时提示下拉可以刷新等
  • 待松开状态:下拉的距离大于下拉刷新头的高度,此时需提示松开刷新子类的
  • 正在运行状态:下拉刷新运行状态,一般会回调自定义的Listener的onRefresh()接口,表示正在刷新
  • 以及回退状态:运行结束,下拉刷新头回退到恢复原样,一般需用动画来实现。

另外我们对于RecycerView 的封装下拉刷新功能,有如下要求:

  • 下拉刷新头可以自定义
  • 下拉效果可以自定义
  • 回退效果可以自定义

基于以上的分析,我们的设计了以下的类来实现设计,先来看类图
这里写图片描述

IViewCreator负责下拉刷新头的创建,以及下拉效果,运行效果,回退效果的实现
XRecyclerHelper 负责下拉刷新,上拉加载更多的View的状态的维护和切换
XRecyclerListener下拉刷新,上拉加载更多回调
XStatus 下拉刷新,上拉加载更多 的状态
XRecyclerView下拉刷新,上拉加载更多的RecyclerView的封装

这里我们知道,在添加了下拉刷新头是,默认我们是需要隐藏的,这里直接通过改变mRefresh的topMargin来实现,在下拉的过程中,不断的改变topMargin,使其具有下拉效果,上拉加载更多也类似。

对于上拉加载,我们不断的改变bottomMargin,使其具有上拉效果

另外:我们需要重写RecyclerView的dispatchTouchEvent(),onTouchEvent()来做事件的监听。

3 代码实现

先来看XStatus.java,这里只定义了四种状态

/**
 * Email: 1273482124@qq.com
 * Created by qiyei2015 on 2017/5/25.
 * Version: 1.0
 * Description: 下拉刷新或者上拉加载的状态
 */
public enum XStatus {
    NORMAL,             // 默认状态
    PULL,          // 下拉或者上拉状态
    RELEASE,            // 松开释放状态
    RUNNING          // 正在刷新状态或者正在加载状态
}
/**
 * Email: 1273482124@qq.com
 * Created by qiyei2015 on 2017/5/21.
 * Version: 1.0
 * Description:
 */
public interface IViewCreator {
    /**
     * 获取下拉刷新的View
     * @param context
     * @param parent
     */
    View getView(Context context, ViewGroup parent);

    /**
     * 正在下拉或者正在上拉
     * @param currentHeight
     * @param refreshHeight
     * @param status
     */
    void onPull(int currentHeight, int refreshHeight, XStatus status);

    /**
     * 正在刷新或者正在加载
     */
    void onRunning();

    /**
     * 停止刷新,或者停止加载
     */
    void onStopRunning();

}

定义了创建View的接口,以及在下拉,上拉的动画效果接口,以及正在刷新,正在加载的效果接口,以及停止恢复的接口

/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/5/24.
* Version: 1.0
* Description:
*/
public class CommonRefreshView implements IViewCreator {
// 加载数据的ImageView
private View mRefreshIv;

@Override
public View getView(Context context, ViewGroup parent) {
    View refreshView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header_view, parent, false);
    mRefreshIv = refreshView.findViewById(R.id.refresh_iv);
    return refreshView;

}

@Override
public void onPull(int currentHeight, int refreshHeight, XStatus status) {
    float rotate = ((float) currentHeight) / refreshHeight;
    // 不断下拉的过程中不断的旋转图片
    mRefreshIv.setRotation(rotate * 360);
}

@Override
public void onRunning() {
    // 刷新的时候不断旋转
    RotateAnimation animation = new RotateAnimation(0, 720,
            Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    animation.setRepeatCount(-1);
    animation.setDuration(1000);
    mRefreshIv.startAnimation(animation);
}

@Override
public void onStopRunning() {
    // 停止加载的时候清除动画
    mRefreshIv.setRotation(0);
    mRefreshIv.clearAnimation();
}

}

IViewCretaor的一个默认实现,只是为了演示效果而编写

/**
 * Email: 1273482124@qq.com
 * Created by qiyei2015 on 2017/5/23.
 * Version: 1.0
 * Description:
 */
public class XRecyclerView extends WrapRecyclerView{
    /**
     * 调试标志
     */
    private static final String TAG = XRecyclerView.class.getSimpleName();
    /**
     * 下拉刷新
     */
    private XRecyclerHelper mRefresh;
    /**
     * 上拉加载更多
     */
    private XRecyclerHelper mLoadMore;
    /**
     * 是否下拉刷新
     */
    private boolean isPullRefresh;
    /**
     * 是否上拉加载更多
     */
    private boolean isPullLoad;
    /**
     * 下拉刷新和上拉加载更多的监听器
     */
    private XRecyclerListener mListener;
    /**
     * 第一个可见的item
     */
    private int mFirstVisibile = -1;
    /**
     * 最后一个可见的item
     */
    private int mLastVisibile = -1;
    /**
     * 布局管理器
     */
    private LayoutManager mLayoutManager;

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

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

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

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed && isPullRefresh){

            int refreshHeight = mRefresh.view.getMeasuredHeight();
            mRefresh.viewHeight = refreshHeight;
            Log.d(TAG,"onLayout,mRefresh.viewHeight:" + refreshHeight);
            if (refreshHeight > 0){
                mRefresh.setViewTopMargin(-refreshHeight);   //隐藏掉RefreshView
            }
        }
    }

    /**
     * 记录手指按下的位置 ,之所以写在dispatchTouchEvent那是因为如果我们处理了条目点击事件,
     * 那么就不会进入onTouchEvent里面,所以只能在这里获取
     * @param ev
     * @return
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                mRefresh.downY = (int) ev.getRawY();
                mLoadMore.downY = (int)ev.getRawY();

                break;
            case MotionEvent.ACTION_UP:         //手指离开时,如果是下拉或者上拉,就恢复refreshview
                if (mRefresh.drag){
                    mRefresh.restoreViewTopMargin(mListener);
                }
                if (mLoadMore.drag){
                    mLoadMore.restoreViewBottomMargin(mListener);
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        boolean isScrollTop = false;
        mLayoutManager = getLayoutManager();
        if (mLayoutManager instanceof LinearLayoutManager){
            mFirstVisibile = ((LinearLayoutManager) mLayoutManager).findFirstVisibleItemPosition();
            if (mFirstVisibile == 0 || mFirstVisibile == 1){
                isScrollTop = true;
            }else {
                isScrollTop = false;
            }
        }else {
            isScrollTop = false;
        }

        switch (e.getAction()){
            case MotionEvent.ACTION_MOVE:

                Log.d(TAG,"onTouchEvent,mRefresh.status:" + mRefresh.status);
                //如果在最顶部或者底部才处理,否则不需要处理
                if (mRefresh.status == XStatus.RUNNING || mLoadMore.status == XStatus.RUNNING){
                    return super.onTouchEvent(e);
                }

//                //如果在下拉就滚动到第一个位置,要不然的话,看不到
                if (mRefresh.drag && isScrollTop){
                    scrollToPosition(0);
                }

                if (mLoadMore != null && isPullLoad){
                    mLoadMore.viewHeight = mLoadMore.view.getMeasuredHeight();
                }

                //如果在上拉就滚动到最后一个位置,要不然看不到
                if (mLoadMore.drag && !canScrollDown()){
                    scrollToPosition(getAdapter().getItemCount() - 1);
                }

                int refreshY = (int)((e.getRawY() - mRefresh.downY) * mRefresh.dragValue);
                int loadMoreY = (int)((e.getRawY() - mLoadMore.downY) * mLoadMore.dragValue);

                //手指下拉
                if (refreshY > 0 && isScrollTop && isPullRefresh){
                    refreshY = refreshY - mRefresh.viewHeight;
                    Log.d(TAG,"onTouchEvent,refreshY:" + refreshY);
                    mRefresh.setViewTopMargin(refreshY);
                    mRefresh.updateViewStatus(refreshY);
                    mRefresh.drag = true;
                    return false;
                }
                //手指上拉
                if (loadMoreY < 0 && !canScrollDown() && isPullLoad){
                    Log.d(TAG,"onTouchEvent,loadMoreY:" + -loadMoreY);
                    mLoadMore.setViewBottomMargin(-loadMoreY);
                    mLoadMore.updateViewStatus(-loadMoreY);
                    mLoadMore.drag = true;
                    return false;
                }
                break;
        }
        return super.onTouchEvent(e);
    }

    /**
     * 初始化
     */
    private void init(){
        mRefresh = new XRecyclerHelper(XRecyclerHelper.REFRESH);
        mLoadMore = new XRecyclerHelper(XRecyclerHelper.LOAD_MORE);
    }

    /**
     * 添加下拉刷新的Creator
     * @param creator
     */
    public void addRefreshViewCreator(IViewCreator creator){
        isPullRefresh = true;
        mRefresh.addViewCreator(creator);
        RecyclerView.Adapter adapter = getAdapter();
        if (adapter != null && mRefresh.creator != null){
            View header = mRefresh.creator.getView(getContext(),this);
            if (header != null){
                addRefreshView(header);
                mRefresh.view = header;
            }
        }
    }

    /**
     * 添加上拉加载更多的的Creator
     * @param creator
     */
    public void addLoadMoreViewCreator(IViewCreator creator){
        isPullLoad = true;
        mLoadMore.addViewCreator(creator);
        RecyclerView.Adapter adapter = getAdapter();
        if (adapter != null && mLoadMore.creator != null){
            View foot = mLoadMore.creator.getView(getContext(),this);
            if (foot != null){
                addLoadMoreView(foot);
                mLoadMore.view = foot;
            }
        }
    }

    /**
     * @param pullRefresh the {@link #isPullRefresh} to set
     */
    public void setPullRefresh(boolean pullRefresh) {
        isPullRefresh = pullRefresh;
        if (isPullRefresh){
            addRefreshViewCreator(new CommonRefreshView());
        }
    }

    /**
     * @param pullLoad the {@link #isPullLoad} to set
     */
    public void setPullLoad(boolean pullLoad) {
        isPullLoad = pullLoad;
        if (isPullLoad){
            addLoadMoreViewCreator(new CommonRefreshView());
        }
    }

    /**
     * 停止刷新
     */
    public void stopRefresh(){
        mRefresh.status = XStatus.NORMAL;
        mRefresh.restoreViewTopMargin(mListener);
        if (mRefresh.creator != null){
            mRefresh.creator.onStopRunning();
        }
    }

    /**
     * 停止加载
     */
    public void stopLoadMore(){
        mLoadMore.status = XStatus.NORMAL;
        mLoadMore.restoreViewBottomMargin(mListener);
        if (mLoadMore.creator != null){
            mLoadMore.creator.onStopRunning();
        }
    }

    /**
     * @param  listener the {@link #mListener} to set
     */
    public void setXRecyclerListener(XRecyclerListener listener) {
        mListener = listener;
    }

    /**
     * @return Whether it is possible for the child view of this layout to
     * scroll up. Override this if the child view is a custom view.
     * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
     */
    public boolean canScrollUp() {
        if (android.os.Build.VERSION.SDK_INT < 14) {
            return ViewCompat.canScrollVertically(this, -1) || this.getScrollY() > 0;
        } else {
            return ViewCompat.canScrollVertically(this, -1);
        }
    }

    /**
     * @return Whether it is possible for the child view of this layout to
     * scroll up. Override this if the child view is a custom view.
     * 判断是不是滚动到了最顶部,这个是从SwipeRefreshLayout里面copy过来的源代码
     */
    public boolean canScrollDown() {
        return ViewCompat.canScrollVertically(this, 1);
    }

}

WarpRecyclerView是定义了addHeaderView和addFooterView的RecyclerView,详细见上一篇博客。
可以看到,这里只关联了XRecyclerHelper ,代码逻辑简单,后续对下拉,上拉的修改,都可以直接在XRecyclerHelper中修改

XRecyclerHelper.java

/**
 * Email: 1273482124@qq.com
 * Created by qiyei2015 on 2017/5/25.
 * Version: 1.0
 * Description: 下拉刷新,上拉加载的帮助类
 */
public class XRecyclerHelper {
    /**
     * 调试标志
     */
    private static final String TAG = XRecyclerHelper.class.getSimpleName();
    /**
     * 下拉刷新
     */
    public static final int REFRESH = 1;
    /**
     * 上拉加载更多
     */
    public static final int LOAD_MORE = 2;

    /**
     * view的类型
     */
    public int type;
    /**
     * 下拉刷新上拉加载更多的
     */
    public IViewCreator creator;
    /**
     * 下拉刷新上拉加载的view的高度
     */
    public int viewHeight = 0;
    /**
     * 下拉刷新上拉加载的View
     */
    public View view;
    /**
     * 手指按下的y位置
     */
    public int downY = 0;
    /**
     * 手指下拉或者上拉的阻力系数
     */
    public float dragValue = 0.35f;
    /**
     * 是否在下拉或者上拉
     */
    public boolean drag = false;

    /**
     * 当前下拉状态
     */
    public XStatus status = XStatus.NORMAL;


    public XRecyclerHelper(int type) {
        this.type = type;
    }

    /**
     * 添加下拉刷新的Creator
     * @param creator
     */
    public void addViewCreator(IViewCreator creator){
        this.creator = creator;
    }

    /**
     * 设置RefrshView的MarginTop
     * @param topMargin
     */
    public void setViewTopMargin(int topMargin) {
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) this.view.getLayoutParams();
        if (topMargin < -this.viewHeight){
            topMargin = -this.viewHeight;
        }
        params.topMargin = topMargin;
        Log.d(TAG,"setRefreshViewMarginTop, params.topMargin:" + params.topMargin);
        this.view.setLayoutParams(params);
        this.view.requestLayout();
    }

    /**
     * 恢复并刷新
     * @param listener
     */
    public void restoreViewTopMargin(XRecyclerListener listener) {
        int currentTopMargin = ((ViewGroup.MarginLayoutParams)this.view.getLayoutParams()).topMargin;

        int finalTopMargin = -this.viewHeight; //最终顶部会不显示

        //如果是松开刷新状态,没有下拉了
        if (this.status == XStatus.RELEASE){
            finalTopMargin = 0;
            //转变成正在刷新状态
            this.status = XStatus.RUNNING;
            if (this.creator != null){
                this.creator.onRunning();
            }
            //回调刷新监听器的刷新接口
            if (listener != null){
                listener.onRefresh();
            }
        }

        int y = currentTopMargin - finalTopMargin;
        Log.d(TAG,"restoreRefreshView, currentTopMargin:" + currentTopMargin + ",finalTopMargin:" + finalTopMargin);
        //从当前状态恢复到最终状态
        ValueAnimator animator = ObjectAnimator.ofFloat(currentTopMargin,finalTopMargin).setDuration(300);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float dy = (float) animation.getAnimatedValue();
                setViewTopMargin((int)dy);
            }
        });
        animator.start();

        this.drag = false;
    }


    /**
     * 设置RefrshView的MarginTop
     * @param marginBottom
     */
    public void setViewBottomMargin(int marginBottom) {
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) this.view.getLayoutParams();
        if (marginBottom < 0){
            marginBottom = 0;
        }
        params.bottomMargin = marginBottom;
        Log.d(TAG,"setViewBottomMargin, params.marginBottom:" + params.bottomMargin);
        this.view.setLayoutParams(params);
    }

    /**
     * 恢复并调用listenerde加载方法
     * @param listener
     */
    public void restoreViewBottomMargin(XRecyclerListener listener) {
        int currentBottomMargin = ((ViewGroup.MarginLayoutParams)this.view.getLayoutParams()).bottomMargin;

        int finalBottomMargin = 0; //最终底部会不显示

        //如果是松开,没有上拉加载更多了
        if (this.status == XStatus.RELEASE){
            //转变成正在加载的状态
            this.status = XStatus.RUNNING;
            if (this.creator != null){
                this.creator.onRunning();
            }
            //回调监听器的加载更多接口
            if (listener != null){
                listener.onLoadMore();
            }
        }

        int y = currentBottomMargin - finalBottomMargin;
        Log.d(TAG,"restoreViewBottomMargin, currentBottomMargin:" + currentBottomMargin + ",finalBottomMargin:" + finalBottomMargin);
        //从当前状态恢复到最终状态
        ValueAnimator animator = ObjectAnimator.ofFloat(currentBottomMargin,finalBottomMargin).setDuration(300);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float dy = (float) animation.getAnimatedValue();
                setViewBottomMargin(-(int)dy);
            }
        });
        animator.start();

        this.drag = false;
    }

    /**
     * 根据margin更新View的状态
     * @param margin
     */
    public void updateViewStatus(int margin){
        switch (type){
            case REFRESH:
                if (margin <= -this.viewHeight){ //正常状态
                    this.status = XStatus.NORMAL;
                }else if (margin < 0){    // 下拉或者上拉状态
                    this.status = XStatus.PULL;
                }else {                       // 该释放状态
                    this.status = XStatus.RELEASE;
                }
                break;
            case LOAD_MORE:
                if (margin <= 0){ //正常状态
                    this.status = XStatus.NORMAL;
                }else if (margin < this.viewHeight){    // 上拉状态
                    this.status = XStatus.PULL;
                }else {                       // 该释放状态
                    this.status = XStatus.RELEASE;
                }
                break;
            default:
                break;
        }

        if (this.creator != null){
            this.creator.onPull(margin,this.viewHeight,this.status);
        }
    }

}

这里面主要就是提供了几个接口,
第一就是addViewCreator(),用于为不同的需求定制View
第二提供接口改变和恢复view的topMargin 或者bottomMargin
第三就是提供改变View的状态,以便根据状态进行相应的效果和状态处理

4 运行效果

以上的分析可以看出,我们做到了下拉,上拉的View与RecyclerView的解耦,方便我们进行项目的定制,因为我采用的演示Demo,效果如下:
这里写图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值