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);
        }
    }
});

顯示如下
在這裏插入圖片描述

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