自定义控件从入门到轻生之---来个结晶
注:所有blog局限于博主水平有限,很多不足之處大家可以指出共同探討進步。
尊重原創轉載請注明:From 倪大葉(http://blog.csdn.net/renyi0109) 侵權必究!
一般自定義控件分成兩大類:
1. 測試()將幾個已有的控件“拼接”成一個特有的控件,一般用到的知識就是上兩篇blog所寫的事件分發,測量等等。。 中心思想就是讓幾個獨立的已有控件根據自己特有的屬性加上外部控制協調的一起工作
2. 上篇我們最后一帶而過的onDraw方法的豐富實現,也就是自己去畫出一個特有的控件,這個的重點主要就是畫的工具的學習,path,paint,layer等等類的使用以及他們提供的海量api,不僅如此一些較復雜的控件繪制還需要扎實的數學基礎,這個基礎可不是指初中高中那些小混混,而是在大學以高數帶頭,手下集結了離散,線數等一幫兇神惡煞小弟的大學第一幫派, 該幫派在大學為非作歹,奸淫擄掠,無惡不作,人神共憤,多少青年俊杰,黃花閨女折在這幫孫子手上。 鑒于我這幾年也是依附在寢室一個天生神力,戰無不勝的大神麾下才在這幫惡徒手下勉強偷生,所以這方面就不多討論。但是也別怕 還是有很多這類需求用一點簡單的數學知識加邏輯就能畫出來,就算碰到復雜的需求還可以去github上找類似的,如果找不到更不用怕了,直接告訴產品實現不了,所以說問題嘛總有解決辦法
今天我就結合前兩篇的知識寫個通用的下拉刷新控件demo,這demo主要是講解為主,臨時花了點時間做的,很多東西沒有考慮進去,BUG什么的也沒有調,所以并不適合拿來直接用到項目中,主要是學習一些用法而已。過程中主要講一些關鍵點,效果圖什么的就不貼了,大家可以到我github(https://github.com/renyindy/SupperRefreshVIew)上去下然后跑起來結合本篇blog來看(千萬別直接用到項目中,如果出了問題這鍋老子可不背,切記!) 廢話不多說 上demo
先分析下結構應該是什么樣的,首先不能像傳統ListView加頭部的方法去做這樣違背了通用原則要知道這是listView獨有的接口,雖然其他View沒有但是我們可以模仿這種做法去給所有需要刷新的View加個”頭部”以及”底部”, 試想一下 我們有這么一個容器,中間是放我們需要刷新的View,上下分別是刷新頭部和底部那問題不就解決了嗎? 這結構腦子隨便一想就知道用Linearlayout再合適不過
既然是通用我們就不能只在內容上允許隨意更改;刷新,加載更多的樣式也應該是可替換的,所以我們先設計一下頭部和底部的通用接口:
public abstract class UpdateSuperView extends LinearLayout {public static final int STATE_NORMAL = 0; //常規狀態public static final int STATE_ALREADY = 1; //已可觸發刷新public static final int STATE_REFRESHING = 2; //正在刷新public static final int STATE_LOADING = 3; //正在加載protected RefreshAndLoadListener mRefreshAndLoadListener;public UpdateSuperView(Context context) {super(context);}public UpdateSuperView(Context context, AttributeSet attrs) {super(context, attrs);}/*** 用來更改頭部或底部的可見高度* @param value*/public abstract void updateHeight(int value);/*** 重置頭部或底部的可見高度,這個取決于當前state以決定重置為什么高度,不一定是不可見*/public abstract void reseatHeight();/*** 設置當前狀態* @param state*/public abstract void setState(int state);/*** 獲取當前狀態* @return*/public abstract int getState();/*** 獲取當前頭部或底部的可見高度* @return*/public abstract int getVisableHeight();/*** 上拉加載和下拉刷新觸發回調監聽*/public interface RefreshAndLoadListener{void onRefresh();void onLoadMore();}protected void setRefeshAndLoarListener(RefreshAndLoadListener refeshAndLoarListener){this.mRefreshAndLoadListener = refeshAndLoarListener;}}頭部和底部有了,再來弄個內容,這里我們就不能讓需要刷新的View去繼承一個接口,然后再面向接口設計了,因為你不可能讓別人還要讓想刷新的View都去繼承這個接口接著還得自己實現這個接口中的方法吧??這不是拿人作寶搞嗎。。 我們設計應該是這樣,用的人只用傳一個想刷新的View進來,其他就不用管了。 既然不能面向接口設計,那么我們就用一個Holder來包裹一下需要刷新的View,然后面向這個Holder編程就OK了,看一下Holder設計:
public class RefreshHolder implements AbsListView.OnScrollListener {//需要刷新的Viewprivate View mChild;//........... public void setContentView(View view) {this.mChild = view;}//.........../*** 達到頂部監聽** @return*/public boolean isTop() {if (mChild instanceof AbsListView) { //ListView到達頂部監聽AbsListView absListView = (AbsListView) mChild;return !canScrollVertically(mChild, -1)|| absListView.getChildCount() > 0&& (absListView.getFirstVisiblePosition() == 0 && absListView.getChildAt(0).getTop() == 0);} else if (mChild instanceof ScrollView) { //ScrollView達到頂部監聽ScrollView scrollView = (ScrollView) mChild;return scrollView.getScrollY() == 0;} else {return canScrollVertically(mChild, -1) || mChild.getScrollY() > 0;}}/*** 到達底部監聽** @return*/public boolean isBottom() {if (mChild instanceof AbsListView) {//ListView到達底部監聽AbsListView absListView = (AbsListView) mChild;return !canScrollVertically(mChild, 1);} else if (mChild instanceof ScrollView) { //ScrollView頂部底部監聽ScrollView scrollView = (ScrollView) mChild;View childView = scrollView.getChildAt(0);if (childView != null) {return !canScrollVertically(mChild, 1)|| childView.getMeasuredHeight() <= scrollView.getHeight() + scrollView.getScrollY();}}return false;}//..............}這個Holder最關鍵的就是isTop方法和isBottom方法,我們控件刷新的設計思想就是如果達到頂部繼續下拉或者到底部繼續上拉就中斷事件下發由我們的刷新控件接管事件,并進行頭部或者底部的相應處理, 如果不滿足這兩個控件就將事件傳遞下去,讓內在的View自己去處理。 這里我們只做了ListView和ScrollView的頂部底部監聽判斷,如果想兼容其他View,比如RecyclerView甚至自定義View就直接在這方面里面添加條件判斷即可。
現在我們來看看這個刷新ViewGroup怎么設計,首先先把HeaderView,ContentView(需要刷新的內容View),FooterView依次加入我們的刷新容器中,因為我們的刷新GroupView是一個豎直的Linearlayout,我們只需要將hearView的高度設置為0,ContentView設置為match_parent,FooterView隨意給需要的高度就行,這樣初始化顯示就只有一個ContentView,當我們滿足下拉條件的時候依次增加HeaderView的高度,就能讓HeadView顯示出來達到下拉刷新的效果,而當我們上拉條件滿足的時候,我們讓FooterView和ContentView一起像上做偏移就能讓FooterView顯示出來, 可能有人會問了干嘛不用和頭部一樣的方式先把高度設置為0然后逐漸增大呢? 這么問我只能說你連LinearLayout都沒想明白,稍稍動下腦如果用增加高度的方法,那么Linearlayout總共就這么大footerView占用了高度那ContentView是否會被擠壓變形呢?如果你又問頭部怎么不會被擠壓。。。。 那么這個話題我覺得就聊不下去了 。
雖然用偏移的方式是可行的但是我們得改一點Linearlayout的onMeasure邏輯,因為LinearLayout的測量規則是不包含超出顯示區域的子View的寬高的,所以我們這里要讓Linearlayout根據子View總共有多高我就要設置LinearLayout多高
這里我就簡單的依次累加了,并沒有考慮margin進去,margin對這個控件有點雞肋,是不建議給里面的ContentView設置margin的,會影響刷新效果很難看, 萬事具備就差最關鍵的事件分發模塊了:
public class SuperRefreshView extends LinearLayout {@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:isIntercept = false;mLastY = ev.getRawY();mLastX = ev.getRawX();break;case MotionEvent.ACTION_MOVE:mLastMoveEvent = ev;int deltaY = (int) (ev.getRawY() - mLastY);int deltaX = (int) (ev.getRawX() - mLastX);//headView 處理滑動if ((mHeadView.getVisableHeight() > 0 || (deltaY > 0 && mRefreshHolder.isTop())) && isRefresh) {if (!isShowHeader) {isShowHeader = true;}sendCancelEvent();updateHeaderHeight(deltaY);} else if ((mRefreshHolder.getOffsetY() < 0 || deltaY < 0 && mRefreshHolder.isBottom()) && isLoadMore) { //footerView 處理滑動if (!isShowFooter) {isShowFooter = true;}sendCancelEvent();updateFooterHeight(deltaY / 2);}if (mHeadView.getVisableHeight() <= 0 && isShowHeader && deltaY < 0) {isShowHeader = false;sendDownEvent();} else if (mRefreshHolder.getOffsetY() >= 0 && isShowFooter && deltaY > 0) {isShowFooter = false;sendDownEvent();}mLastY = ev.getRawY();break;case MotionEvent.ACTION_UP:if (mHeadView.getVisableHeight() > 0) {reseatHeaderHeight();}if (mRefreshHolder.getOffsetY() != 0) {reseatFooterHeight();}break;}return super.dispatchTouchEvent(ev);}邏輯很簡單就不細講了,但是有個關鍵的地方大家應該注意到有兩個方法 sendCancelEent()和sendDownEvent(), 這兩個方法是干嘛的先暫且不說,我們來看看整個刷新控件中唯一的難點,試想一種情況: 當一開始判定沒有滿足刷新規則,直接將事件分發下去讓子View做處理,這時候達到刷新條件父View需要接管事件自己處理而不讓子View處理,好了,肯定有人會說簡單啊攔截掉不就行了么,最開始我也這么天真過,攔唄~~, OK問題解決了提交代碼上傳測試
轉天測試過來:XX你這刷新有問題啊! “what? 我的代碼有問題?你是認真的嗎?”,”真的 你看我拉到刷新這里以后不松手,再慢慢放回去,然后繼續往下移動,里面的View不能跟著滑動了”,仿佛一道驚雷打在我天靈蓋上, 對啊 我攔截了事件傳不下去了,這時候只要不放手里面的內容View就再也不能接收到事件,所以當再滑回頭部繼續滑動的時候,按道理應該是子View接管事件做自己的滑動處理,可是現在不行了!怎么辦 當時這個問題也的確難住了我,攔截是中斷形式的,一次攔截終生受用,最后為了趕著上線當這種情況下我根據父View拿到的滑動事件信息去手動的調ListView(當時內部是listView)的滑動方法強行滑動,效果爛不說,還要處理一堆雜事,比如放手后做根據放手時的加速度去模擬listView做慣性滑動等等。。。 事后我決定從根源上找到解決辦法而不是這么low的外部輔助方法,我再一次看了事件方面的源碼想從里面找到思路,可是中斷攔截貌似是鐵律除非你去自己寫分發邏輯,這種傻逼想法就不說了。。。 當我卡在傳統思維死胡同中的時候突然驚醒,我既然能外部模擬ListView滑動去處理,為什么我不能模擬事件給子View呢?為什么非要死板的認為事件中斷下發了一定要用戶重新按下才能傳遞下去,一旦想通這一點這問題就迎刃而解了,再看上面的代碼當hearView處理滑動的時候我不是攔截事件而是調用了sendCancelEent()方法,我們進去看一下:
接下來子View不會再接收到事件當然就不會再有響應,當發生到上訴所說的情況只需同樣的思想再模擬一個down事件傳遞給子View,那么就可以完美的繞開中斷機制實現事件分發橋接
/*** 模擬 down事件 用于分發到 內部子View*/private void sendDownEvent() {final MotionEvent last = mLastMoveEvent;if (last == null)return;MotionEvent e = MotionEvent.obtain(last.getDownTime(),last.getEventTime(), MotionEvent.ACTION_DOWN, last.getX(),last.getY(), last.getMetaState());dispatchTouchEventSupper(e);}這小玩意兒差不多就講完了,這東西大家自己強化強化改吧改吧完全可以用到自己項目中,當然網上已經有很多成熟強大的下拉刷新庫,我喜歡自己寫純屬因為改起來快,想怎么弄怎么弄,碰到BUG定位也很快,也比較輕。
總結
以上是生活随笔為你收集整理的自定义控件从入门到轻生之---来个结晶的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IVM格式(互动媒体)
- 下一篇: 详谈为什么互联网公司禁用外键约束