1. 程式人生 > 程式設計 >Android實現簡單的下拉重新整理控制元件

Android實現簡單的下拉重新整理控制元件

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

效果圖:

Android實現簡單的下拉重新整理控制元件

原理分析:

在coding之前,我們先分析一下原理,原理分析出來之後,我們才可以確定實現方案。

先上一張圖,來個直觀的認識:

Android實現簡單的下拉重新整理控制元件

在列表上面有個重新整理頭,隨著手指向下拉,逐漸把頂部不可見的重新整理頭拉到螢幕中來,使用者能看到重新整理的狀態變化,達到下拉重新整理的目的。

通過分析,我們確定一種實現方案:我們自定義一個容器,容器裡面包含兩個部分。

1. 頂部重新整理頭。
2. 列表區域。

確定好佈局容器之後,我們來分析重新整理頭的幾種狀態

Android實現簡單的下拉重新整理控制元件

把下拉重新整理分為5中狀態,通過不同狀態間的切換實現下拉重新整理能力。

狀態間的流程圖如下:

Android實現簡單的下拉重新整理控制元件

整個下拉重新整理的流程就如圖中所示。

流程清楚了之後,接下來就是編寫程式碼實現了。

程式碼實現:

/**
 * @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";
 
 /**
 * 預設初始狀態
 */
 private @State
 int mState = State.INIT;
 
 /**
 * 是否被拖拽
 */
 private boolean mIsDragging = false;
 
 /**
 * 上下文
 */
 private Context mContext;
 
 
 /**
 * RecyclerView
 */
 private RecyclerView mRecyclerView;
 
 /**
 * 頂部重新整理頭
 */
 private View mHeaderView;
 
 /**
 * 初始Y的座標
 */
 private int mInitMotionY;
 
 /**
 * 上一次Y的座標
 */
 private int mLastMotionY;
 
 /**
 * 手指觸發滑動的臨界距離
 */
 private int mSlopTouch;
 
 /**
 * 觸發重新整理的臨界值
 */
 private int mRefreshHeight = 200;
 
 /**
 * 滑動時長
 */
 private int mDuring = 300;
 
 /**
 * 使用者重新整理監聽器
 */
 private OnRefreshListener mOnRefreshListener;
 
 /**
 * 重新整理文字提示
 */
 private TextView mRefreshTip;
 
 /**
 * 是否可拖拽,因為在重新整理頭自由滑動和重新整理狀態的時候,
 * 我們應該保持介面不被破壞
 */
 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,defStyleAttr);
 
 mContext = context;
 
 initView();
 }
 
 public RecyclerView getRecyclerView() {
 return mRecyclerView;
 }
 
 /**
 * 設定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);
 }
 
 /**
 * 設定自定義重新整理頭部
 * @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、新增內容RecyclerView */
 mRecyclerView = new RecyclerView(context);
 mRecyclerView.setTag(LIST_TAG);
 mListLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
 this.addView(mRecyclerView,mListLayoutParams);
 
 /** 3、一開始的時候要讓Header看不見,設定向上的負paddingTop */
 setPadding(0,-DensityUtil.dip2px(mContext,500),0);
 
 ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
 mSlopTouch = viewConfiguration.getScaledTouchSlop();
 
 setState(State.INIT);
 
 }
 
 /**
 * 設定狀態,每個狀態下,做不同的事情
 *
 * @param state 狀態
 */
 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;
 }
 
 /**
 * 處理初始化狀態方法
 */
 private void initState() {
 
 // 只有在初始狀態時,恢復成可拖拽
 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;
 
 /** 自由滾動狀態可以從兩個狀態進入:
 * 1、READY狀態。
 * 2、其他狀態。
 *
 * !滑動均需要平滑滑動
 * */
 if (mState == State.READY) {
 
 Log.e(TAG,"flingState: 從Ready狀態開始自由滑動");
 // 從準備狀態進入,重新整理頭滑到 200 的位置
 
 smoothScroll(getScrollY(),-mRefreshHeight);
 }
 else {
 
 Log.e(TAG,"flingState: 鬆手後,從其他狀態開始自由滑動");
 // 從重新整理狀態進入,重新整理頭直接回到最初預設的位置
 // 即: 滑出介面,ScrollY 變成 0
 smoothScroll(getScrollY(),0);
 }
 
 }
 
 /**
 * 光滑滾動
 * @param startPos 開始位置
 * @param targetPos 結束位置
 */
 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();
 }
 
 /**
 * 是否準備好觸發下拉的狀態了
 */
 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() {
 /** 滾動值 = 初始值 - 結尾值 */
 int scrollValue = (mInitMotionY - mLastMotionY) / 3;
 
 if (scrollValue > 0) {
 scrollTo(0,0);
 return;
 }
 
 if (Math.abs(scrollValue) > mRefreshHeight
 && mState == State.DRAGGING) {
 // 約定:如果偏移量超過 200(這個值,表示是否可以啟動重新整理的臨界值,可任意定),// 那麼狀態變成 State.READY
 Log.e(TAG,"pullScroll: 超過了觸發重新整理的臨界值");
 setState(State.READY);
 }
 
 scrollTo(0,scrollValue);
 }
 
 /**
 * 重新整理完成,需要呼叫方主動發起,才能完成將重新整理頭收起
 */
 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 {
 
 /**
 * 初始狀態
 */
 int INIT = 1;
 
 /**
 * 手指拖拽狀態
 */
 int DRAGGING = 2;
 
 /**
 * 就緒狀態,鬆開手指後,可以重新整理
 */
 int READY = 3;
 
 /**
 * 重新整理狀態,這個狀態下,使用者用於發起重新整理請求
 */
 int REFRESHING = 4;
 
 /**
 * 鬆開手指,頂部自然回彈的狀態,有兩種表現
 * 1、手指釋放時的高度大於重新整理頭的高度。
 * 2、手指釋放時的高度小於重新整理頭的高度。
 */
 int FLING = 5;
 }
 
 /**
 * 使用者重新整理狀態的操作
 */
 public interface OnRefreshListener {
 void onRefresh();
 }
 
}

實現的邏輯並不複雜,新手都能看懂,先理解了整個流程,程式碼就是水到渠成的事。
思想第一,最後程式碼。

完整DEMO直通車

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。