How to Implement Android View Touch Event Distribution Process by Yourself

  • 2021-12-11 19:11:12
  • OfStack

Directory MotionEventViewViewGroup event interception
Find the target view and distribute ACTION_DOWN
Distribute events other than ACTION_DOWN
Use Summarize

Android Touch event distribution is an important content in Android UI. Touch events go up from the driver layer, pass through InputManagerService, WindowManagerService, ViewRootImpl and Window, reach DecorView, and are distributed through View tree, and finally consumed.

This article attempts to rewrite the View part of the event distribution, which is also most closely related to daily development. Rewrite, in fact, this part of the source code of Android is greatly simplified without losing the main points, and can run independently, so as to get a glimpse of its whole picture without falling into the complicated details of the source code.

The following classes are custom classes, not Android native classes with 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(): String {
  return "MotionEvent(x=$x, y=$y, action=$action)"
 }
}

First, MotionEvent is defined, where the touch event action is reduced to the four most commonly used ones, and only single finger operation is supported, so the value of action only supports four constants. And in order to simplify the subsequent position calculation, 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 = 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

Next, define View. (1) Defines the position of the View, which again represents the absolute coordinates, not the position relative to the parent View. (2) We also use the layout method to pass the location, because we focus on the event distribution of View rather than its layout and drawing, so only layout is defined. (3) Touch callback is directly defined by function type here, and (4) dispatchTouchEvent handles onTouch callback first. If there is no callback, onTouchEvent is called, which shows the priority of the two. (5) onTouchEvent mainly deals with onClick callback. Although the judgment of click in real source code is more complicated, the actual effect is the same as here. (6) Spread function is used to determine whether the event occurs inside View, and (7) two calls cooperate with downed tag to ensure that ACTION_MOVE and ACTION_UP occur inside View before being recognized as clicks. As for the monitoring of other gestures such as long press, it is no longer realized here because it is cumbersome.

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
 }
}

Finally, to implement ViewGroup: (1) sub-View is passed in by constructor, but no longer provides addView and other methods, (2) onInterceptTouchEvent simply returns false, mainly through subclass inheritance to modify the return, (3) dispatchTouchEvent is the most important logic in the whole implementation, to explain in detail, the implementation here only includes the processing of single-finger Touch events, and does not include requestDisallowInterceptTouchEvent.

(4) There are methods to clean up fields and marks at the beginning and end of the source code. Used to clean up old data at the beginning and end of an event sequence (starting from ACTION_DOWN, passing through several ACTION_MOVE, etc., and finally ending with ACTION_UP, that is, the whole touch process), Simplify here to empty the only 1 field mFirstTouchTarget in our class (representing the target view of the whole event sequence. In the source code, this variable type is TouchTarget, which is implemented as a linked list node of View to support multi-finger touch, which is simplified to View here).

Next, the method is divided into several parts to introduce:

Event interception

(5) It means that onInterceptTouchEvent needs to be called to judge whether the ViewGroup intercepts events at the beginning of an event sequence or when the target view has been found. (6) indicates that if ACTION_DOWN has no view consumption, subsequent events will be intercepted, and the intercepted View is the top View in the View tree, that is, DecorView in Android.

Find the target view and distribute ACTION_DOWN

(7) When the ACTION_DOWN event is not intercepted, (8) traverses the sub-View array in reverse, (9) looks for the View in which the ACTION_DOWN event falls, (10) and passes the ACTION_DOWN event to the sub-View. This step uses dispatchTransformedTouchEvent. This method simplifies the method in the source code to 3 parameters. The Transformed representation in the method name will transform the coordinate system of the Touch event, and the coordinates used here for simplification are absolute, so there is no need for transformation. At this point, ACTION_DOWN is distributed to the child View at (11) in dispatchTransformedTouchEvent, and child is mFirstTouchTarget.

Distribute events other than ACTION_DOWN

(12) For the ACTION_DOWN event, alreadyDispatchedToNewTouchTarget is set, and (13) the if block is entered, whereas the non-ACTION_DOWN event is entered into the else block. (14) When the event is ACTION_CANCEL or the event is intercepted, the event is modified to ACTION_CANCEL after calling (15) of dispatchTransformedTouchEvent, and then (11) is called to distribute ACTION_CANCEL to child View, (16) while leaving mFirstTouchTarget empty. When the next event in the sequence of events arrives, it enters (17), the final call (18), which invokes the event processing of View in the previous section, i.e. ViewGroup consumes the event, and the ViewGroup consuming the event intercepts the non-ACTION_DOWN event and distributes ViewGroup of ACTION_CANCEL to the child View.

Use

At this point, MotionEvent, View and ViewGroup are implemented for 1 verification.

Define 3 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)
 }
}

Defines an event occurrence method that simulates the Touch event trajectory and 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
}

Next, we can verify that in Android, events are passed step by step from driver layer 1 to the top of View tree. Here, we define a 3-layer layout page, and (1) directly call dispatchTouchEvent of top-level ViewGroup to start event distribution.


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
 }
}

The program can be executed normally and printed 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

Because we added click events in (2), the above indicates the distribution of one-click events. You can also rewrite and modify the page layout (3) to view the event distribution process under other scenarios, or rewrite the methods of VG1 and VG2 to increase printing and viewing.

Summarize

By sorting out the source code of Android, a simplified version of Android Touch View event distribution can be realized with about 150 lines of code. Although some functions are abandoned for the sake of simplicity of code structure, the whole process is the same as Android Touch View event distribution, which makes it easier to understand this mechanism.

The above is how to implement the Android View Touch event distribution process in detail, more information about the implementation of Android View Touch event distribution process please pay attention to other related articles on this site!


Related articles: