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"/>
資料檔案
專案目錄
專案結構分三部分,自己引用即可
專案地址:
造輪子的經驗總結
因為控制元件結構簡單,子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();
}