close

作者:唐子玄鏈接:https://juejin.cn/post/6968237652017414151

App 界面中,有一些控件尺寸很小,不容易點到。我總是通過加 padding 來解決這個問題。這樣容易牽一髮動全身,特別對於複雜界面,往往改變了一個控件的大小,其他控件的位置也隨之而動。有沒有更好的辦法解決辦法?在閱讀觸摸事件源碼中,無意間發現了一種更解耦的方式。

引子

觸摸事件源碼分析可以點擊這裡, 現援引結論如下:

Activity接收到觸摸事件後,會傳遞給PhoneWindow,再傳遞給DecorView,由DecorView調用ViewGroup.dispatchTouchEvent()自頂向下分發ACTION_DOWN觸摸事件。

ACTION_DOWN事件通過ViewGroup.dispatchTouchEvent()從DecorView經過若干個ViewGroup層層傳遞下去,最終到達View。View.dispatchTouchEvent()被調用。

View.dispatchTouchEvent()是傳遞事件的終點,消費事件的起點。它會調用onTouchEvent()或OnTouchListener.onTouch()來消費事件。

每個層次都可以通過在onTouchEvent()或OnTouchListener.onTouch()返回true,來告訴自己的父控件觸摸事件被消費。只有當下層控件不消費觸摸事件時,其父控件才有機會自己消費。

觸摸事件的傳遞是從根視圖自頂向下「遞」的過程,觸摸事件的消費是自下而上「歸」的過程。

觸摸代理

View 對觸摸事件的消費邏輯都集中在onTouchEvent()中,它是觸摸事件傳遞的終點,消費的起點。但其中居然將觸摸事件又傳遞給了別人:

