Android RecyclerView刷新和加载

上一章中,我们了解了RecyclerView控件的基本用法,这里我们将实现RecyclerView的刷新和加载。

1. SwipeRefreshLayout下拉刷新

我们可以利用SwipeRefreshLayout控件来实现下拉刷新,详见Android SwipeRefreshLayout控件

2. 自定义下拉刷新

我们首先创建一个下拉刷新的父类RefreshViewCreator,一般拥有四种状态,普通、下拉刷新、释放刷新和刷新。
主要方法如下

  • View onCreateRefreshView(Context context, ViewGroup parent),创建刷新界面
  • int move(int dy),用于下拉时改变界面
  • release(float dy),用于释放时改变界面
  • refreshFinish(),刷新结束,一般用于回调

具体代码如下,

public abstract class RefreshViewCreator {
    public final static int DONE               = 0;
    public final static int PULL_TO_REFRESH    = 1; // 下拉刷新
    public final static int RELEASE_TO_REFRESH = 2; // 释放刷新
    public final static int REFRESHING         = 3; // 刷新

    public IOnRefreshListener mListener;

    public RefreshViewCreator(IOnRefreshListener listener) {
        this.mListener = listener;
    }

    // 创建刷新界面
    public abstract View onCreateRefreshView(Context context, ViewGroup parent);

    // 下拉时改变界面
    public int move(int dy) {
        return 0;
    }

    // 释放时改变界面
    public void release(float dy) {
    }

    // 刷新结束
    public void refreshFinish() {
    }

    protected void startRefresh() {
        if (mListener != null) {
            mListener.onRefresh();
        }
    }

    public interface IOnRefreshListener {
        void onRefresh();
    }

}

CustomRecyclerView继承RecyclerView,默认使用LinearLayoutManager布局。

public class CustomRecyclerView extends RecyclerView {
    private LinearLayoutManager mLayoutManager;
    private RefreshViewCreator mRefreshViewCreator;
    private boolean mRecord;
    private int mLastEventY;
    private boolean mRefresh;
    private int mTouchSlop;

    public CustomRecyclerView(@NonNull Context context) {
        this(context, null);
    }

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

    public CustomRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
        setLayoutManager(mLayoutManager);

        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    public void setRefreshViewCreator(RefreshViewCreator refreshViewCreator) {
        this.mRefreshViewCreator = refreshViewCreator;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                record(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                record(ev);

                int dy = (int) (ev.getY() - mLastEventY);
                mLastEventY = (int) ev.getY();
                if (allowRefresh()) {
                    if (dy > mTouchSlop) {
                        mRefresh = true;
                    }
                    if (mRefresh) {
                        int distance = mRefreshViewCreator.move(dy / 3);
                        if (distance == 0) {
                            ev.setAction(MotionEvent.ACTION_CANCEL);
                            return dispatchTouchEvent(ev);
                        }
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (mRefresh) {
                    mRefreshViewCreator.release((int) (ev.getY() - mLastEventY) / 3);
                    mRefresh = false;
                    mLastEventY = 0;
                    mRecord = false;
                    return true;
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    private void record(MotionEvent ev) {
        if (allowRefresh() && !mRecord) {
            mLastEventY = (int) ev.getY();
            mRecord = true;
        }
    }

    // 是否滚动到表头了
    private boolean allowRefresh() {
        return mRefreshViewCreator != null && !canScrollVertically(-1);
    }

}

我们通过allowRefresh()方法来判断是否已经滚动到表头了,这里有个问题是canScrollVertically()方法,当direction等于-1的时候,才是判断是否可以下拉。另外如果刷新界面高度是0或者gone的时候,canScrollVertically(-1)始终返回true。

/**
 * Check if this view can be scrolled vertically in a certain direction.
 *
 * @param direction Negative to check scrolling up, positive to check scrolling down.
 * @return true if this view can be scrolled in the specified direction, false otherwise.
 */
public boolean canScrollVertically(int direction) {
    final int offset = computeVerticalScrollOffset();
    final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
    if (range == 0) return false;
    if (direction < 0) {
        return offset > 0;
    } else {
        return offset < range - 1;
    }
}

Activity里面,创建一个RefreshViewCreator,利用RecyclerViewAdapter加入到表头第一行,RecyclerViewAdapter可查看上一章

private Handler mHandler = new Handler();
private RefreshViewCreator mRefreshViewCreator;

private RefreshViewCreator.IOnRefreshListener mListener = new RefreshViewCreator.IOnRefreshListener() {
    @Override
    public void onRefresh() {

        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                mRefreshViewCreator.refreshFinish();
            }
        }, 3000);
    }
};

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_material_design_recycler_view_refresh);

    CustomRecyclerView recyclerView = findViewById(R.id.recycler_view);
    RecyclerViewAdapter adapter = new RecyclerViewAdapter(this);
    recyclerView.setAdapter(adapter);

    mRefreshViewCreator = new CustomRefreshViewCreator(mListener);
    recyclerView.setRefreshViewCreator(mRefreshViewCreator);

    adapter.addHeaderView(mRefreshViewCreator.onCreateRefreshView(this, recyclerView));
}

CustomRefreshViewCreator继承RefreshViewCreator,实现自定义刷新界面

public class CustomRefreshViewCreator extends RefreshViewCreator {

    private View mRefreshView;
    private ImageView mIvArrow;
    private ProgressBar mProgressBar;
    private TextView mTvRefreshTips;

    private String mPullToRefreshTips, mReleaseToRefreshTips, mRefreshingTips;
    private int mContentHeight;

    private int mHeadState = DONE;
    private int mDistance = 0;

    public CustomRefreshViewCreator(IOnRefreshListener listener) {
        super(listener);

        mPullToRefreshTips = "下拉刷新";
        mReleaseToRefreshTips = "松开刷新";
        mRefreshingTips = "正在刷新...";
    }

    @Override
    public View onCreateRefreshView(Context context, ViewGroup parent) {
        if (mRefreshView == null) {
            mRefreshView = LayoutInflater.from(context).inflate(getLayoutId(), parent, false);

            mContentHeight = context.getResources().getDimensionPixelOffset(R.dimen.margin_dpi_50);
            mIvArrow = mRefreshView.findViewById(R.id.iv_arrow);
            mProgressBar = mRefreshView.findViewById(R.id.progress_bar_refresh);
            mTvRefreshTips = mRefreshView.findViewById(R.id.tv_refresh_tips);

            refreshFinish();
        }
        return mRefreshView;
    }

    protected @LayoutRes int getLayoutId() {
        return R.layout.list_view_refresh_view;
    }

    @Override
    public int move(int dy) {
        if (mHeadState != REFRESHING) {
            mDistance += dy;

            if (mDistance < 0) {
                mDistance = 0;
            } else if (mDistance > mContentHeight + 10) {
                mDistance = mContentHeight + 10;
            }

            if (mDistance <= 0) {
                mHeadState = DONE;
            } else if (mDistance >= mContentHeight) {
                mHeadState = RELEASE_TO_REFRESH;
                mIvArrow.setRotation(180);
            } else {
                mHeadState = PULL_TO_REFRESH;
                mIvArrow.setRotation(0);
            }
            if (mHeadState == PULL_TO_REFRESH || mHeadState == RELEASE_TO_REFRESH) {
                int padding = mDistance - mContentHeight;
                mRefreshView.setPadding(0, padding > 0 ? 0 : padding, 0, 0);
            }
            changeHeaderViewByState();
        }
        return mDistance;
    }

    @Override
    public void release(float dy) {
        if (mHeadState != REFRESHING) {
            mDistance += dy;
            if (mDistance >= mContentHeight) {
                mHeadState = REFRESHING;
            } else {
                mHeadState = DONE;
            }
            mDistance = 0;
            changeHeaderViewByState();

            if (mHeadState == REFRESHING) {
                startRefresh();
            }
        }
    }

    @Override
    public void refreshFinish() {
        mHeadState = DONE;
        changeHeaderViewByState();
    }

    private void changeHeaderViewByState() {
        switch (mHeadState) {
            case DONE:
                mRefreshView.setPadding(0, -1 * mContentHeight + 1, 0, 0);

                mIvArrow.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);

                mTvRefreshTips.setText(mPullToRefreshTips);
                break;
            case PULL_TO_REFRESH:
                mIvArrow.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);

                mTvRefreshTips.setText(mPullToRefreshTips);
                break;
            case RELEASE_TO_REFRESH:
                mIvArrow.setVisibility(View.VISIBLE);
                mProgressBar.setVisibility(View.GONE);

                mTvRefreshTips.setText(mReleaseToRefreshTips);
                break;
            case REFRESHING:
                mRefreshView.setPadding(0, 0, 0, 0);

                mIvArrow.setVisibility(View.GONE);
                mProgressBar.setVisibility(View.VISIBLE);

                mTvRefreshTips.setText(mRefreshingTips);
                break;
        }
    }

}

