Android Touch event distribution is an important content in the Android UI. Touch event distribution from the driver layer up through InputManagerService, WindowManagerService, ViewRootImpl, Window, Arrives at the DecorView, is distributed through the View tree, and is eventually consumed.

This article attempts to rewrite it by distributing events in the View section, which is most relevant to daily development. A rewrite is a significant simplification of the Android source code without losing any of its essentials, and can be run independently to see the big picture without getting bogged down in the details of the source code.

The following classes are custom classes, not Android native classes of the same name.

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(a): String {
        return "MotionEvent(x=$x, y=$y, action=$action)"}}Copy the code

First define MotionEvent, which reduces touch events to the four most commonly used actions and supports only one-finger operations, so action values support only four constants. And to simplify subsequent positional calculations, x and y represent absolute coordinates (equivalent to getRawX() and getRawY()), not relative coordinates.

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 = falseonClick? .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
Copy the code

Next, define the View. (1) defines the position of the View, again representing absolute coordinates, not relative to the parent View position. (2) Also use layout method to pass location, because we focus on the event distribution of View rather than its layout and drawing, so only layout is defined. (4) onTouch callback is handled by dispatchTouchEvent first. If no callback is called, onTouchEvent is called. (5) onTouchEvent mainly handles the onClick callback, although the real source code is more complicated to determine the click, but the actual effect is the same as here. (6) Use the extension function to determine whether the event occurred inside the View. (7) The two calls cooperate with downed to ensure that ACTION_MOVE and ACTION_UP are recognized as clicks only when they occur within the View. As for the long press and other gestures of monitoring, because of the more tedious, here is no longer implemented.

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
    }
}
Copy the code

Finally, implement ViewGroup: (2) onInterceptTouchEvent simply returns false, which is modified by subclass inheritance. (3) dispatchTouchEvent is the main logic in the implementation. To explain in detail, and the implementation contains only for single refers to the processing of Touch events, and does not contain requestDisallowInterceptTouchEvent situation.

(4) At the beginning and end of the source code, there are methods to clean up fields and marks, which are used to clean up old data at the beginning and end of an event sequence (starting with ACTION_DOWN, going through several ACTION_MOVE, etc., and finally ending with ACTION_UP, i.e. the whole touch process). This is simplified to empty the only field in our class, mFirstTouchTarget, which represents the target View of the entire sequence of events. In the source code, this variable is of type TouchTarget, implemented as a list node of a View to support multi-touch, simplified to View.

Next, the method is divided into several parts:

Events to intercept

Call onInterceptTouchEvent to check whether the ViewGroup intercepts the event at the beginning of an event sequence or if the target view has been found. (6) indicates that if ACTION_DOWN has no View consumption, subsequent events will be blocked, and the blocked View is the top-level View in the View tree, i.e., the Android DecorView.

Find the target view and distribute ACTION_DOWN

(7) If the ACTION_DOWN event is not intercepted, (8) reverse traverse the subview array, (9) find the View in which the ACTION_DOWN event falls, (10) pass the ACTION_DOWN event to the subview, Call the dispatchTransformedTouchEvent in this step, the method to simplify the method of source to three parameters, the method name in the Transformed, said will Touch events on coordinate system transform, which in order to simplify the use of coordinates is absolute, so there is no need to change. When invoked dispatchTransformedTouchEvent (11) to the child in the View distribution ACTION_DOWN, child mFirstTouchTarget namely.

Distribute events other than ACTION_DOWN

(12) for ACTION_DOWN events, will alreadyDispatchedToNewTouchTarget setting, (13) comes into the if block at this time, rather than ACTION_DOWN event will enter the else block. (14) when the event is intercepted ACTION_CANCEL or events, are called dispatchTransformedTouchEvent (15), amend the events to ACTION_CANCEL, then call (11), Assign ACTION_CANCEL to the child View, (16) and set mFirstTouchTarget to null. When the next event in the sequence of events arrives, it enters (17), which is the final call (18), which invokes the event handling of the View in the previous section, that is, the ViewGroup consumes the event, The ViewGroup that consumes the event is the ViewGroup that intercepts the non-ACTION_DOWN event and issues ACTION_CANCEL to its child views.

use

So now we’ve implemented MotionEvent, View, and ViewGroup to verify this.

Define three subclasses:

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)
    }
}
Copy the code

Define an event-generating method that simulates the trace and action of a Touch event:

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
}
Copy the code

In Android, events are passed step by step from the driver layer to the top of the View tree. Here we define a three-layer layout page, (1) directly invoke the dispatchTouchEvent of the top-level ViewGroup to enable event distribution.

fun main(a) {
    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}}Copy the code

The program can execute normally, print as follows:

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
Copy the code

Because we added click events in (2), the above represents the event distribution of one click. You can also rewrite the page layout (3) to see the event distribution flow in other scenarios, or rewrite the methods of VG1 and VG2 to add print and view.

conclusion

Through the arrangement of Android source code, with about 150 lines of code can achieve a simplified version of the Android Touch View event distribution, although for the concise code structure abandoned some functions, but the whole process is consistent with the Android Touch View event distribution, It makes it easier to understand the mechanics.