Android實現(xiàn)簡單的下拉刷新控件
背景:列表控件在Android App開發(fā)中用到的場景很多。在以前我們用ListView,GradView,現(xiàn)在應(yīng)該大多數(shù)開發(fā)者都已經(jīng)在選擇使用RecyclerView了,谷歌給我們提供了這些方便的列表控件,我們可以很容易的使用它們。但是在實際的場景中,我們可能還想要更多的能力,比如最常見的列表下拉刷新,上拉加載。上拉刷新和下拉加載應(yīng)該是列表的標(biāo)配吧,基本上有列表的地方都要具體這個能力。雖然刷新這個功能已經(jīng)有各種各樣的第三方框架可以選擇,但是畢竟不是自己的嘛,今天我們就來實現(xiàn)一個自己的下拉刷新控件,多動手才能更好的理解。
效果圖:

原理分析:
在coding之前,我們先分析一下原理,原理分析出來之后,我們才可以確定實現(xiàn)方案。
先上一張圖,來個直觀的認(rèn)識:

在列表上面有個刷新頭,隨著手指向下拉,逐漸把頂部不可見的刷新頭拉到屏幕中來,用戶能看到刷新的狀態(tài)變化,達到下拉刷新的目的。
通過分析,我們確定一種實現(xiàn)方案:我們自定義一個容器,容器里面包含兩個部分。
1. 頂部刷新頭。
2. 列表區(qū)域。
確定好布局容器之后,我們來分析刷新頭的幾種狀態(tài)

把下拉刷新分為5中狀態(tài),通過不同狀態(tài)間的切換實現(xiàn)下拉刷新能力。
狀態(tài)間的流程圖如下:

整個下拉刷新的流程就如圖中所示。
流程清楚了之后,接下來就是編寫代碼實現(xiàn)了。
代碼實現(xiàn):
/**
* @author luowang8
* @date 2020-08-21 10:54
* @desc 下拉刷新控件
*/
public class PullRefreshView extends LinearLayout {
/**
* 頭部tag
*/
public static final String HEADER_TAG = "HEADER_TAG";
/**
* 列表tag
*/
public static final String LIST_TAG = "LIST_TAG";
/**
* tag
*/
private static final String TAG = "PullRefreshView";
/**
* 默認(rèn)初始狀態(tài)
*/
private @State
int mState = State.INIT;
/**
* 是否被拖拽
*/
private boolean mIsDragging = false;
/**
* 上下文
*/
private Context mContext;
/**
* RecyclerView
*/
private RecyclerView mRecyclerView;
/**
* 頂部刷新頭
*/
private View mHeaderView;
/**
* 初始Y的坐標(biāo)
*/
private int mInitMotionY;
/**
* 上一次Y的坐標(biāo)
*/
private int mLastMotionY;
/**
* 手指觸發(fā)滑動的臨界距離
*/
private int mSlopTouch;
/**
* 觸發(fā)刷新的臨界值
*/
private int mRefreshHeight = 200;
/**
* 滑動時長
*/
private int mDuring = 300;
/**
* 用戶刷新監(jiān)聽器
*/
private OnRefreshListener mOnRefreshListener;
/**
* 刷新文字提示
*/
private TextView mRefreshTip;
/**
* 是否可拖拽, 因為在刷新頭自由滑動和刷新狀態(tài)的時候,
* 我們應(yīng)該保持界面不被破壞
*/
private boolean mIsCanDrag = true;
/**
* 頭部布局
*/
private LayoutParams mHeaderLayoutParams;
/**
* 列表布局
*/
private LayoutParams mListLayoutParams;
/**
* 屬性動畫
*/
private ValueAnimator mValueAnimator;
/// 分割 ///
/**
* @param context
*/
public PullRefreshView(Context context) {
this(context, null);
}
/**
* @param context
* @param attrs
*/
public PullRefreshView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* @param context
* @param attrs
* @param defStyleAttr
*/
public PullRefreshView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initView();
}
public RecyclerView getRecyclerView() {
return mRecyclerView;
}
/**
* 設(shè)置RecyclerView
*
* @param recyclerView
*/
public void addRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null) {
return;
}
View view = findViewWithTag(LIST_TAG);
if (view != null) {
removeView(view);
}
this.mRecyclerView = recyclerView;
this.mRecyclerView.setTag(LIST_TAG);
addView(recyclerView, mListLayoutParams);
}
/**
* 設(shè)置自定義刷新頭部
* @param headerView
*/
public void addHeaderView(View headerView) {
if (headerView == null) {
return;
}
View view = findViewWithTag(HEADER_TAG);
if (view != null) {
removeView(view);
}
this.mHeaderView = headerView;
this.mHeaderView.setTag(HEADER_TAG);
addView(mHeaderView, mHeaderLayoutParams);
}
/**
* @param onRefreshListener
*/
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
mOnRefreshListener = onRefreshListener;
}
/**
* 初始化View
*/
private void initView() {
setOrientation(LinearLayout.VERTICAL);
Context context = getContext();
/** 1、添加刷新頭Header */
mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header, null);
mHeaderView.setTag(HEADER_TAG);
mRefreshTip = mHeaderView.findViewById(R.id.content);
mHeaderLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
DensityUtil.dip2px(mContext, 500)
);
this.addView(mHeaderView, mHeaderLayoutParams);
/** 2、添加內(nèi)容RecyclerView */
mRecyclerView = new RecyclerView(context);
mRecyclerView.setTag(LIST_TAG);
mListLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
this.addView(mRecyclerView, mListLayoutParams);
/** 3、一開始的時候要讓Header看不見,設(shè)置向上的負paddingTop */
setPadding(0, -DensityUtil.dip2px(mContext, 500), 0, 0);
ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
mSlopTouch = viewConfiguration.getScaledTouchSlop();
setState(State.INIT);
}
/**
* 設(shè)置狀態(tài),每個狀態(tài)下,做不同的事情
*
* @param state 狀態(tài)
*/
private void setState(@State int state) {
switch (state) {
case State.INIT:
initState();
break;
case State.DRAGGING:
dragState();
break;
case State.READY:
readyState();
break;
case State.REFRESHING:
refreshState();
break;
case State.FLING:
flingState();
break;
default:
break;
}
mState = state;
}
/**
* 處理初始化狀態(tài)方法
*/
private void initState() {
// 只有在初始狀態(tài)時,恢復(fù)成可拖拽
mIsCanDrag = true;
mIsDragging = false;
mRefreshTip.setText("下拉刷新");
}
/**
* 處理拖拽時方法
*/
private void dragState() {
mIsDragging = true;
}
/**
* 拖拽距離超過header高度時,如何處理
*/
private void readyState() {
mRefreshTip.setText("松手刷新");
}
/**
* 用戶刷新時,如何處理
*/
private void refreshState() {
if (mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
mIsCanDrag = false;
mRefreshTip.setText("正在刷新,請稍后...");
}
/**
* 自由滾動時,如何處理
*/
private void flingState() {
mIsDragging = false;
mIsCanDrag = false;
/** 自由滾動狀態(tài)可以從兩個狀態(tài)進入:
* 1、READY狀態(tài)。
* 2、其他狀態(tài)。
*
* !滑動均需要平滑滑動
* */
if (mState == State.READY) {
Log.e(TAG, "flingState: 從Ready狀態(tài)開始自由滑動");
// 從準(zhǔn)備狀態(tài)進入,刷新頭滑到 200 的位置
smoothScroll(getScrollY(), -mRefreshHeight);
}
else {
Log.e(TAG, "flingState: 松手后,從其他狀態(tài)開始自由滑動");
// 從刷新狀態(tài)進入,刷新頭直接回到最初默認(rèn)的位置
// 即: 滑出界面,ScrollY 變成 0
smoothScroll(getScrollY(), 0);
}
}
/**
* 光滑滾動
* @param startPos 開始位置
* @param targetPos 結(jié)束位置
*/
private void smoothScroll(int startPos, final int targetPos) {
// 如果有動畫正在播放,先停止
if (mValueAnimator != null && mValueAnimator.isRunning()) {
mValueAnimator.cancel();
mValueAnimator.end();
mValueAnimator = null;
}
// 然后開啟動畫
mValueAnimator = ValueAnimator.ofInt(getScrollY(), targetPos);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (int) valueAnimator.getAnimatedValue();
scrollTo(0, value);
if (getScrollY() == targetPos) {
if (targetPos != 0) {
setState(State.REFRESHING);
}
else {
setState(State.INIT);
}
}
}
});
mValueAnimator.setDuration(mDuring);
mValueAnimator.start();
}
/**
* 是否準(zhǔn)備好觸發(fā)下拉的狀態(tài)了
*/
private boolean isReadyToPull() {
if (mRecyclerView == null) {
return false;
}
LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
if (manager == null) {
return false;
}
if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {
View child = mRecyclerView.getChildAt(0);
int height = child.getHeight();
if (height > mRecyclerView.getHeight()) {
return child.getTop() == 0 && manager.findFirstVisibleItemPosition() == 0;
}
else {
return manager.findFirstCompletelyVisibleItemPosition() == 0;
}
}
return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
Log.e(TAG, "onInterceptTouchEvent: action = " + action);
if (!mIsCanDrag) {
return true;
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsDragging = false;
return false;
}
if (mIsDragging && action == MotionEvent.ACTION_MOVE) {
return true;
}
switch (action) {
case MotionEvent.ACTION_MOVE:
int diff = (int) (ev.getY() - mLastMotionY);
if (Math.abs(diff) > mSlopTouch && diff > 1 && isReadyToPull()) {
mLastMotionY = (int) ev.getY();
mIsDragging = true;
}
break;
case MotionEvent.ACTION_DOWN:
if (isReadyToPull()) {
setState(State.INIT);
mInitMotionY = (int) ev.getY();
mLastMotionY = (int) ev.getY();
}
break;
default:
break;
}
return mIsDragging;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
Log.e(TAG, "onTouchEvent: action = " + action);
if (!mIsCanDrag) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
if (isReadyToPull()) {
setState(State.INIT);
mInitMotionY = (int) event.getY();
mLastMotionY = (int) event.getY();
}
break;
case MotionEvent.ACTION_MOVE:
if (mIsDragging) {
mLastMotionY = (int) event.getY();
setState(State.DRAGGING);
pullScroll();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsDragging = false;
setState(State.FLING);
break;
default:
break;
}
return true;
}
/**
* 下拉移動界面,拉出刷新頭
*/
private void pullScroll() {
/** 滾動值 = 初始值 - 結(jié)尾值 */
int scrollValue = (mInitMotionY - mLastMotionY) / 3;
if (scrollValue > 0) {
scrollTo(0, 0);
return;
}
if (Math.abs(scrollValue) > mRefreshHeight
&& mState == State.DRAGGING) {
// 約定:如果偏移量超過 200(這個值,表示是否可以啟動刷新的臨界值,可任意定),
// 那么狀態(tài)變成 State.READY
Log.e(TAG, "pullScroll: 超過了觸發(fā)刷新的臨界值");
setState(State.READY);
}
scrollTo(0, scrollValue);
}
/**
* 刷新完成,需要調(diào)用方主動發(fā)起,才能完成將刷新頭收起
*/
public void refreshComplete() {
mRefreshTip.setText("刷新完成!");
setState(State.FLING);
}
@IntDef({
State.INIT
, State.DRAGGING
, State.READY
, State.REFRESHING
, State.FLING,
})
@Retention(RetentionPolicy.SOURCE)
public @interface State {
/**
* 初始狀態(tài)
*/
int INIT = 1;
/**
* 手指拖拽狀態(tài)
*/
int DRAGGING = 2;
/**
* 就緒狀態(tài),松開手指后,可以刷新
*/
int READY = 3;
/**
* 刷新狀態(tài),這個狀態(tài)下,用戶用于發(fā)起刷新請求
*/
int REFRESHING = 4;
/**
* 松開手指,頂部自然回彈的狀態(tài),有兩種表現(xiàn)
* 1、手指釋放時的高度大于刷新頭的高度。
* 2、手指釋放時的高度小于刷新頭的高度。
*/
int FLING = 5;
}
/**
* 用戶刷新狀態(tài)的操作
*/
public interface OnRefreshListener {
void onRefresh();
}
}
實現(xiàn)的邏輯并不復(fù)雜,新手都能看懂,先理解了整個流程,代碼就是水到渠成的事。
思想第一,最后代碼。
完整DEMO直通車
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
android判斷手機是否安裝地圖應(yīng)用實現(xiàn)跳轉(zhuǎn)到該地圖應(yīng)用
這篇文章主要給大家介紹了android如何判斷手機是否安裝地圖應(yīng)用,并實現(xiàn)跳轉(zhuǎn)到該地圖應(yīng)用的方法,需要的朋友可以參考借鑒,下面來一起學(xué)習(xí)學(xué)習(xí)吧。2017-01-01
Android開發(fā)RecyclerView實現(xiàn)折線圖效果
這篇文章主要為大家詳細介紹了Android開發(fā)RecyclerView實現(xiàn)折線圖效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-09-09
Android簡單實現(xiàn)動態(tài)權(quán)限獲取相機權(quán)限及存儲空間等多權(quán)限
這篇文章主要介紹了Android簡單實現(xiàn)動態(tài)權(quán)限獲取相機權(quán)限及存儲空間等多權(quán)限,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-07-07
Android退出應(yīng)用最優(yōu)雅的方式(改進版)
這篇文章主要介紹了Android退出應(yīng)用最優(yōu)雅的方式,改進版,感興趣的小伙伴們可以參考一下2016-01-01
Android 使用Picasso加載網(wǎng)絡(luò)圖片等比例縮放的實現(xiàn)方法
在做android圖片加載的時候,由于手機屏幕受限,很多大圖加載過來的時候,我們要求等比例縮放,接下來小編給大家?guī)砹薃ndroid 使用Picasso加載網(wǎng)絡(luò)圖片等比例縮放的實現(xiàn)方法,感興趣的朋友一起看看吧2018-08-08
Android 自定義圖片地圖坐標(biāo)功能的實現(xiàn)
最近項目要求實現(xiàn)一個在自定義地圖圖片上添加坐標(biāo)信息的功能,類似于在圖片做標(biāo)注的功能,這種功能糾結(jié)該如何實現(xiàn)呢?下面小編通過實例代碼給大家介紹Android 自定義地圖的實現(xiàn),需要的朋友參考下吧2021-07-07

