1. 程式人生 > >View的學習筆記(三)_自己造輪子_一個帶header重新整理頭和footer載入腳的

View的學習筆記(三)_自己造輪子_一個帶header重新整理頭和footer載入腳的

帶重新整理指示item的RecyclerView

實現效果

重新整理前
下拉重新整理
上拉載入
使用方法
可以指定控制元件大小,預設的RecyclerView會填充指定的大小
自定義屬性就三條

		<!--可以指定列表控制元件為ListView,賦值1-->
		<attr name="view_type" format="integer"/>
		<!--指定自己的header/footer佈局-->
        <attr name
="header_layout" format="reference"/>
<attr name="footer_layout" format="reference"/> <!--可以自行控制需要顯示header/還是footer--> <attr name="header_visible" format="boolean"/> <attr name="footer_visible" format="boolean"/>

資料檔案
專案目錄
專案結構分三部分,自己引用即可
專案地址:

github連線點這裡

專案結構

造輪子的經驗總結

因為控制元件結構簡單,子View數量也比較小,因此初始化/測量/擺放都很簡單
難點在於滑動事件的處理

設計思路

在學習筆記裡寫到

觸控事件的處理,首先判斷需要攔截的情況,在onInterceptTouchEvent(MotionEvent ev)中對應情況下返回true
然後在onTounch()中處理具體的滑動和手指擡起事件

但是自己去寫的時候不知道如何下手,經常寫的時候信心滿滿,測試的時候心態就崩了.
後來自己整理了下思路
1.響應式設計.不用考慮所有情況,只對符合我要求的情況,進行處理
2.面向過程式設計.假設要觸發一個事件,我們從down手指按下事件分發開始,到move滑動處理,最後up手指彈起處理,一步一步考慮
對於觸控事件比較複雜,而需要的效果比較簡單,可以考慮響應式思路,比如本專案,如果只需要處理下拉的彈出,其他都不管,那麼只需要監聽view滑動到底端事件分發處理,滑動彈出footer即可
但是如果我們需要考慮的情況多了,就需要從使用者的角度,來考慮,使用者的上拉或者下拉操作目的是什麼
下面開始記錄我的設計過程,這只是最終的思路,實際上,在這個思路之前,我已經更換了兩種思路,都不太理想

事件分發處理

因為我們的列表zview自身是可以滑動的,所以如果不對事件分發進行處理,介面效果就是一個單純的滑動View,header/fpooter不會彈出
因此我們需要處理事件分發,什麼樣的情況下,需要把螢幕滑動事件分配給ViewGroup整體滑動
header的彈出/取消
header的作用是提示使用者重新整理.何時header該彈出,何時header該消失呢
彈出
當列表View滑動到頂端的時候,如果使用者還在向上滑動,我們就認為是想重新整理,此時彈出header,
取消
當header已經彈出,使用者向下拉的時候,是想要取消重新整理,我們就取消header
另外,當後臺重新整理邏輯處理完以後,也需要我們取消header

footer的原理類似
列表View滑動到頂端/底端的監聽如何開始呢
我是下面這樣設計的

				int distance = (int) (lastY - ev.getRawY());
                if (Math.abs(distance) > mSlop) {
                    if (contentView instanceof RecyclerView) {
                        LinearLayoutManager manager = (LinearLayoutManager) ((RecyclerView) contentView).getLayoutManager();
                        if (manager != null) {
//                       判斷滑動到頂端,開始下拉
                            if (headerVisible&&manager.findFirstCompletelyVisibleItemPosition() == 0 && distance < 0) {
                                return true;
                            }
//                     判斷滑動到底端,開始上拉
                            if (footerVisible&&(manager.findLastCompletelyVisibleItemPosition() + 1) == Objects.requireNonNull(((RecyclerView) contentView).getAdapter()).getItemCount() && distance > 0) {
                                return true;
                            }
//                       只要當前顯示了header/footer,就攔截事件
                            if (headerRefreshCompleted&&headerVisible){
                                return true;
                            }
                            if (footerRefreshCompleted&&footerVisible){
                                return true;
                            }
                        }
                    }
				}

首先判斷是否在頂端,根據完全露出的item是否是第一條決定,但是view載入的時候預設顯示的就是第一條.所以,我們需要排除預設顯示的情況,預設顯示的時候,如果滑動方向向下,那自然就是view自己的滑動,所以加上方向的限制.就能把滑動到頂端/底端跟正常顯示到頂端/底端的事件區分開
然後,footerRefreshCompleted || headerRefreshCompleted是什麼意思呢
想象一下,如果header正常顯示了,使用者希望取消header這個時候,開始下拉(distance>0),這個時候也需要處理,因此我們定義了一個標誌值,當header顯示的時候,headerRefreshCompleted為true/footer顯示的時候footerRefreshCompleted 為true.只要這兩個標誌有一個為真,就繼續分發事件
這樣事件分發就搞定了

然後就是難點,滑動事件處理
我們按照滑動距離來分類,我們用Scroller類來輔助滑動
scroller類的實現如下

 @Override
 ...
 //初始化
 mScroller = new Scroller(context);
        autoScrollRange = 0.6;
        ...
        //實現自動滑動的方法(格式可以是固定的)
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
    //使用,實現滑動返回原位
     mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
      invalidate();

