如何自己實現(xiàn)Android View Touch事件分發(fā)流程
Android Touch事件分發(fā)是Android UI中的重要內容,Touch事件從驅動層向上,經過InputManagerService,WindowManagerService,ViewRootImpl,Window,到達DecorView,經View樹分發(fā),最終被消費。
本文嘗試通過對其中View部分的事件分發(fā),也是與日常開發(fā)聯(lián)系最緊密的部分,進行重寫。說是重寫,其實是對Android該部分源碼進行大幅精簡而不失要點,且能夠獨立運行,以一窺其全貌,而不陷入到源碼繁雜的細節(jié)中。
以下類均為自定義類,而非Android同名原生類。
MotionEvent
class MotionEvent {
companion object {
const val ACTION_DOWN = 0
const val ACTION_MOVE = 1
const val ACTION_UP = 2
const val ACTION_CANCEL = 3
}
var x = 0
var y = 0
var action = 0
override fun toString(): String {
return "MotionEvent(x=$x, y=$y, action=$action)"
}
}
首先定義MotionEvent,這里將觸摸事件action減少為最常用的4種,同時只支持單指操作,因此action取值僅支持4個常量。并且為了簡化后續(xù)的位置計算,x和y表示的是絕對坐標(相當于getRawX()與getRawY()),而非相對坐標。
View
open class View {
var left = 0
var right = 0
var top = 0
var bottom = 0//1
var enable = true
var clickable = false
var onTouch: ((View, MotionEvent) -> Boolean)? = null
var onClick: ((View) -> Unit)? = null//3
set(value) {
field = value
clickable = true
}
private var downed = false
open fun layout(l: Int, t: Int, r: Int, b: Int) {
left = l
top = t
right = r
bottom = b
}//2
open fun onTouchEvent(ev: MotionEvent): Boolean {
var handled: Boolean
if (enable && clickable) {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
downed = true
}
MotionEvent.ACTION_UP -> {
if (downed && ev.inView(this)) {//7
downed = false
onClick?.invoke(this)
}
}
MotionEvent.ACTION_MOVE -> {
if (!ev.inView(this)) {//7
downed = false
}
}
MotionEvent.ACTION_CANCEL -> {
downed = false
}
}
handled = true
} else {
handled = false
}
return handled
}//5
open fun dispatchTouchEvent(ev: MotionEvent): Boolean {
var result = false
if (onTouch != null && enable) {
result = onTouch!!.invoke(this, ev)
}
if (!result && onTouchEvent(ev)) {
result = true
}
return result
}//4
}
fun MotionEvent.inView(v: View) = v.left <= x && x <= v.right && v.top <= y && y <= v.bottom//6
接下來定義View。(1)定義了View的位置,這里同樣表示絕對坐標,而不是相對于父View的位置。(2)同時使用layout方法傳遞位置,因為我們的重點是View的事件分發(fā)而不是其布局與繪制,因此只定義了layout。(3)觸摸回調這里直接使用函數(shù)類型定義,(4)dispatchTouchEvent先處理了onTouch回調,如果未回調,則調用onTouchEvent,可見二者的優(yōu)先級。(5)onTouchEvent則主要處理了onClick回調,雖然真實源碼中對點擊的判斷更為復雜,但實際效果與此處是一致的,(6)使用擴展函數(shù)來確定事件是否發(fā)生在View內部,(7)兩處調用配合downed標記確保ACTION_MOVE與ACTION_UP發(fā)生在View內才被識別為點擊。至于長按等其他手勢的監(jiān)聽,因為較為繁瑣,這里就不再實現(xiàn)。
ViewGroup
open class ViewGroup(private vararg val children: View) : View() {//1
private var mFirstTouchTarget: View? = null
open fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return false
}//2
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {//3
val intercepted: Boolean
var handled = false
if (ev.action == MotionEvent.ACTION_DOWN) {
mFirstTouchTarget = null
}//4
if (ev.action == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev)//5
} else {
intercepted = true//6
}
val canceled = ev.action == MotionEvent.ACTION_CANCEL
var alreadyDispatchedToNewTouchTarget = false
if (!intercepted) {
if (ev.action == MotionEvent.ACTION_DOWN) {//7
for (child in children.reversed()) {//8
if (ev.inView(child)) {//9
if (dispatchTransformedTouchEvent(ev, false, child)) {//10
mFirstTouchTarget = child
alreadyDispatchedToNewTouchTarget = true//12
}
break
}
}
}
}
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null)//17
} else {
if (alreadyDispatchedToNewTouchTarget) {//13
handled = true
} else {
val cancelChild = canceled || intercepted//14
if (dispatchTransformedTouchEvent(ev, cancelChild, mFirstTouchTarget)) {
handled = true
}
if (cancelChild) {
mFirstTouchTarget = null//16
}
}
}
if (canceled || ev.action == MotionEvent.ACTION_UP) {
mFirstTouchTarget = null
}//4
return handled
}
private fun dispatchTransformedTouchEvent(ev: MotionEvent, cancel: Boolean, child: View?): Boolean {
if (cancel) {
ev.action = MotionEvent.ACTION_CANCEL//15
}
val oldAction = ev.action
val handled = if (child == null) {
super.dispatchTouchEvent(ev)//18
} else {
child.dispatchTouchEvent(ev)//11
}
ev.action = oldAction
return handled
}
}
最后來實現(xiàn)ViewGroup:(1)子View這里通過構造函數(shù)傳入, 而不再提供addView等方法,(2)onInterceptTouchEvent簡單返回false,主要通過子類繼承來修改返回,(3)dispatchTouchEvent是整個實現(xiàn)中最主要的邏輯,來詳細解釋,這里的實現(xiàn)只包含對單指Touch事件的處理,并且不包含requestDisallowInterceptTouchEvent的情況。
(4)源碼中開頭和結尾處有清理字段與標記的方法,用于在一個事件序列(由ACTION_DOWN開始,經過若干ACTION_MOVE等,最終以ACTION_UP結束,即整個觸摸過程)開頭和結束時清理舊數(shù)據(jù),這里簡化為了將我們類中的唯一字段mFirstTouchTarget(表示整個事件序列的目標視圖,在源碼中,此變量類型為TouchTarget,實現(xiàn)為一個View的鏈表節(jié)點,以此來支持多指觸摸,這里簡化為View)置空。
接下來將該方法分為幾部分來介紹:
事件攔截
(5)表示在一個事件序列的開始或者已經找到了目標視圖的情況下,才需要調用onInterceptTouchEvent判斷本ViewGroup是否攔截事件。(6)表示如果ACTION_DOWN沒有視圖消費,則之后的事件將被攔截,且攔截的View是View樹中的頂層View,即Android中的DecorView。
尋找目標視圖,分發(fā)ACTION_DOWN
(7)當ACTION_DOWN事件未被攔截,(8)則反向遍歷子View數(shù)組,(9)尋找ACTION_DOWN事件落在其中的View,(10)并將ACTION_DOWN事件傳遞給該子View,這一步調用了dispatchTransformedTouchEvent,該方法將源碼中的方法簡化為了三參數(shù),方法名中的Transformed表示,會將Touch事件進行坐標系的變換,而這里為了簡化使用的坐標是絕對的,因此不需要變換。此時會調用dispatchTransformedTouchEvent中(11)處向子View分發(fā)ACTION_DOWN,child即mFirstTouchTarget。
分發(fā)除ACTION_DOWN外的其他事件
(12)對于ACTION_DOWN事件,會將alreadyDispatchedToNewTouchTarget置位,(13)此時會會進入if塊,而非ACTION_DOWN事件會進入else塊。(14)當該事件是ACTION_CANCEL或者事件被攔截,則在調用dispatchTransformedTouchEvent的(15)處后,將事件修改為ACTION_CANCEL,然后調用(11),將ACTION_CANCEL分發(fā)給子View,(16)同時將mFirstTouchTarget置空。當事件序列中的下個事件到來時,會進入(17)處,即最終調用(18),調用上節(jié)中View的事件處理,即ViewGroup消費該事件,消費該事件的ViewGroup即攔截了非ACTION_DOWN事件并向子View分發(fā)ACTION_CANCEL的ViewGroup。
使用
至此,實現(xiàn)了MotionEvent,View,與ViewGroup,來進行一下驗證。
定義三個子類:
class VG1(vararg children: View) : ViewGroup(*children)
class VG2(vararg children: View) : ViewGroup(*children)
class V : View() {
override fun onTouchEvent(ev: MotionEvent): Boolean {
println("V onTouchEvent $ev")
return super.onTouchEvent(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
println("V dispatchTouchEvent $ev")
return super.dispatchTouchEvent(ev)
}
}
定義一個事件發(fā)生方法,由該方法來模擬Touch事件的軌跡與action:
fun produceEvents(startX: Int, startY: Int, endX: Int, endY: Int, stepNum: Int): List<MotionEvent> {
val list = arrayListOf<MotionEvent>()
val stepX = (endX - startX) / stepNum
val stepY = (endY - startY) / stepNum
for (i in 0..stepNum) {
when (i) {
0 -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_DOWN
x = startX
y = startY
})
}
stepNum -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_UP
x = endX
y = endY
})
}
else -> {
list.add(MotionEvent().apply {
action = MotionEvent.ACTION_MOVE
x = stepX * i + startX
y = stepY * i + startY
})
}
}
}
return list
}
接下來就可以驗證了,在Android中事件由驅動層一步步傳遞至View樹的頂端,這里我們定義一個三層的布局page,(1)直接將事件序列遍歷調用頂層ViewGroup的dispatchTouchEvent來開啟事件分發(fā)。
fun main() {
val page = VG1(
VG2(
V().apply { layout(0, 0, 100, 100); onClick = { println("Click in V") } }//2
).apply { layout(0, 0, 200, 200) }
).apply { layout(0, 0, 300, 300) }//3
val events = produceEvents(50, 50, 90, 90, 5)
events.forEach {
page.dispatchTouchEvent(it)//1
}
}
程序可以正常執(zhí)行,打印如下:
V dispatchTouchEvent MotionEvent(x=50, y=50, action=0) V onTouchEvent MotionEvent(x=50, y=50, action=0) V dispatchTouchEvent MotionEvent(x=58, y=58, action=1) V onTouchEvent MotionEvent(x=58, y=58, action=1) V dispatchTouchEvent MotionEvent(x=66, y=66, action=1) V onTouchEvent MotionEvent(x=66, y=66, action=1) V dispatchTouchEvent MotionEvent(x=74, y=74, action=1) V onTouchEvent MotionEvent(x=74, y=74, action=1) V dispatchTouchEvent MotionEvent(x=82, y=82, action=1) V onTouchEvent MotionEvent(x=82, y=82, action=1) V dispatchTouchEvent MotionEvent(x=90, y=90, action=2) V onTouchEvent MotionEvent(x=90, y=90, action=2) Click in V
因為我們在(2)增加了點擊事件,以上表示了一次點擊的事件分發(fā)。也可以重寫修改page布局(3)來查看其它情景下的事件分發(fā)流程,或者重寫VG1,VG2的方法,增加打印并查看。
總結
通過對Android 源碼的整理,用約150行代碼就能實現(xiàn)了一個簡化版的Android Touch View事件分發(fā),雖然為了代碼結構的簡潔舍棄了部分功能,但整個流程與Android Touch View事件分發(fā)是一致的,能夠更方便理解這套機制。
以上就是如何自己實現(xiàn)Android View Touch事件分發(fā)流程的詳細內容,更多關于實現(xiàn)Android View Touch事件分發(fā)流程的資料請關注腳本之家其它相關文章!
相關文章
解決Android SearchView不顯示搜索icon的問題
這篇文章主要介紹了解決Android SearchView不顯示搜索icon問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05

