Android實現簡單的下拉重新整理控制元件
阿新 • • 發佈:2020-09-23
背景:列表控制元件在Android App開發中用到的場景很多。在以前我們用ListView,GradView,現在應該大多數開發者都已經在選擇使用RecyclerView了,谷歌給我們提供了這些方便的列表控制元件,我們可以很容易的使用它們。但是在實際的場景中,我們可能還想要更多的能力,比如最常見的列表下拉重新整理,上拉載入。上拉重新整理和下拉載入應該是列表的標配吧,基本上有列表的地方都要具體這個能力。雖然重新整理這個功能已經有各種各樣的第三方框架可以選擇,但是畢竟不是自己的嘛,今天我們就來實現一個自己的下拉重新整理控制元件,多動手才能更好的理解。
效果圖:
原理分析:
在coding之前,我們先分析一下原理,原理分析出來之後,我們才可以確定實現方案。
在列表上面有個重新整理頭,隨著手指向下拉,逐漸把頂部不可見的重新整理頭拉到螢幕中來,使用者能看到重新整理的狀態變化,達到下拉重新整理的目的。
通過分析,我們確定一種實現方案:我們自定義一個容器,容器裡面包含兩個部分。
1. 頂部重新整理頭。
2. 列表區域。
確定好佈局容器之後,我們來分析重新整理頭的幾種狀態
把下拉重新整理分為5中狀態,通過不同狀態間的切換實現下拉重新整理能力。
狀態間的流程圖如下:
整個下拉重新整理的流程就如圖中所示。
流程清楚了之後,接下來就是編寫程式碼實現了。
程式碼實現:
/** * @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直通車
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。