在onTouchEvent()中首先支援滑動

 public boolean onTouchEvent(MotionEvent event) {
        float distance = lastY - event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                lastY = event.getRawY();
//                預設可滑動,在move中處理檢視內滑動事件,在up中處理檢視外滑動事件
                scrollBy(0, (int) distance);
...
}

然後還是在case MotionEvent.ACTION_MOVE中,當檢視在我們的viewGroup範圍內滑動時
如果滑動方向向下,那麼分兩種情況考慮,一個是使用者想要下拉顯示header,另一種是想要取消footer
我們開始新增效果,如果滑動的距離超過dheader/footer的高度的一定範圍,那麼久呼叫Scroller類,來輔助滑動,顯示/隱藏完整的header/footer
如果滑動方向向上,那麼也是分兩種情況,一個是使用者想要上拉顯示footer,另一種是想要取消header

//                在檢視內滑動處理
                if (getScrollY() >= -headerHeight && getScrollY() <= footerHeight) {
//                向上滑動,a想要上拉顯示footer,b想要上拉取消header
                    if (distance > 0) {
//                        a要上拉顯示footer,超過角標的autoScrollRange就自動下拉顯示
                        if (!footerRefreshCompleted && getScrollY() >= footerHeight * autoScrollRange) {
                            Log.i(TAG, "onTouchEvent: 自動上拉");
                            mScroller.startScroll(getScrollX(), getScrollY(), 0, footerHeight - getScrollY());
                            footerRefreshCompleted = true;
                            if (mListener != null) {
                                mListener.footerRefreshStart(footer, contentView);
                            } else {
                                Log.e(TAG, "onTouchEvent: mListener=null");
                            }
                        }
//                      b想要上拉取消header,超過角標的autoScrollRange就自動下拉顯示
                        if (headerRefreshCompleted && getScrollY() < 0) {
                            Log.i(TAG, "onTouchEvent: 取消下拉");
                            mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY(), 1000);
                            headerRefreshCompleted = false;
                            if (mListener != null) {
                                mListener.headerRefreshCancel();
                            } else {
                                Log.e(TAG, "onTouchEvent: mListener=null");
                            }
                        }
                    }
                    //                    向下滑動,a想要下拉顯示header,b想要下拉取消footer
                    if (distance < 0) {

//                  a判定下拉顯示header,超過角標的autoScrollRange就自動下拉顯示
                        if (!headerRefreshCompleted && getScrollY() <= -headerHeight * autoScrollRange) {
                            Log.i(TAG, "onTouchEvent: 自動下拉");
                            mScroller.startScroll(getScrollX(), getScrollY(), 0, -headerHeight - getScrollY());
                            headerRefreshCompleted = true;
                            headerRefreshStart();
                        }

//                  b判定想要上拉,取消上拉footer,超過角標的autoScrollRange就自動取消下拉footer
                        if (footerRefreshCompleted && getScrollY() > 0) {
                            Log.i(TAG, "onTouchEvent: 取消上拉");
                            mScroller.startScroll(getScrollX(), getScrollY(), 0, -footerHeight, 1000);
                            footerRefreshCompleted = false;
                            if (mListener != null) {
                                mListener.footerRefreshCancel();
                            } else {
                                Log.e(TAG, "onTouchEvent: mListener=null");
                            }
                        }
                    }
                }
                invalidate();

記得在處理完的最後,新增invalidate(),Scroller類才生效
這樣處理完了以後,已經可以自己彈出/隱藏header/footer了,但是還有兩點不足的地方需要改進
1當滑動的距離超過自動顯示/隱藏範圍時,自動顯示/隱藏,那麼當沒有超過的時候,顯示的就是不完整的header/footer怎麼辦
2當滑動的範圍超過了我們定義的GroupView的範圍時,會在header/footer的外圍露出大片的空白
解決辦法,在手指擡起事件中,如果滑動的範圍超過了我們定義的GroupView的範圍,那麼久預設顯示邊界為header頂端或者footer底端,呼叫Scroller滾動即可;滑動的距離沒有超過自動顯示/隱藏範圍時,我們直接呼叫Scroller類隱藏即可

case MotionEvent.ACTION_UP:
//                如果移動範圍超過檢視頂端範圍,那麼在手指擡起時,返回到檢視最頂端
                if (getScrollY() < -headerHeight) {
                    mScroller.startScroll(getScrollX(), getScrollY(), 0, -headerHeight - getScrollY(), 500);
                }
//                如果移動範圍超過檢視底端範圍,那麼在手指擡起時,返回到檢視最底端
                if (getScrollY() > footerHeight) {
                    mScroller.startScroll(getScrollX(), getScrollY(), 0, footerHeight - getScrollY());
                }
//                如果在檢視範圍內,手指擡起時,沒有觸發自動顯示header/footer,就自動隱藏
                if (getScrollY() >= -headerHeight && getScrollY() <= footerHeight) {
//                    自動隱藏header
                    if (!headerRefreshCompleted && getScrollY() > -headerHeight * autoScrollRange) {
                        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
                    }
//                    自動隱藏footer
                    if (!footerRefreshCompleted && getScrollY() < footerHeight * autoScrollRange) {
                        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
                    }
                }
                invalidate();
                break;

##新增邏輯處理完後,取消header/footer的方法

 public void onHeaderRefreshCompleted() {
        headerRefreshCompleted = false;
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
        invalidate();
    }

    public void onFooterRefreshCompleted() {
        footerRefreshCompleted = false;
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
        invalidate();
    }