刷新界面list_view_refresh_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/margin_dpi_50"
        android:orientation="horizontal"
        android:gravity="center">
        <ImageView
            android:id="@+id/iv_arrow"
            android:layout_width="12dp"
            android:layout_height="12dp"
            android:src="@drawable/list_arrow"
            android:scaleType="fitXY" />
        <ProgressBar
            android:id="@+id/progress_bar_refresh"
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:indeterminateDrawable="@anim/anim_progress_bar"
            android:indeterminateDuration="1000"
            android:visibility="gone"/>
        <TextView
            android:id="@+id/tv_refresh_tips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:textSize="15sp"/>
    </LinearLayout>
</LinearLayout>

显示如下
在这里插入图片描述

3. 自定义上拉加载

上拉加载的实现类似于刷新,同样需要上拉加载的父类LoadViewCreator

public abstract class LoadViewCreator {
    public final static int DONE                = 0;
    public final static int PULL_TO_LOAD        = 1; // 上拉加载
    public final static int RELEASE_TO_LOAD     = 2; // 释放加载
    public final static int LOADING             = 3; // 加载

    public IOnLoadListener mListener;

    public LoadViewCreator(IOnLoadListener listener) {
        this.mListener = listener;
    }

    public abstract View onCreateLoadView(Context context, ViewGroup parent);

    public int move(int dy) {
        return 0;
    }

    public void release(float dy) {
    }

    public void loadFinish() {
    }

    protected void startLoad() {
        if (mListener != null) {
            mListener.onLoad();
        }
    }

    public interface IOnLoadListener {
        void onLoad();
    }

}

自定义LoadViewCreator的实现类似于CustomRefreshViewCreator,只需要注意的是我们设置bottompadding

mLoadView.setPadding(0, 0, 0, padding);

而在CustomRecyclerView里面,同样在dispatchTouchEvent()方法里面处理事件吗,只是在调用LoadViewCreatormove()和release()`方法,需要把参数的值转换成正数。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            record(ev);
            break;
        case MotionEvent.ACTION_MOVE:
            record(ev);

            int dy = (int) (ev.getY() - mLastEventY);
            mLastEventY = (int) ev.getY();
            if (allowRefresh()) {
                if (dy > mTouchSlop) {
                    mRefresh = true;
                }
                if (mRefresh) {
                    int distance = mRefreshViewCreator.move(dy / 3);
                    if (distance == 0) {
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        return dispatchTouchEvent(ev);
                    }
                    return true;
                }
            }
            if (allowLoad()) {
                if (-dy > mTouchSlop) {
                    mLoad = true;
                }
                if (mLoad) {
                    int distance = mLoadViewCreator.move(-dy);
                    if (distance == 0) {
                        ev.setAction(MotionEvent.ACTION_CANCEL);
                        return dispatchTouchEvent(ev);
                    }
                    return true;
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mRecord = false;
            if (mRefresh) {
                mRefreshViewCreator.release((int) (ev.getY() - mLastEventY) / 3);
                mRefresh = false;
                mLastEventY = 0;
                return true;
            }
            if (mLoad) {
                mLoadViewCreator.release((int) (mLastEventY - ev.getY()));
                mLoad = false;
                mLastEventY = 0;
                return true;
            }
            break;
    }
    return super.dispatchTouchEvent(ev);
}

private void record(MotionEvent ev) {
    if ((allowRefresh() || allowLoad()) && !mRecord) {
        mLastEventY = (int) ev.getY();
        mRecord = true;
    }
}

private boolean allowLoad() {
    return mLoadViewCreator != null && !canScrollVertically(1);
}

显示如下
在这里插入图片描述

4. 自动上拉加载

当用户向上翻动列表,快要到所有列表项的底部或者已经到底部的时候,可以自动加载更多的数据。为此,我们需要监听RecyclerView列表的滚动,

public void addOnScrollListener(OnScrollListener listener)

同时也获取列表最下面的元素是哪一行,LinearLayoutManager为我们提供了很多接口

public int findFirstVisibleItemPosition()
public int findFirstCompletelyVisibleItemPosition()
public int findLastVisibleItemPosition()
public int findLastCompletelyVisibleItemPosition()

实现代码如下

final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(layoutManager);

final RecyclerViewAdapter adapter = new RecyclerViewAdapter(this);
recyclerView.setAdapter(adapter);

View footerView = getLayoutInflater().inflate(R.layout.list_view_load_more_view, recyclerView, false);
adapter.addFooterView(footerView);

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        if (!mLoadMore && layoutManager.findLastVisibleItemPosition() == adapter.getItemCount() - 1) {
            mLoadMore = true;
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    adapter.addContent();
                    mLoadMore = false;
                }
            }, 1000);
        }
    }
});

显示如下
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章