自定义控件从入门到轻生之---初尝禁果
所有blog局限于博主水平有限,很多不足之處大家可以指出共同探討進步。
尊重原創轉載請注明:From 倪大葉http://blog.csdn.net/renyi0109 侵權必究!雖然我不知道具體怎么究,但是看大家都這么寫我也就這么寫吧
這是個老生常談的技術話題了,不管你自己寫控件,還是拿網上的開源框架來改,很多自定義View開源框架為了符合大眾需求,都做得很通用,可定制性很強,其實大多數開源框架拿來并不能直接使用到項目中,都多多少少需要根據自己的實際需求做一點更改,這時候如果你對自定義控件方面知識是模糊的,在改的時候你肯定持續懵逼,這時候有兩種解決辦法,1 是告訴產品這個實現不了。 2 hiahiahia 看我blog呀。 其實吧我更趨向第一種,對產品說不,是每個程序員的責任義務,不然產品生物會越來越了不得,記得有一次周末一個人去清酒吧放松下聽聽音樂,結果旁邊坐了4只產品,一晚上都在聊他們那些自認為可以改變世界的idea,聊到最后貌似產品出來效果并不理想,結果怪程序員沒實現出他們想要的效果,真的 不是看我只有1個人他們有4個…我剛到公司的時候產品來和我提需求都是 “喂,這個需求給我實現了” 半年后 “哥,這個需求你看能實現嗎?”,但是碰到雌的產品還是小心點,有一次無意中看到她有個黑色的筆記本,在純黑色底色紙上用紅色筆寫了一連串名字,而我的赫然在第一位,瘆得我一個多月沒敢擋她的需求。。 扯遠了~ 拒絕歸拒絕,講道理的話該懂的還是得懂
說到自定義控件就先來看看事件分發
說到事件分發大家都知道有三個方法
dispatchTouchEvent : 負責事件的分發
onInterceptTouchEvent : 負責對事件傳遞的控制
onTouchEvent : 負責處理事件
先說說現象
這里有個注意點:getParent().requestDisallowInterceptTouchEvent(true) 調用父類的這個方法可以影響父類的正常攔截機制
這些結論大家應該已經爛熟于胸了吧,網上其實有很多各種各樣總結出來的事件分發的規律,有錯有對但五花八門,所以以死背規律口訣來處理實際應用中的事件分發明顯是不明智的,就拿上面的幾條常規結論來說,dispatchTouchEvent返回false就一定不會再接收事件了?onTouchEvent返回false代表不處理事件,但是事件一定就能拋給父View?真的是你想要就要,不想要就特么能隨便拋?今天就從源碼去推推這些規律:
透過現象看本質:
網上有種說法這三個方法是以隧道的方式逐步下發傳遞的,這種說法可對可不對,至于為什么下面再介紹。其實當我們的手按到屏幕上后,硬件上的某玩意兒會首先捕獲到這次點擊,然后經過一系列我也不清楚的處理傳到我們上層,然后就是WMS什么的再處理一次最后才傳到我們熟悉的地方Activity,事件傳遞的參數MotionEvent對象就是上面一堆流程封裝出的產物;這里有一點需要注意,用戶手指觸摸到屏幕是由硬件來捕獲的,既然是硬件那么Android碎片化這么嚴重不同的硬件會不會有什么區別呢?答案是肯定的,捕獲事件的反應速度就是在這里體現出來的,可能說反應速度大家不是很清晰,大家是否記得在打印move事件坐標的時候這個值有點怪異呢?特別是在你快速滑動的時候兩個相鄰的事件的坐標值卻相差很大,這就是因為設備的硬件在捕捉屏幕事件的時候會有一個捕捉反應時間,硬件越好這個時間越短,我們拿到的值也就越合理,我曾經碰到一款讀不出名字也不知道誕生于哪個年代的神機,反應時間達到了100ms左右,這直接導致我的下拉刷新控件下拉的時候和跳機械舞一樣。在我們自己處理一些滑動邏輯的時候需要注意一下, 接下來進到Activity的dispatchTouchEvent方法去看看
/*** Called to process touch screen events. You can override this to* intercept all touch screen events before they are dispatched to the* window. Be sure to call this implementation for touch screen events* that should be handled normally.** @param ev The touch screen event.** @return boolean Return true if this event was consumed.*/ public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}if (getWindow().superDispatchTouchEvent(ev)) {return true;}這里其實你可以理解成我們認知的事件分發的源頭,代碼很簡單,第一個if沒什么用,只是一個空實現不用關心,主要看下第二個if里的方法,這個是個什么東西呢?我們要知道每個Activity都有一個自己的window對象,這個玩意在Activity中有著很重的地位礙于不是本篇的重點,下篇再聊它,進入phone的實現類PhoneWindow找到superDispatchTouchEvent方法看看
@Overridepublic void superDispatchTouchEvent(MotionEvent event){return mDeco.superDispatchTouchEvent(event);}這方法比我兜還干凈,只有一句代碼,就是調用mDecor的superDispatchTouchEvent方法,mDecor就是DecorView,那么這時候事件就正式傳進了我們的View層里了。整個流程很簡單,Activity的dispatchTouchEven方法會先把事件傳遞給View層,如果DecorView的dispatchTouchEvent返回了true,那么代表要處理此次事件,如果返回false,那么此次事件將會交給Activity的onTonchEvent處理,并把處理結果作為dispatchTouchEvent的返回值返回,如果返回的是false,那么事件就不會再傳入Activity了,至于具體原因大家感興趣的話可以再往上去追溯源碼,我覺得并沒有什么用,就不多介紹了
好了現在現在進入正題,上面這些對你實際開發并沒有什么用,最多面試問到了可以多扯兩句,View系列子類的事件分發機制很簡單,因為View系列中沒有Intercept機制,所以你可以單純的理解成你要不要,你要我就傳,不要就算了,看看源碼很容易明白或者隨便找篇入門級blog都有介紹我們重點看看ViewGroup的dispatchTouchEvent 我只貼對我們有幫助的一些源碼,就不全部貼上來了看官們可以自己打開源碼結合起來一起看,我用的是21的源碼,大家最好用4.0以后的源碼,4.0以前在事件分發上的源碼和現在區別還是比較大的,當然這個改動是不影響事件分發機制的,只是可能你和我這里的源碼對不上而已
if (actionMasked == MotionEvent.ACTION_DOWN) {// Throw away all previous state when starting a new touch gesture.// The framework may have dropped the up or cancel event for the previous gesture// due to an app switch, ANR, or some other state change.cancelAndClearTouchTargets(ev);resetTouchState();}首選如果發現此次事件是Down事件,就會做一些clear和reseat操作,因為每次當down事件到來就代表結束了上一次系列事件,開始了新的一次事件
// Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was changed} else {intercepted = false;}} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.intercepted = true;}接下來這個幾句代碼就比較關鍵了,我們重點看看, 這里就是處理我們Intercept機制的地方了,首選如果是down或者mFirstTouchTarget != null就會進入Intercept邏輯,down很好理解,每當新的一輪事件到來,父View都一定會去走攔截邏輯來判斷是否攔截本次事件,如果不是down事件那么就會去看 mFirstTouchTarget這個玩意兒,這是一個什么東西呢?這個可以簡單的理解為封裝了響應了此次事件的子View信息,如果為空代表沒有子VIew響應或者處理這次事件,那么就沒必要走Intercept邏輯了,反之亦然。
接下來看下disallowIntercept這個變量,默認情況下這個值是false,所以必定會走onInterceptTouchEvent方法去看是否需要攔截事件,上面說過有一種特殊情況,當子View調用parent的requestDisallowInterceptTouchEvent這個方法時會影響到父View的攔截機制,也就是我們常用的getParent().requestDisallowInterceptTouchEvent(true)來阻止父VIew攔截事件, 這里能影響到攔截機制的就只有mGroupFlags這個變量了,那么調用這個方法肯定是對mGroupFlags這個值做了修改,我們進入requestDisallowInterceptTouchEvent方法看一下
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Pass it up to our parentif (mParent != null) {mParent.requestDisallowInterceptTouchEvent(disallowIntercept);}}我們知道 (mGroupFlags&FLAG_DISALLOW_INTERCEPT)!=0 為false的時候是會走攔截方法,反之亦然,第一個if只是做一個check,接下來看到了我們想看到的代碼,這里的確改變了mGroup的值以達到影響parent攔截的效果。是不是茅房頓開,但是有一個地方得注意一下這個方法只有在父View沒有攔截當前事件的情況下,影響下一次事件的攔截,就是說如果父View在down事件就已經攔截了,你別說調這個方法做什么什么,你連調這個方法的機會都沒有。所以不能影響down事件的攔截,也就是說down無論如何都會走onInterceptTouchEvent
上面說過onInterceptTouchEvent方法一旦返回true表示攔截本次事件,那么接下來的后續事件都將不會再調用onInterceptTouchEvent方法,自然也不會再分發給子View(下面dispatch的邏輯進入條件是Intercepted為false,這里如果不進入攔截代碼,intercepted恒為true,一會下面會介紹),而是直接都交給當前View的onTouchEvent方法來處理, 這就是事件分發中下傳中斷機制。 來看看這是為什么,首先我們知道要進入onIntercept機制的判斷邏輯的條件是 為down事件或者mFirstTouchTarget!=null ,那么我們試想下這種情況,父View在down事件的時候沒有攔截,然后接下來的move事件也傳遞給子View并且子View也需要這個事件,然后突然在某個move事件的時候父View決定攔截掉(中斷下發).
intercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was changed這時候Intercepted就為true,既然是一旦返回true攔截,以后都不會再走onInterceptTouchEvent方法,意味著攔截的邏輯都不會再走,前面的事件我們沒有攔截,所以這時mFirstTouchTarget一定是有值的,如果以后都不會再走onIntercept邏輯,這個mFirstTouchTarget在后面的代碼中一定會被置空,帶著這個想法我們找到在方法最后有一段代碼:
if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary view.handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else {// Dispatch to touch targets, excluding the new touch target if we already// dispatched to it. Cancel touch targets if necessary.TouchTarget predecessor = null;TouchTarget target = mFirstTouchTarget;while (target != null) {final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}if (cancelChild) {if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}predecessor = target;target = next;}}因為mFirstTouchTarget不為空,這里會走else,這里可能不是很明顯能看出mFirstTouchTarget被置為null,看這里之前我們得先知道,當一個事件到來以后,在這個范圍內的所有子view都有資格參與事件分發機制中,代表能接收事件的子View不止一個,那么每個View都有一個TouchTarget對應,TouchTarget是一個ViewGroup的內部類,里面有一個next本類變量,這個變量的作用就是指向下一個TouchTarget,這樣所有的view就以一個鏈表結構存儲,mFirstTouchTarget其實就是這個鏈表的頭,頭Target所對應的View就是實際能響應事件的子View(Handler里的Message也是以這種鏈表結構存儲的,感興趣可以自己去看看)
再來看代碼 先把mFirstTouchTarget賦給target,接著進入while循環,next就是當前Target指向的下一個Target,cancelChild在攔截的情況下恒為true就會進入的if循環體,可以看到執行if體就會continue,所以predecessor一直等于空,每次都會把mFirstTouchTarget指向下一個View,然后再將原本的頭target調用recycle進行清除, 這里其實就是做一個迭代,將所有鏈表里的TouchTarget都調用 recycle方法做清除,這樣當target==null跳出循環體的時候,所有的TouchTarget都被清除了,mFirstTouchTarget也為null了, 為什么onInterceptTouchEvnet方法一旦返回true就會直接中斷下發源頭就在這里。
接下來才到事件的分發:
注意看最后一個if,這個if里面就是處理事件的分發, canceled是看這個事件如果為cancel那么就不會再做分發,intercepted就是上面攔截邏輯以后的結果,為false不攔截才會走分發。 這個事件分發的代碼較多,無非就是根據觸摸點的坐標,計算這個點在哪些子類上,哪些有資格參與這次事件的消費云云,我們看下具體調用子類的dispatchTouchEvent代碼
if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//.............resetCancelNextUpFlag(child);if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// Child wants to receive touch within its bounds.mLastTouchDownTime = ev.getDownTime();if (preorderedList != null) {// childIndex points into presorted list, find original indexfor (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {mLastTouchDownIndex = j;break;}}} else {mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;}注意看這里外層if,是要down或者action_pointer_down(第二個點down),action_hover_move(這個好像是兼容鼠標的的類型吧,記住不是傳統的move事件)才會進入這個分發,也就是說這里只是down事件分發的地方,接下來的move事件并不走這里分發,至于有什么用下面再說這里大家先mark一下
dispatchTouchEvent方法,自己點進去看下一目了然,如果說子類的dispatchTouchEvent返回true代表對這個事件感興趣,就會把這個View加入到當前TouchTarget鏈表中,addTouchTarget方法進入看一下
private TouchTarget addTouchTarget(View child, int pointerIdBits) {TouchTarget target = TouchTarget.obtain(child, pointerIdBits);target.next = mFirstTouchTarget;mFirstTouchTarget = target;return target;}根據這個childView 創建一個 TroucTarget, 然后將該target插入到當前TouchTarget鏈表的頭部,并把mFirstTouchTarget重新指向當前的鏈表頭。 那么回到最開始的時候,如果不為down事件就要這個mFirstTouchTarget來判斷,如果不為null代表有子類對這個事件進行了接收感興趣,那么接下來后續事件到來都會走上訴邏輯,直到父View攔截掉事件,如果沒有一個子類對事件感興趣,那么mFirstTouchTarget就會為null,那么再走到Intercept機制的時候就不會進入,intercepted就恒為true,也不會再走分發的邏輯,這也就是解釋了為什么dispatchTouchEvent返回false以后,事件不會再傳遞下來。
回到最開始我提的問題:dispatchTouchEvent返回false就一定不會再接收事件了?onTouchEvent返回false代表不處理事件,但是事件一定就能拋給父View? 上面mark的時候說過 最開始的分析的分發邏輯只針對down事件,換個意思也就是說如果down事件子View處理了返回true,那么該View就會被加到TouchTarget隊列中,就算接下來的move事件返回false不處理也不會清除這個TouchTarget,因為從上面分析知道清除TouchTarget只會在父View攔截事件以后才會去做,事件依然會被傳到子View,所以”規律”中所說的比如子View的onTouchEvent返回false事件會被丟回給父View和dispatchTouchEvent返回false代表不需要事件不會再接收事件是只針對down事件而言的,切記! 至于move事件是在哪里分發的留給大家自己去源碼里找了。 這里感覺有點繞我白話一點總結一下吧:
當down事件分發的時候,父View傳遞給子View,如果子View的dispatchTouchEvent和onTouchEvent任何一個返回false都會將該事件拋給父View,并且接下來的事件都不會收到,如果down事件子View處理了,那么后續事件不管你返回什么要不要處理都會依然傳給子View,這時候除非父View攔截事件,不然父View的onTouchEvent方法永遠都不會拿到事件了,就記住父View第一次問你(down事件)要不要,不要就不會再給你了,要你特么就給我拿好了,別像穿過的破鞋一樣不要了還給老子就算還了老子也不要,除非我自己愿意穿破鞋搶過來。
這就是整個Android中的事件分發的邏輯,其實并沒有多復雜。 上面說了這么多都是圍繞著分發,攔截展開的,那么我們是不是該進onInterceptTouchEvent方法里去看看呢? 說走就走
這方法光看注釋就知道這一定是一個不遜色于dispatchTouchEvent的吊炸天方法,不如各位先去倒杯水潤潤喉我們再接著來?恩,現在我們看看onInterceptTouchEvent的實現,在你往下看的時候請確保你剛才口中的水已經咽下
public boolean onInterceptTouchEvent(MotionEvent ev) {return false;}當當當當。。 你們沒看錯,這個在事件分發體系中排行老二的重要方法,只有一句代碼, renturn false;
誒,誒,誒。。摸刀的手給我停下… 來來來 喝杯可樂殺殺精
VIewGroup對dispatchTouchEvent的實現這么多,為什么onIntercept方法就只有一句代碼呢?我也曾一度認為這難道是GOOgle工程師加班寫出來的偷懶代碼?說到加班,我覺得加班調調明顯簡單的BUG,UI,和后臺聯調啥的都是OK的,但是比較重的修改就別了,我接手過幾個項目看到一些反人類的代碼我都會去問當事人當時是以吃了什么味道屎的情況下寫出來的,一般回答都是,哦這是我上次加班到凌晨,眼睛都快閉上了隨便寫的,其實如果一天你能高度集中精神工作思考寫代碼7個小時就已經很厲害了,別用上班時長來衡量一個研發的工作態度和能力! 說回正題,這個其實可以從Android高定制性來解釋,ViewGroup為所有容器View的基類,為了子類的定制性足夠靈活,所以基類統一不攔截事件,也不做其他邏輯處理,讓子類有足夠的權限去處理自己的事件邏輯,這點和dispatchTouchEvent不是很一樣,上面我說過,網上有種說法,這3個事件分發方式是以隧道的形式逐下傳遞,為什么說可對可不對呢,因為 其實在事件分發體系中,傳遞的唯一方法入口只有dispatchTouchEvent,至于onIntercept和onTouchEvent都是由dispatchTouchEvent來負責調用的, 可以這樣理解Google給你實現了一套事件分發的框架,這個框架的具體體現就是dispatchTouchEvent,dispatchTouchEvent一人承擔了Android事件機制的絕大數任務,它是控制中樞,而onIntercept和onTouchEvent只是在這個分發機制中以類似開關角色存在,使它們盡最大限度的提供給外部重寫以控制事件分發的走向來實現多樣化控件。可以把整個事件分發想象成一個電路圖,這個電路圖已經是畫好了的,但是電流并非只有一條電路可走,可以用開關來控制電流的走向。 官方的原生控件ListView,ScrollView等都是基于這種思想實現的產物,大家感興趣可以去看源碼求證下, 所以你可能會看見很多開源框架都會重寫onIntercept和onTouchEvent方法來實現自己的邏輯,但是你看到過復寫dispatchTouchEvent的嗎?如果有 不是大神就是大嬸, 各位別輕易去挑戰反人類行為。
就目前來看,好像只差onTouchEvent方法沒有講到,這個方法是用來處理事件的,說白了就是怎么去用事件來實現自己想要的東西,就像我上面說的每個View都有不同的實現,就拿VIewGroup來說,你會看見并沒有復寫onTouchEvent方法,因為它只是一個容器View基類,并不需要處理什么邏輯,如果他的子類對事件都不做處理,丟給了它,它沒有自己的實現,會把這個事件丟給parent也就是View的onTouchEvent處理,具體代碼就不分析了。
總結:
Android整個事件分發機制其實就是用責任鏈模式思想來設計的, 事件分發機制其實并沒有大家以為的這么復雜,其實道理很簡單,復雜其實是復雜在各種View實現了各種自己的”開關”邏輯,兩個View的邏輯可能又產生了沖突(滑動沖突),事件分發的傳遞規律大家應該很清楚了,以后也不用在網上去找那些各種結論然后跟著照搬,懂了原理不管什么需求情況都萬變不離其中,大家可以把這篇文章當個參考,然后自己去看源碼,從源碼中去證實這些東西印象會很深刻,但是我個人就以我的感悟給點意見, 看源碼千萬別太較真,碰到實在看不懂的又不影響繼續往下看的代碼時,不要去花費時間研究,意義并不大,其實這些源代碼都是老外寫的,先拋開水平的差距,光是中西思維邏輯的不同都會導致很多代碼你理解不了他這么設計實現的意義在哪,就像你覺得老外是逗逼,人家覺得你是傻逼一樣的道理,不用去深究,大概能懂整體就好,這樣也不是說你一目十行的去看,你要知道可能在一些微不足道的地方就隱藏著一句非常優美的代碼,或者藏著一個鮮為人知的知識點,這些可能對你有致命的幫助,甚至能解開那些困擾你很久的疑惑。
寫到這里的時候,我讀初中的侄兒拿了道題過來問我,當我看到這題的時候。。。。。。。。。
轉頭再看看他希翼的眼神,我陷入了沉思,深層度的沉思。。
總結
以上是生活随笔為你收集整理的自定义控件从入门到轻生之---初尝禁果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DNA双螺旋结构是大自然长期进化的结果
- 下一篇: 解决Mybatis报错问题:org.ap