RecyclerView 上拉加载 PullToRefreshRecyclerView

来源:互联网 发布:放置江湖人物评价算法 编辑:程序博客网 时间:2024/05/20 17:27
## 设计思路ListView 上拉加载很容易实现,监听 ListView 滑动到底部,显示 FootView 即可,但是 RecyclerView 没有 addFootView(View view) 这个方法,有一种方案是在 adapter 中加一个 itemView 用于显示 FootView,但是这样做就得每一个使用到 RecyclerView 的地方都写一遍重复代码,而且不能适配多种类型的 LayoutManager,所以 RecyclerView 就需要另外一种思路来实现,我的解决方案是:自定义 PullToRefreshRecyclerView 继承 FrameLayout,将 RecyclerView 与 FootView 添加到 PullToRefreshRecyclerView 中,上拉时显示 FootView。效果如下:
预览图
## 具体方案布局文件中很简单,一个 FrameLayout 内部放一个 RecyclerView 和 一个 FootView,布局文件如下:
<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:id="@+id/root_view"    android:layout_width="match_parent"    android:layout_height="match_parent">    <android.support.v7.widget.RecyclerView        android:id="@+id/recycler_view"        android:layout_width="match_parent"        android:layout_height="wrap_content"/>    <LinearLayout        android:id="@+id/footer_view"        android:layout_width="match_parent"        android:layout_height="80dp"        android:orientation="horizontal"        android:gravity="center"        android:layout_gravity="bottom">        <ProgressBar            android:id="@+id/progress"            android:layout_width="20dp"            android:layout_height="20dp"            android:visibility="gone"            android:layout_marginRight="5dp"/>        <ImageView            android:id="@+id/img_arrow"            android:layout_width="20dp"            android:layout_height="20dp"            android:scaleType="centerInside"            android:src="@mipmap/arrow"/>        <TextView            android:id="@+id/tv_load_tag"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:gravity="center"            android:layout_marginLeft="5dp"            android:text="上拉加载数据"/>    </LinearLayout></FrameLayout>
这个布局的效果如下图:
预览图

可以看到有个很明显的问题,FootView 是一直在屏幕内部的,即使隐藏掉滑动到底部在显示效果也很差劲,我的解决方案是重写 PullToRefreshRecyclerView 的 onMeasure 方法,将 PullToRefreshRecyclerView 高度设置为屏幕高度加上 FootView 的高度,上拉时使用 scrollTo 方法显示 FootView。
onMeasure 方法如下:

@Overridepublic void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    final int height = getMeasuredHeight() + footViewHeight;    setMeasuredDimension(getMeasuredWidth(), height);    LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams();    rootLP.height = height;    rootView.setLayoutParams(rootLP);//设置 FootView的高度    LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams();    recyclerLp.height = height - footViewHeight;    recyclerView.setLayoutParams(recyclerLp);//将 RecyclerView 的高度设置为屏幕高度}

这样基本布局就完成了,然后就是对滑动事件的拦截与分发,如果滑动到底部则拦截事件,并且根据滑动距离来计算 scrollTo 的移动距离,具体代码如下:

@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    if (recyclerView == null || recyclerView.getChildCount() == 0)        return super.onInterceptTouchEvent(ev);    if(isLoading) return true;//如果正在加载中则拦截滑动事件    switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            lastDownY = (int) ev.getY();            break;        case MotionEvent.ACTION_MOVE:            int lastPosition = -1;            //以下代码用于获取当前 RecyclerView 中显示的最后一个 position            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();            if (layoutManager instanceof GridLayoutManager) {                lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();            } else if (layoutManager instanceof LinearLayoutManager) {                lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();            } else if (layoutManager instanceof StaggeredGridLayoutManager) {                int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];                ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions);                lastPosition = findMax(lastPositions);            }            int offerY = (int) ev.getY() - lastDownY;            if (offerY < 0) {              //如果正在上拉                View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);                if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) {                  //如果滑动到最底部则拦截事件并设置标志位                    canScroll = true;                    return true;                } else {                    canScroll = false;                }            } else {                canScroll = false;            }            break;    }    return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent ev) {    if (recyclerView == null || recyclerView.getChildCount() == 0)        return super.onInterceptTouchEvent(ev);    int offerY;    switch (ev.getAction()) {        case MotionEvent.ACTION_DOWN:            lastDownY = (int) ev.getY();            break;        case MotionEvent.ACTION_MOVE:            if (canScroll) {                offerY = (int) ev.getY() - lastDownY;                lastOfferY = offerY;                if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE);                //将 PullToRefreshRecyclerView 内部跟着手指向上移动,移动距离为手指滑动距离的一半                scrollTo(getScrollX(), -offerY / 2);                imgArrow.setVisibility(VISIBLE);                if (Math.abs(offerY) / 2 < footViewHeight) {                    progressBar.setVisibility(GONE);                    tvLoadTag.setText("上拉加载数据");                    if (!arrowIsTop) {                        imgArrow.startAnimation(topAnimation);                        arrowIsTop = true;                    }                    canLoad = false;                } else {                    progressBar.setVisibility(GONE);                    tvLoadTag.setText("松手加载更多");                    if (arrowIsTop) {                        imgArrow.startAnimation(bottomAnimation);                        arrowIsTop = false;                    }                    canLoad = true;                }            }            break;        case MotionEvent.ACTION_UP:            if (canScroll) {                if (!canLoad) {                  //手指松开后如果滑动距离小于设定距离则回到初始状态                    scrollTo(getScrollX(), 0);                } else {                  //如果滑动距离大于设定距离则加载数据并回弹到加载状态                    mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500);                    lastOfferY = 0;                    loadData();                }                canScroll = false;            }            break;    }    return super.onTouchEvent(ev);}