1publicclassView{ 2//觸摸代理 3privateTouchDelegatemTouchDelegate=null; 4publicbooleanonTouchEvent(MotionEventevent){ 5finalfloatx=event.getX(); 6finalfloaty=event.getY(); 7finalintviewFlags=mViewFlags; 8finalintaction=event.getAction(); 9...10//將觸摸事件分發給觸摸代理11if(mTouchDelegate!=null){12if(mTouchDelegate.onTouchEvent(event)){13returntrue;14}15}16}17}

在onTouchEvent()中,觸摸事件在被消費之前先傳遞給了mTouchDelegate。它是一個觸摸代理實例:

1publicclassTouchDelegate{ 2//代理控件 3privateViewmDelegateView; 4//代理控件響應觸摸事件的區域 5privateRectmBounds; 6 7//構造函數 8publicTouchDelegate(Rectbounds,ViewdelegateView){ 9mBounds=bounds;10mDelegateView=delegateView;11...12}1314//處理觸摸事件15publicbooleanonTouchEvent(@NonNullMotionEventevent){16intx=(int)event.getX();17inty=(int)event.getY();18//是否將觸摸事件傳遞給代理19booleansendToDelegate=false;20booleanhandled=false;2122switch(event.getActionMasked()){23caseMotionEvent.ACTION_DOWN:24//若 DOWN 事件發生在設定區域內,則將所有事件都傳遞給它。25sendToDelegate=mBounds.contains(x,y);26...27break;28...29}30if(sendToDelegate){31//改變觸摸事件的位置,假裝它發生在代理控件的中心32event.setLocation(mDelegateView.getWidth()/2,mDelegateView.getHeight()/2);33//將觸摸事件傳遞給代理控件34handled=mDelegateView.dispatchTouchEvent(event);35}36returnhandled;37}38}

觸摸代理的構造函數中需傳入代理控件及其響應觸摸事件的區域。若觸摸事件落在該區域內則將事件傳遞給代理控件消費。

所以只需將代理控件的響應區域人為地增大即可實現點擊區域的擴大:

1valviewGroup:ViewGroup 2valchildView:View 3 4//為了獲取子控件相對於父控件的位置,必須psot 5viewGroup.post{ 6valrect=Rect() 7//獲取子控件相對於父控件位置並記錄在rect中 8ViewGroupUtils.getDescendantRect(viewGroup,childView,rect) 9//將rect橫向和縱向都往外擴100像素10rect.inset(-100,-100)11//為父控件設置觸摸代理12viewGroup.touchDelegate=TouchDelegate(childView,rect)13}

觸摸代理得設置在父控件上,因為子控件的觸摸事件經由父控件傳遞過來的,只有父控件中的觸摸代理才能優先處理事件。

若用上述代碼連續為兩個控件擴大點擊區域,就不奏效了。。。

自定義觸摸代理

因為View中只有一個TouchDelegate成員,且TouchDelegate中只有一個代理控件。

為了讓觸摸代理能服務多個控件,就不得不通過繼承擴展它:

1//多重觸摸代理 2classMultiTouchDelegate(bound:Rect?=null,delegateView:View) 3:TouchDelegate(bound,delegateView){ 4//保存多個代理控件及其觸摸區域的容器 5valdelegateViewMap=mutableMapOf<View,Rect>() 6//當前的代理控件 7privatevardelegateView:View?=null 8 9//新增代理控件10funaddDelegateView(delegateView:View,rect:Rect){11delegateViewMap[delegateView]=rect12}1314//完全重寫,以屏蔽父類邏輯15overridefunonTouchEvent(event:MotionEvent):Boolean{16valx=event.x.toInt()17valy=event.y.toInt()18varhandled=false19when(event.actionMasked){20MotionEvent.ACTION_DOWN->{21//DOWN發生時找到對應坐標下的代理控件22delegateView=findDelegateViewUnder(x,y)23}24MotionEvent.ACTION_CANCEL->{25delegateView=null26}27}28//若找到代理控件,則將所有事件都傳遞給它消費29delegateView?.let{30event.setLocation(it.width/2f,it.height/2f)31handled=it.dispatchTouchEvent(event)32}33returnhandled34}3536//遍歷代理控件,返回其觸摸區域包含指定坐標的那一個代理控件37privatefunfindDelegateViewUnder(x:Int,y:Int):View?{38delegateViewMap.forEach{entry->if(entry.value.contains(x,y))returnentry.key}39returnnull40}41}

然後就可以像這樣為多個控件擴大點擊區域:

1valviewGroup:ViewGroup 2valchildView1:View 3valchildView2:View 4valmultiTouchDelegate=MultiTouchDelegate(childView1) 5viewGroup.touchDelegate=multiTouchDelegate 6 7viewGroup.post{ 8valrect1=Rect() 9ViewGroupUtils.getDescendantRect(viewGroup,childView1,rect1)10rect1.inset(-100,-100)11multiTouchDelegate.addDelegateView(childView1,rect1)1213valrect2=Rect()14ViewGroupUtils.getDescendantRect(viewGroup,childView2,rect2)15rect2.inset(-200,-200)16multiTouchDelegate.addDelegateView(childView2,rect2)17}Kotlin 語法糖重構

這樣的使用成本還是太高了,對於業務層最友好的方式應該是只傳遞擴大的像素值,而無需關心「Rect對象創建」,「觸摸代理對象創建」這些實現細節。

那就運用 Kotlin 的擴展方法重構一下:

1//為View新增expand擴展方法 2funView.expand(dx:Int,dy:Int){ 3//將剛才定義代理類放到方法內部,調用方不需要了解這些細節 4classMultiTouchDelegate(bound:Rect?=null,delegateView:View):TouchDelegate(bound,delegateView){ 5valdelegateViewMap=mutableMapOf<View,Rect>() 6privatevardelegateView:View?=null 7 8overridefunonTouchEvent(event:MotionEvent):Boolean{ 9valx=event.x.toInt()10valy=event.y.toInt()11varhandled=false12when(event.actionMasked){13MotionEvent.ACTION_DOWN->{14delegateView=findDelegateViewUnder(x,y)15}16MotionEvent.ACTION_CANCEL->{17delegateView=null18}19}20delegateView?.let{21event.setLocation(it.width/2f,it.height/2f)22handled=it.dispatchTouchEvent(event)23}24returnhandled25}2627privatefunfindDelegateViewUnder(x:Int,y:Int):View?{28delegateViewMap.forEach{entry->if(entry.value.contains(x,y))returnentry.key}29returnnull30}31}3233//獲取當前控件的父控件34valparentView=parentas?ViewGroup35//若父控件不是ViewGroup,則直接返回36parentView?:return3738//若父控件未設置觸摸代理,則構建MultiTouchDelegate並設置給它39if(parentView.touchDelegate==null)parentView.touchDelegate=MultiTouchDelegate(delegateView=this)40post{41valrect=Rect()42//獲取子控件在父控件中的區域43ViewGroupUtils.getDescendantRect(parentView,this,rect)44//將響應區域擴大45rect.inset(-dx,-dy)46//將子控件作為代理控件添加到MultiTouchDelegate中47(parentView.touchDelegateas?MultiTouchDelegate)?.delegateViewMap?.put(this,rect)48}49}

然後業務層就可以像這樣輕鬆的擴大點擊區域:

1valchildView1:View2valchildView2:View34childView1.expand(100,100)5childView2.expand(200,200)talk is cheap, show me the code

View.expand()在這個倉庫的Layout.kt文件中

https://github.com/wisdomtl/Layout_DSL

推薦閱讀:

RxJava 堆棧異常信息顯示不全,怎麼搞

Android 音視頻開發【特效篇】【一】抖音傳送帶特效

快手線上 OOM 監控方案 - KOOM 分析

Flutter自定義之旋轉木馬 - 帶你回到童年時光

愛奇藝 Xcrash 是怎麼捕獲 crash 的

真牛系列 - 一步步解決 App 隱私違規問題

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()