以上便是事件的拦截与分发,另外需要注意的是:如果与 SwipeRefreshLayout 结合使用会出现一个小问题,就是当处于上拉加载状态时如果下拉可能会出现滑动冲突,当然了也有解决方案,自定义的这个 PullToRefreshRecyclerView 类实现了 SwipeRefreshLayout.OnChildScrollUpCallback 接口,当 SwipeRefreshLayout 判断当前 View 是否处于可下拉刷新状态时会首先使用这个接口来判断,我这里做了相应的方法,使用时只需要将 PullToRefreshRecyclerView 对象传给 SwipeRefreshLayout.OnChildScrollUpCallback 接口即可,如下:

swipeRefresh.setOnChildScrollUpCallback(pullToRefreshRecyclerView);

下面放上 PullToRefreshRecyclerView 源码:

package com.zhangke.widget;import android.content.Context;import android.support.annotation.Nullable;import android.support.v4.view.ViewPager;import android.support.v4.widget.SwipeRefreshLayout;import android.support.v7.widget.GridLayoutManager;import android.support.v7.widget.LinearLayoutManager;import android.support.v7.widget.RecyclerView;import android.support.v7.widget.StaggeredGridLayoutManager;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.ViewParent;import android.view.animation.Animation;import android.view.animation.RotateAnimation;import android.widget.FrameLayout;import android.widget.ImageView;import android.widget.ProgressBar;import android.widget.Scroller;import android.widget.TextView;import com.seagetech.ptduser.test.R;/** * Created by 张可 on 2017/7/5. */public class PullToRefreshRecyclerView extends FrameLayout implements SwipeRefreshLayout.OnChildScrollUpCallback {    private static final String TAG = "PullToRefreshRecycler";    private View rootView;    private RecyclerView recyclerView;    private View footView;    private ProgressBar progressBar;    private TextView tvLoadTag;    private ImageView imgArrow;    private int footViewHeight = 100;    private int lastDownY;    private boolean canScroll = false;    private boolean canLoad = false;    private boolean isLoading = false;//是否正在加载,正在加载时拦截滑动事件    /**     * 箭头方向是否向上     */    private boolean arrowIsTop = true;    private RotateAnimation bottomAnimation;//箭头由上到下的动画    private RotateAnimation topAnimation;//箭头由下到上的动画    private Scroller mScroller;    private OnPullToBottomListener onPullToBottomListener;    private int lastOfferY = 0;    public PullToRefreshRecyclerView(Context context) {        super(context);        init();    }    public PullToRefreshRecyclerView(Context context, @Nullable AttributeSet attrs) {        super(context, attrs);        init();    }    private void init() {        inflate(getContext(), R.layout.view_pull_to_refresh_recycler, this);        rootView = findViewById(R.id.root_view);        recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);        footView = findViewById(R.id.footer_view);        progressBar = (ProgressBar) findViewById(R.id.progress);        tvLoadTag = (TextView) findViewById(R.id.tv_load_tag);        imgArrow = (ImageView) findViewById(R.id.img_arrow);        footViewHeight = dip2px(getContext(), 80);        bottomAnimation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);        bottomAnimation.setDuration(200);        bottomAnimation.setFillAfter(true);        topAnimation = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);        topAnimation.setDuration(200);        topAnimation.setFillAfter(true);        mScroller = new Scroller(getContext());    }    public void setLayoutManager(RecyclerView.LayoutManager layout) {        recyclerView.setLayoutManager(layout);    }    public void setAdapter(RecyclerView.Adapter adapter) {        recyclerView.setAdapter(adapter);        adapter.notifyDataSetChanged();    }    public RecyclerView getRecyclerView() {        return recyclerView;    }    @Override    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        final int height = getMeasuredHeight() + footViewHeight;        setMeasuredDimension(getMeasuredWidth(), height);        LayoutParams rootLP = (LayoutParams) rootView.getLayoutParams();        rootLP.height = height;        rootView.setLayoutParams(rootLP);        LayoutParams recyclerLp = (LayoutParams) recyclerView.getLayoutParams();        recyclerLp.height = height - footViewHeight;        recyclerView.setLayoutParams(recyclerLp);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        if (recyclerView == null || recyclerView.getChildCount() == 0)            return super.onInterceptTouchEvent(ev);        if(isLoading) return true;        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                lastDownY = (int) ev.getY();                break;            case MotionEvent.ACTION_MOVE:                int lastPosition = -1;                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();                if (layoutManager instanceof GridLayoutManager) {                    lastPosition = ((GridLayoutManager) layoutManager).findLastVisibleItemPosition();                } else if (layoutManager instanceof LinearLayoutManager) {                    lastPosition = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();                } else if (layoutManager instanceof StaggeredGridLayoutManager) {                    int[] lastPositions = new int[((StaggeredGridLayoutManager) layoutManager).getSpanCount()];                    ((StaggeredGridLayoutManager) layoutManager).findLastVisibleItemPositions(lastPositions);                    lastPosition = findMax(lastPositions);                }                int offerY = (int) ev.getY() - lastDownY;                if (offerY < 0) {                    View lastView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);                    if (lastView != null && lastView.getBottom() + footViewHeight >= getHeight() && lastPosition == recyclerView.getLayoutManager().getItemCount() - 1) {                        canScroll = true;                        return true;                    } else {                        canScroll = false;                    }                } else {                    canScroll = false;                }                break;        }        return super.onInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent ev) {        if (recyclerView == null || recyclerView.getChildCount() == 0)            return super.onInterceptTouchEvent(ev);        int offerY;        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                lastDownY = (int) ev.getY();                break;            case MotionEvent.ACTION_MOVE:                if (canScroll) {                    offerY = (int) ev.getY() - lastDownY;                    lastOfferY = offerY;                    if(footView.getVisibility() == GONE) footView.setVisibility(VISIBLE);                    scrollTo(getScrollX(), -offerY / 2);                    imgArrow.setVisibility(VISIBLE);                    if (Math.abs(offerY) / 2 < footViewHeight) {                        progressBar.setVisibility(GONE);                        tvLoadTag.setText("上拉加载数据");                        if (!arrowIsTop) {                            imgArrow.startAnimation(topAnimation);                            arrowIsTop = true;                        }                        canLoad = false;                    } else {                        progressBar.setVisibility(GONE);                        tvLoadTag.setText("松手加载更多");                        if (arrowIsTop) {                            imgArrow.startAnimation(bottomAnimation);                            arrowIsTop = false;                        }                        canLoad = true;                    }                }                break;            case MotionEvent.ACTION_UP:                if (canScroll) {                    if (!canLoad) {                        scrollTo(getScrollX(), 0);                    } else {                        mScroller.startScroll(getScrollX(), getScrollY(), getScrollX(), -(Math.abs(lastOfferY) / 2 - footViewHeight), 500);                        lastOfferY = 0;                        loadData();                    }                    canScroll = false;                }                break;        }        return super.onTouchEvent(ev);    }    @Override    public boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child) {        if(recyclerView.getChildCount() > 0 && recyclerView.getChildAt(0).getTop() < recyclerView.getPaddingTop()){            Log.e(TAG, "canChildScrollUp return true");            return true;        }        Log.e(TAG, "canChildScrollUp return false");        return false;    }    private void loadData() {        isLoading = true;        imgArrow.clearAnimation();        imgArrow.setVisibility(GONE);        progressBar.setVisibility(VISIBLE);        tvLoadTag.setText("正在加载...");        if (this.onPullToBottomListener != null) {            postDelayed(new Runnable() {                @Override                public void run() {                    PullToRefreshRecyclerView.this.onPullToBottomListener.onPullToBottom();                }            }, 500);        } else {            progressBar.setVisibility(GONE);            scrollTo(getScrollX(), 0);        }    }    /**     * 设置是否正在加载,一般来书,在加载完毕之后应该调用此方法     *     * @param loading     */    public void setLoading(final boolean loading) {        isLoading = loading;        if (!isLoading) {            post(new Runnable() {                @Override                public void run() {                    if (!loading) {                        progressBar.setVisibility(GONE);                        scrollTo(getScrollX(), 0);                    }                }            });        }    }    @Override    public void computeScroll() {        if (mScroller.computeScrollOffset()) {            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());            invalidate();        }    }    //找到数组中的最大值    private int findMax(int[] lastPositions) {        int max = lastPositions[0];        for (int value : lastPositions) {            if (value > max) {                max = value;            }        }        return max;    }    public void setOnPullToBottomListener(OnPullToBottomListener onPullToBottomListener) {        this.onPullToBottomListener = onPullToBottomListener;    }    public interface OnPullToBottomListener {        void onPullToBottom();    }    /**     * 将dip或dp值转换为px值,保证尺寸大小不变     *     * @param dipValue     * @param dipValue (DisplayMetrics类中属性density)     * @return     */    public static int dip2px(Context context, float dipValue) {        final float scale = context.getResources().getDisplayMetrics().density;        return (int) (dipValue * scale + 0.5f);    }}

点次查看源码及资源文件

原创粉丝点击