(Please indicate the author: RubiTree, address:blog.rubitree.com )

primers

Event distribution, I think most people can say a few words, oh, the three methods, oh, that classic pseudo-code, oh, chain of responsibility… But if you have to go through it completely, you might start to falter and just say what comes to mind

This piece of stuff is really troublesome, I’m not afraid to scare you, how the flow of events is related to these factors: Which event type (DOWN/MOVE/UP/CANCEL), which View level (Activity/ViewGroup/View), which callback method (dispatch()/onIntercept()/onTouch()), which callback method returns different values (tr Ue /false/super.xxx), and even different processing of the current event can have different effects on subsequent events in the same event stream

For example, I could ask: what would happen if I overwrote the dispatchTouchEvent method in a ViewGroup and returned false for a MOVE event?

So some people summed up these situations, got a lot of rules, and drew a complicated flow chart of event distribution:

There are even dynamic flow charts like the picture of the topic (yes, the picture of the topic that attracted you is actually a negative textbook. I also feel very distressed. It took me half the afternoon to draw it, but the result was not much help)

These rules and flowcharts are true, and in a sense very clear, to help you find a little direction when debugging bugs. You may be able to work hard and memorize these flow charts and patterns, or you may be able to rattle them off when you need to. But they don’t really give you an idea of what event distribution looks like, and you might spend a lot of time trying to understand them at one time, but “get it every time! And then forget it!” (Quote of a typical comment)

But in all fairness, why is it so complicated to distribute a touch event? Does it need to be that complicated? What figure?

So, let’s go back to the beginning and see what kind of problem distributing touch events solves. Is there an easier way to distribute them? Then see how this simple distribution strategy can be adjusted as demand increases. When you see the end, you will realize that everything is so natural.

So, don’t have to memorize, also need not rush to Dui complete event distribution process, so many complex logic and condition are developed around the fundamental problem, is step by step, with the increase of demand become complex, understand the evolution process, you will realize the results of its evolution, want to forget all forget.

Start with the basics and everything will fall into place.

Ivy Batty, black feed the dog! Next, I will start with the simplest requirements to think about the solution and write the code, then step by step increase the requirements, adjust the solution, continue to write the code, and strive to create a small event distribution framework.

5. Build the wheel

1.1. First build: Pass directly to target View

Let’s start with one of the simplest requirements: an Activity has a bunch of views nested in layers, and only the innermost View will consume events

(A yellow highlighted View represents an event that can be consumed, and a blue View represents an event that cannot be consumed)

Thinking scheme:

  1. First of all, where does the event come from? It must come from the father, because the child View is wrapped inside, there is no way to communicate directly with the outside world, and in realityActivityIt’s attached to the root ViewDecorViewIt is a bridge to the outside world and can receive touch events sent from the screen hardware
  2. So the event starts withActivityYou start by going through the layer by layer ViewGroup to the View at the bottom
  3. All you need is one that passes events from the outside inpassEvent(ev)Method, the father layers to the inside, can pass the event in the past, to complete the demand

Schematic diagram

Sparrow code:

(The code for this article is written using Kotlin, and the core code is providedJava version)

open class MView {
    open fun passEvent(ev: MotionEvent) {
        // do sth}}class MViewGroup(private val child: MView) : MView() {
    override fun passEvent(ev: MotionEvent) {
        child.passEvent(ev)
    }
}
Copy the code
  1. putActivityAs aMViewGroupThere was no problem with it
  2. Why is itMViewGroupinheritanceMViewNot the other way around, becauseMViewYou don’t need tochildThe field

1.2. Second build: Pass to target View from inside out

Then we add a requirement to complicate things a bit: An Activity has a bunch of views nested in layers, with several views stacked on top of each other to handle events

We also need to add a design principle: each user action can only be processed by one View.

  1. This principle is required so that the feedback of the operation is intuitive to the user
  2. It’s easy to understand that normally people only think about doing one thing at a time
    1. For example, if you have a list item, the list can be clicked to go to details, and there is an edit button on the list that can be clicked to edit the item
      1. This is a scenario where you can click on both the top and bottom views, but when you click on a place, you only want to do one thing, either to access details or to edit items, and if you click on the edit result and skip two pages, it’s definitely not appropriate
    2. Another example is in a list of clickable items (such as wechat’s message interface), items can be clicked into a chat, and the list can be swiped up and down to view
      1. If you let both items and lists handle events, you might have to jump to a bunch of chat pages you don’t want to go to while you’re swiping

If you’re using the framework you’re trying out for the first time, you need to stop calling child’s passEvent() method at each View hierarchy that can handle events, and ensure that only you handled the event. But if this were implemented, it would look strange in most scenarios because the events are handled in the wrong order:

  1. For example, in the list above, when the user clicks on the button to edit the item, clicking on the event will be transmitted to the item first. If you decide that you need the event in the item, and then consume the event without passing it to the child View, the user will never be able to open the edit item
  2. And it’s even more obvious if you look at it from another perspective, where do you want to click, where is the closest thing to your finger to be triggered

So one of the keys to implementing the new requirement was to find the View that was suitable for handling events, and we analyzed the business scenario and came up with the answer: the innermost View was suitable for handling events

Instead of waiting for the parent to stop processing events and then pass them to the child, you need events to be handled from the inside out: when the child inside doesn’t want the event, the parent calls passEvent() to pass the event out. So you have to add an outward channel, and you can only process events on this outward channel, and the front inward channel does nothing but pass events in. So now you have two channels, let’s call them passIn(), passOut(), passIn().

Schematic diagram

Sparrow code:

open class MView {
    var parent: MView? = null

    open fun passIn(ev: MotionEvent) {
        passOut(ev)
    }

    open fun passOut(ev: MotionEvent){ parent? .passOut(ev) } }class MViewGroup(private val child: MView) : MView() {
    init {
        child.parent = this //
    }

    override fun passIn(ev: MotionEvent) {
        child.passIn(ev)
    }
}
Copy the code

This code is fine and very simple, but it is not clear about the intent of the requirements, which makes it difficult to use the framework

  1. As mentioned earlier, we hopepassIn()When only events are passed, hope inpassOut()Each View decides whether to handle the event, does so, and is not called again after the event is handledparentthepassOut()Method to spread the word
  2. You’ll notice that there are two types of responsibilities:
    1. One is event-passing control logic and the other is event-handling hooks
    2. The event-passing control logic is basically unchanged, but anything can be done in the hooks that handle the event
  3. We need to separate the code with different responsibilities, and even more importantly, we need to separate the variable from the constant, reducing the focus of the framework’s users

So we use a method called dispatch() to hold the event-passing control logic separately, and a method called onTouch() to hold the event-handling hook with a return value indicating whether the event was handled in the hook:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false}}class MViewGroup(private val child: MView) : MView() {
    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = child.dispatch(ev)
        if(! handled) handled = onTouch(ev)return handled
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false}}Copy the code

When you write this, the behavior doesn’t really change, but you can see:

  1. The control logic is focused ondispatch()In, at a glance
  2. onTouch()Simply a hook, the framework user only needs to care about the hook and its return value, not the control flow
  3. In addition, evenparentI don’t need it anymore

1.3. Three builds: Distinguish event types

The implementation above looks like it’s starting to take shape, but it doesn’t even implement the original principle, because the principle requires that only one View be processed at a time, whereas we’re implementing only one View for a touch event. Here is the difference between a touch operation and a touch event:

  1. Assuming there is no concept of a touch event, how can we distinguish a touch operation?
    1. Break it down into touch actions, such as pressing, lifting, moving and holding while touching the screen
    2. Is easy to think, to distinguish between two touch, can be differentiated by the movements of the press and lift, press the action began a touch, lift action over a touch, press and lift in the middle of the movement and stay belong to touch this time, as for mobile and if we need to distinguish, now didn’t see the need to distinguish, can be treated as the touch
  2. So there are three types of actions in a single touch:DOWN/UP/ING, includingINGIt’s a little unprofessional. Change it toMOVE!
  3. Each touch action generates a touch event of the same type in the software system
  4. So in the end, a touch is done by a group of usersDOWNThe event begins and the middle is multipleMOVEEvent, and finally end atUPEvent flow composition of events

The design principle is more accurate: a stream of events generated by a single touch can only be consumed by a single View

Turning an event into a group event stream is very simple: handle the DOWN event as you did the previous one, but remember who consumed the DOWN event, and pass the subsequent MOVE/UP events directly to it

Sparrow code:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false}}class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
        
            handled = child.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if(! handled) handled = onTouch(ev) }else {
            if (isChildNeedEvent) handled = child.dispatch(ev)
            if(! handled) handled = onTouch(ev) }if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
            
        return handled
    }
    
    private fun clearStatus(a) {
        isChildNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false}}Copy the code

The code seems to have increased a lot, but it only does two more things:

  1. Add oneisChildNeedEventState, for whether the child View is processedDOWNEvent is logged and used for other touch events
  2. Upon receipt ofDOWNThe beginning and receipt of the eventUPAt the end of the event, the state is reset

At this point, the framework user just needs to worry about the onTouch() hook, handle it when it needs to handle the event and return true, and the framework has done everything else.

1.4. Build four: Added external event interception

The above framework can already do the basic work of event distribution, but the following requirements, you try to use the current framework can achieve? Requirement: There is a clickable View in the slideable View, so the user can slide the outside slideable View even if the position is clickable View.

If the frame above is used:

  1. The slippable View will transmit the event first to the clickable View inside
  2. Can click on View a look at the event, I can click, that she who ah
  3. And then the outside slideable View can never handle the event, so it can’t slide

So the clickable sliding list of items implemented directly with the current model will never slide.

So what to do?

  1. Do you want the clickable View inside to feel that it is wrapped in a View that consumes events? If so, do not consume events?
    1. This is definitely not possible, let alone the child View layer layer reverse traversal father is not a good implementation, at least not outside can be sliding, inside View click events are all invalid
  2. Or we adjustdispatch()Method in the process of passing events, so that it can not only pass events in, but to itself when it can consume events
    1. This is definitely not going to work either, as is the main problem with the first method

If you want to achieve it directly, there are contradictions everywhere and you can’t find a breakthrough. Let’s start from the beginning, starting from what kind of touch feedback is natural for users, see if there is such an intuitive feedback scheme, find out what it is, and then consider how to achieve it:

  1. When the user has a sliding View and a clickable View, what does he do when he touches the clickable View?
  2. Obviously, there are only two possibilities, either the user wants to click on this clickable View, or the user wants to swipe on this clickable View
  3. So, when the user first touches it with his finger, which is thetaDOWNWhen the event first comes in, can you tell what the user wants to do? I’m sorry, but I can’t
  4. So, objectively, you just can’t beDOWNWhen the event comes in, they figure out what the user wants to do, so neither View can actually decide whether they want to consume the event, right

I *, this is not silly *, also make what GUI ah, we all use the command line and so on, don’t worry, GUI still have to do, don’t make no rice to eat I tell you, so you still have to think, try your best to achieve.

If you forget about the previous principle, think about it, regardless of other factors, not only use DOWN events, as long as you can determine what the user is thinking, what can you do

  1. There has to be a way. You canWait a little longer to see what pattern the user’s next actions match
    1. Click action works like this: user firstDOWNAnd thenMOVEIt’s a very small section, it’s not going to MOVE out of this sub-view, but it’s going to take a very short timeUP
    2. The mode of sliding is like this: user firstDOWNAnd then startMOVE“, it may or may not MOVE out of the child View, but the point is that it hasn’t been there for a long timeUP, has been inMOVE
  2. So your conclusion is, “DOWN” doesn’t work. We’ll see what happens next
  3. Consider one more long press case, and the summary is:
    1. If at some point in timeUPClick on the View inside
    2. If it takes a long timeUPBut nothingMOVE, is the View inside the long press
    3. If in a relatively short timeMOVEFor a longer distance, just slide the View outside

This goal View decision scheme seems to be a good one, and it is clearly laid out, but our existing event processing framework does not achieve this decision scheme, and there are at least two conflicts:

  1. Because neither child View nor parent View can be inDOWN“To determine whether the current stream of events should be given to them, so at first they can only be returnedfalse. But in order to be able to make judgments about subsequent events, you want events to continue flowing through them, and by the logic of the current framework, you can’t go backfalse.
  2. Suppose events flow through them, and when the event flows for a while, the parent View decides that it fits its consumption pattern and wants to consume the event for itself, but the child View may already be consuming the event, and the current framework can’t prevent the child View from consuming the event

So resolving these conflicts will require a change to the previous version of the event-handling framework, and it looks like it could be a big one if you’re not careful

  1. Let’s start with the second conflict, which has an immediate solution: adjustmentdispatch()Method in the process of passing the event, so that it is not only passing the event, but also before passing the event in the interception, can see the situation under the interception of the event and give their ownonTouch()To deal with
  2. Based on this solution, there are probably the following two solution adjustment ideas with relatively little change:
    1. Idea 1:
      1. When an event reaches the slideable parent View, it intercepts and processes the event, and saves the event
      2. After several events
        1. If you decide it fits your spending pattern, you can start spending on your own without having to accumulate more events
        2. If it determines that it is not its own consumption pattern, then it gives all the saved events to the sub-view and triggers the click operation inside
    2. Idea 2:
      1. All views consume events as long as they canonTouch()In theDOWNEvent to returntrue, regardless of whether they recognize their current consumption patterns
      2. When the event comes to the slippable parent View, it first passes the event to the inside, which may or may not process the event, the slippable parent View is temporarily unconcerned
      3. Then see if the child View handles the event
        1. If the child View does not handle events, then there is no problem. The parent View handles events directly
        2. If the child View processing events, sliding parent View will be taut nerve secretly observe waiting for the opportunity to act, observe the event is not in line with their consumption patterns, once found in line, it will intercept the event flow, even if the child View is also processing events, it also does not go indisptachEvent, but directly to himselfonTouch()
  3. Two ideas are summarized:
    1. Idea 1: The external parent View blocks the event first, if the judgment blocks wrong, then sends the event to the inside
    2. Idea 2: The parent View outside first does not block the event, when judging should block, suddenly block the event
  4. Both of these ideas need to change the current framework, which seems similar, but in fact there are more obvious advantages and disadvantages
    1. The first problem is obvious:
      1. The parent View blocks the event, and then gives it to the child View, but the child View does not necessarily consume the event, so it is a waste of time. Wait until the child View does not process the event, and return the events to the parent View, the parent View has to continue to process the event. The whole process is tedious and uncomfortable for developers
      2. So that’s not a very good idea, but you have to give the event to the child View first
    2. Train of thought 2 is much more normal, with only one problem (in the next section, you can guess, but I’ll ignore it here) and very few changes to the framework:
      1. Add an interception methodonIntercept()Inside the parent ViewdispatchBefore an event, developers can override this method, add their own event-pattern analysis code, and intercept it if they decide to
        1. It makes sense to divide the analysis of blocking logic into a single method: when to block and when not to block. There is a lot of logic inside, but the exposed API can be small enough to be easily removed
      2. When you are sure you want to intercept the event, even if you consume the event in the beginning, you do not send the event to the inside, but directly to yourselfonTouch()

Schematic diagram:

So using idea 2, on the basis of “three build”, modify the following code:

open class MView {
    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false}}class MViewGroup(private val child: MView) : MView() {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            if (onIntercept(ev)) {
                isSelfNeedEvent = true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if(! handled) { handled = onTouch(ev)if (handled) isSelfNeedEvent = true}}}else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                if (onIntercept(ev)) {
                    isSelfNeedEvent = true
                    handled = onTouch(ev)
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }

        if (ev.actionMasked == MotionEvent.ACTION_UP) {
            clearStatus()
        }
        
        return handled
    }

    private fun clearStatus(a) {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false}}Copy the code

Some details were added to the writing process:

  1. Not only inDOWNThe eventdispatch()Interception is required before, and interception also needs to be added in subsequent events, otherwise the goal of interception cannot be achieved
  2. After deciding to intercept an event, do you need to decide whether to intercept again in subsequent events?
    1. Not at all. What we want is for as few objects as possible to consume events in a single touch. The decision is yours, so don’t change
    2. So add oneisSelfNeedEventRecord whether you have intercepted the event, if so, subsequent events are directly handed to you
  3. In the subsequent event, the child View does not process the event, and the external View does not process the event, again because only one View can process the event.

This code might seem a bit more complicated, but it just adds an event blocking mechanism that makes it easier to understand than the wheel we tried last time. (If only Markdown supported custom coloring within code blocks)

And for the users of the framework, there is still very little focus

  1. rewriteonIntercept()Method to determine when an event needs to be intercepted and return when it needs to be interceptedtrue
  2. rewriteonTouch()Method, if the event was handledtrue

1.5. Fifth build: Added internal event interception

The parent View may not be able to give it any subsequent events after receiving half of the events that the parent View has already started processing and doing some work. Or even stranger?

This problem is really more troublesome, divided into two cases to discuss

  1. The View inside receives half of the events, but hasn’t actually started feedback interaction yet, or is in the process of feedback that can be cancelled
    1. For example, for a clickable View, the default implementation of the View is as long as it’s touchedpressedState, if you set the correspondingbackgroundAnd your View will be highlighted
    2. This kind of highlighting is fine even if it is interrupted. It won’t make users feel strange. Try wechat’s chat list yourself
    3. But one point worth noting is if you just don’t send it directlyMOVEEvent up, this will be a problem on this highlight example if you just don’t passMOVEWho is going to tell the child View inside to unhighlight? So you need to pass an end event when you interrupt
      1. But you can just pass oneUPEvent? It doesn’t work either, because it matches the pattern of the click inside, and it triggers a click event, which is obviously not what we want
      2. So you need to give a new event, and the type of event is called cancel eventCANCEL
    4. To summarize, for this simple cancelable case, you can do this:
      1. When sure to intercept, forward the real event to your ownonTouch()At the same time, generate a new event to send to its child View, event type isCANCEL, which will be the last event received by the child View
      2. The child View can cancel some of the current actions upon receiving this event
  2. The View inside receives half of the event and has already started to give feedback to the interaction, and this feedback is best not to cancel it, or cancel it would be weird
    1. At this point, things will be a little more complicated, and the scenario will happen in a lot more ways than you think, and the consequences of not handling it well will be much more serious than just making the user feel strange, and some features may not be implemented. Here are two examples
      1. inViewPagerThere are three pages in the pageScrollView.ViewPagerYou can swipe horizontally, in pageScrollViewI can slide vertically
        1. If we follow the previous logic, whenViewPagerPut the event insideScrollViewAfter that, it also watches, and if you keep swiping vertically, that’s fine,ViewPagerNo interception event is triggered
        2. But if you slide vertically, your hand shakes, and you start sliding sideways (or just sideways),ViewPagerYou start to get nervous and think, “Did the division finally decide it’s me? Seriously, I’m not going to do anything about it.” And then, after you’ve skidded a certain distance, you suddenly realize that you can’t moveScrollViewAnd theViewPagerBegan to move
        3. The reason is thatScrollViewThe vertical slide has been cancelled.ViewPagerStop the incident and start the slide
        4. It’s a weird experience that feels overly sensitive and leaves the user swiping gingerly
      2. In aScrollViewThere are some buttons, buttons have long press events, long press and drag to move the button
        1. (A more common example is a list of items that can be dragged and held)
        2. Using the same logic, how do you make sure you don’t drag a button after a long pressScrollViewWhat about stopping it?
    2. So these kinds of problems must be solved, but how
      1. Or from the business point of view, from the user’s point of view, when the inside has started to do some special processing, should the outside take away the event?
        1. Shouldn’t be, right? OK, so the solution is you shouldn’t let outside views rob events
      2. So the next question is: who can tell first that the outside View should not rob the event, the child View inside or the parent View outside? And then why don’t you let the View outside grab it?
        1. First of all, it must be the View inside to make a judgment: this event, really, you’d better not rob the View outside, or the user is not happy
        2. And then the inside has to tell the outside, you don’t rob, there are several ways to tell
          1. Ask if I can rob the inside before you rob the outside
          2. The inside, after determining that the incident could not be robbed, begandispatchThe method returns a special value to the outsidetrueandfalse, now add one.)
          3. Inside through other ways to inform the outside, you do not rob
        3. To be fair, I think all three methods work, but the third method is the most straightforward and doesn’t change the framework too much. Android also uses this method, where the parent View provides a method for the child ViewrequestDisallowInterceptTouchEvent()The child View calls it to change a state of the parent View, and the parent View determines this state each time before preparing to intercept (of course, this state is only valid for the current event stream).
        4. One more thing to note about this situation, however, is that it should be recursive upwards, meaning that in complex situations, it is possible for multiple superiors to be watching, and when the inside View decides to handle the event and is not going to hand it over, all the outside parent views that are watching should turn their heads back

So, together with the last trial build, to sum up

  1. In the case of nested views with multiple consumable events, how can the attribution of events become very troublesome and not immediately availableDOWNIt is determined at the time of the event and can only be further determined in the subsequent event stream
  2. Therefore, when there is no attribution judgment, the sub-view inside first consumes the event, and the outside secretly observes it. At the same time, the two parties make further matching of the event type, and prepare to shoot the attribution of the event stream after successful matching
  3. Snatch is the first shot
    1. Dad gets it first. Send itCANCELIt’s over for the son
    2. When the son gets it first, he has to yell and scream, and the father has to be kind enough to deal with the incident

There are a few other things worth mentioning:

  1. This kind of first-mover approach feels a bit crazy, but there is no better way to do it now. It is usually the developer’s own adjustment based on the actual user experience, allowing father or son to grab the right events at the right time and at the right time
  2. After intercepting the event, the parent View passes the following event to its ownonTouch()Later,onTouch()Only the second half of the event will be received, so will it be a problem?
    1. It is true that directly feeding the latter half of the event can be problematic, so it is generally a good idea to do some preparatory work when you do not intercept the event, so that you can intercept the event later, and use only the latter half of the event to achieve intuitive feedback

On the basis of “Four manufacturing”, the following code is modified:

interface ViewParent {
    fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean)
}

open class MView {
    var parent: ViewParent? = null

    open fun dispatch(ev: MotionEvent): Boolean {
        return onTouch(ev)
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false}}open class MViewGroup(private val child: MView) : MView(), ViewParent {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false
    private var isDisallowIntercept = false

    init {
        child.parent = this
    }

    override fun dispatch(ev: MotionEvent): Boolean {
        var handled = false
        
        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()
            
            // add isDisallowIntercept
            if(! isDisallowIntercept && onIntercept(ev)) { isSelfNeedEvent =true
                handled = onTouch(ev)
            } else {
                handled = child.dispatch(ev)
                if (handled) isChildNeedEvent = true

                if(! handled) { handled = onTouch(ev)if (handled) isSelfNeedEvent = true}}}else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                // add isDisallowIntercept
                if(! isDisallowIntercept && onIntercept(ev)) { isSelfNeedEvent =true

                    // add cancel
                    val cancel = MotionEvent.obtain(ev)
                    cancel.action = MotionEvent.ACTION_CANCEL
                    handled = child.dispatch(cancel)
                    cancel.recycle()
                } else {
                    handled = child.dispatch(ev)
                }
            }
        }
        
        if (ev.actionMasked == MotionEvent.ACTION_UP 
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }
        
        return handled
    }
    
    private fun clearStatus(a) {
        isChildNeedEvent = false
        isSelfNeedEvent = false
        isDisallowIntercept = false
    }

    override fun onTouch(ev: MotionEvent): Boolean {
        return false
    }

    open fun onIntercept(ev: MotionEvent): Boolean {
        return false
    }

    override fun requestDisallowInterceptTouchEvent(isDisallowIntercept: Boolean) {
        this.isDisallowIntercept = isDisallowIntercept parent? .requestDisallowInterceptTouchEvent(isDisallowIntercept) } }Copy the code

This change is mainly added a CANCEL events and requestDisallowInterceptTouchEvent mechanism

  1. In aCANCELThere was one detail of the incident: there was no givingchilddistributionCANCELEvent while continuing to distribute the original event to their ownonTouch2. This is written in the source code, not my intention, may be to let an event can only have a View processing, to avoid bugs
  2. implementationrequestDisallowInterceptTouchEventMechanism, increasedViewParentinterface
    1. It’s fine not to use this, but it’s more elegant from a clean code point of view, like avoiding reverse dependencies, and it’s also the source code, so just move it

Although the code of the whole framework is a bit complicated at present, it is still very simple for users. It is just added on the basis of the previous version of the framework:

  1. The parent View needs to be called immediately if the View determines that it wants to consume events and is performing operations that it does not want interrupted by the parent ViewrequestDisallowInterceptTouchEvent()methods
  2. If theonTouchMethod to consume events and do something that needs to be noted before receivingCANCELEvent to cancel the operation

The main logic of event distribution has been explained, but there is still a bit of processing left in the Activity. In fact, it does something similar to ViewGroup, with these differences:

  1. Events are not intercepted
  2. As long as there are events that the child View is not handling, it will hand them over to itselfonTouch()

So without further ado, let’s go straight to the sparrow of Activity:

open class MActivity(private val childGroup: MViewGroup) {
    private var isChildNeedEvent = false
    private var isSelfNeedEvent = false

    open fun dispatch(ev: MotionEvent): Boolean {
        var handled = false

        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            clearStatus()

            handled = childGroup.dispatch(ev)
            if (handled) isChildNeedEvent = true

            if(! handled) { handled = onTouch(ev)if (handled) isSelfNeedEvent = true}}else {
            if (isSelfNeedEvent) {
                handled = onTouch(ev)
            } else if (isChildNeedEvent) {
                handled = childGroup.dispatch(ev)
            }

            if(! handled) handled = onTouch(ev) }if (ev.actionMasked == MotionEvent.ACTION_UP
            || ev.actionMasked == MotionEvent.ACTION_CANCEL) {
            clearStatus()
        }

        return handled
    }

    private fun clearStatus(a) {
        isChildNeedEvent = false
        isSelfNeedEvent = false
    }

    open fun onTouch(ev: MotionEvent): Boolean {
        return false}}Copy the code

1.6. Summarize

At this point, we have finally built a rough but not inferior wheel, the main logic of the source code and it is not much different, specific differences are probably: TouchTarget mechanism, multi-touch mechanism, NestedScrolling mechanism, handling various listener, combined with View state processing, etc., compared to the main logic, they are not so important, you can read the source code. I will also write about multi-touch and TouchTarget when I have time.

The full code for the wheel can be viewed here (Java version). The wheel has been stripped of the source code related to event distribution and can be seen:

  1. Compared to the source code, this code is short and simple enough that anything that has nothing to do with event distribution doesn’t bother me
    1. No more than 150 lines in length, all code unrelated to event distribution is removed, and some logic that might have been complicated by other details is expressed in a simpler and more direct way
  2. In contrast to the classic event distribution pseudo-code (see appendix), this code is detailed enough to tell you all the details you need to know about event distribution
    1. While that classic pseudo-code was only designed in a nutshell, this sparrows code is extremely concise but has all the elements in it to run — you can use it to create pseudo-layouts and trigger touch events

But it’s not the wheel that matters, it’s the evolution that matters.

So if you look back, you’ll see that event distribution is very simple, and it’s not about “different event types, different View types, different callback methods, different return values of methods”. The key is “What does it do? What does it require? What is the solution?” From this perspective, the whole process of event distribution can be clearly and simply sorted out.

The function of event distribution is to give feedback to the user’s touch operation and make it conform to the user’s intuition.

Two requirements emerge from the user’s intuition:

  1. Users only have one View to consume at a time
  2. Make the View of the consumption event consistent with the user’s intention

The second requirement is the most difficult to implement. If there are multiple views that can consume touch events, how to determine which View is more suitable to consume and give the event to it. We use a simple but effective first-come, first-served strategy to give both internal and external consumable event views near-equal competitive consumer status: they can receive events, and when they decide that they should consume events, they can initiate a competitive application, and after the application is successful, the events will be consumed by them.

(Please indicate the author: RubiTree, address:blog.rubitree.com )

2. Test the wheels

Someone may ask, listen to your armchair strategist for a long time, you really speak with the same source, this if not I am not a loss. That’s a good question, so I’m going to run a simple test on this sparrow using a log test framework that tests event distribution, and I’m going to have a practice session where I’m going to practice what I said above.

2.1. Test framework

The idea is to track the process of event distribution by printing a log in the hook for each event distribution. Therefore, different operations need to be performed on different hooks in different View hierarchies for different touch events to create various event distribution scenarios.

In order to reduce duplicate code, a simple test framework is built (all codes can be viewed here), including an interface IDispatchDelegate and its implementation class that can delegate these operations in View, and a DispatchConfig unified configuration of different scenarios. After that, we created real controls SystemViews that use unified configuration and proxy operations and SparrowViews that we implemented ourselves.

After configuring the event distribution policy in DispatchConfig, directly start DelegatedActivity in SystemViews, touch and filter with TouchDojo keyword to get the event distribution trace log. Also, running the Dispatch () test method in SparrowActivityTest gives you the event distribution trace log for the Sparrow control.

2.2. Test process

The scene of a

Configure a policy to simulate a scenario where neither View nor ViewGroup consumes events:

fun getActivityDispatchDelegate(layer: String = "Activity"): IDispatchDelegate {
    return DispatchDelegate(layer)
}

fun getViewGroupDispatchDelegate(layer: String = "ViewGroup"): IDispatchDelegate {
    return DispatchDelegate(layer)
}

fun getViewDispatchDelegate(layer: String = "View"): IDispatchDelegate {
    return DispatchDelegate(layer)
}
Copy the code

You can see the printed event distribution trace log:

[down]
|layer:SActivity |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Dispatch_BE |type:down
|layer:SViewGroup |on:Intercept_BE |type:down
|layer:SViewGroup |on:Intercept_AF |result(super):false |type:down
|layer:SView |on:Dispatch_BE |type:down
|layer:SView |on:Touch_BE |type:down
|layer:SView |on:Touch_AF |result(super):false |type:down
|layer:SView |on:Dispatch_AF |result(super):false |type:down
|layer:SViewGroup |on:Touch_BE |type:down
|layer:SViewGroup |on:Touch_AF |result(super):false |type:down
|layer:SViewGroup |on:Dispatch_AF |result(super):false |type:down
|layer:SActivity |on:Touch_BE |type:down
|layer:SActivity |on:Touch_AF |result(super):false |type:down
|layer:SActivity |on:Dispatch_AF |result(super):false |type:down
 
[move]
|layer:SActivity |on:Dispatch_BE |type:move
|layer:SActivity |on:Touch_BE |type:move
|layer:SActivity |on:Touch_AF |result(super):false |type:move
|layer:SActivity |on:Dispatch_AF |result(super):false |type:move

[move]
...
 
[up]
|layer:SActivity |on:Dispatch_BE |type:up
|layer:SActivity |on:Touch_BE |type:up
|layer:SActivity |on:Touch_AF |result(super):false |type:up
|layer:SActivity |on:Dispatch_AF |result(super):false |type:up
Copy the code
  1. Because the system control and sparrow control print log exactly the same, so only a post
  2. Here withBEOn behalf ofbeforeIs used when the method starts processing eventsAFOn behalf ofafter, when the method finishes processing the event and prints the result of the processing
  3. It is clear from the logs that whenViewandViewGroupDon’t consumeDOWNEvent, subsequent events are no longer passed toViewandViewGroup

Scenario 2

Reconfigure the policy to simulate the scenario where both the View and ViewGroup consume events and the ViewGroup thinks it needs to intercept events on the second MOVE event:

fun getActivityDispatchDelegate(layer: String = "Activity"): IDispatchDelegate {
    return DispatchDelegate(layer)
}

fun getViewGroupDispatchDelegate(layer: String = "ViewGroup"): IDispatchDelegate {
    return DispatchDelegate(
        layer,
        ALL_SUPER,
        In the onInterceptTouchEvent method, the DOWN event returns false, the first MOVE event returns false, and the second and third MOVE events return true
        EventsReturnStrategy(T_FALSE, arrayOf(T_FALSE, T_TRUE, T_TRUE), T_SUPER), 
        ALL_TRUE
    )
}

fun getViewDispatchDelegate(layer: String = "View"): IDispatchDelegate {
    return DispatchDelegate(layer, ALL_SUPER, ALL_SUPER, ALL_TRUE)
}
Copy the code

You can see the printed event distribution trace log:

[down] |layer:SActivity |on:Dispatch_BE |type:down |layer:SViewGroup |on:Dispatch_BE |type:down |layer:SViewGroup |on:Intercept |result(false):false |type:down |layer:SView |on:Dispatch_BE |type:down |layer:SView |on:Touch |result(true):true |type:down |layer:SView |on:Dispatch_AF |result(super):true |type:down |layer:SViewGroup |on:Dispatch_AF |result(super):true |type:down |layer:SActivity |on:Dispatch_AF |result(super):true |type:down [move] |layer:SActivity |on:Dispatch_BE |type:move |layer:SViewGroup |on:Dispatch_BE |type:move |layer:SViewGroup |on:Intercept  |result(false):false |type:move |layer:SView |on:Dispatch_BE |type:move |layer:SView |on:Touch |result(true):true |type:move |layer:SView |on:Dispatch_AF |result(super):true |type:move |layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move |layer:SActivity |on:Dispatch_AF |result(super):true |type:move [move] |layer:SActivity |on:Dispatch_BE |type:move |layer:SViewGroup |on:Dispatch_BE |type:move |layer:SViewGroup |on:Intercept |result(true):true |type:move |layer:SView |on:Dispatch_BE |type:cancel |layer:SView |on:Touch_BE |type:cancel |layer:SView |on:Touch_AF |result(super):false |type:cancel |layer:SView |on:Dispatch_AF |result(super):false |type:cancel |layer:SViewGroup |on:Dispatch_AF |result(super):false |type:move |layer:SActivity |on:Touch_BE |type:move |layer:SActivity |on:Touch_AF |result(super):false |type:move |layer:SActivity |on:Dispatch_AF |result(super):false |type:move [move] |layer:SActivity |on:Dispatch_BE |type:move |layer:SViewGroup |on:Dispatch_BE |type:move |layer:SViewGroup |on:Touch |result(true):true |type:move |layer:SViewGroup |on:Dispatch_AF |result(super):true |type:move |layer:SActivity |on:Dispatch_AF |result(super):true |type:move [up] |layer:SActivity |on:Dispatch_BE |type:up |layer:SViewGroup |on:Dispatch_BE |type:up |layer:SViewGroup |on:Touch |result(true):true |type:up |layer:SViewGroup |on:Dispatch_AF |result(super):true |type:up |layer:SActivity |on:Dispatch_AF |result(super):true |type:upCopy the code
  1. Also because the system control and sparrow control print log exactly the same, so only a post
  2. As you can clearly see from the logs, theViewGroupHow are events distributed before and after interception

2.3. Test results

In addition to the above scene, I also simulated other complex scene, can see the system control and sparrow control print log exactly the same, which shows that the sparrow control event distribution logic, is indeed consistent with the system source code.

In addition, you can clearly see the event distribution track from the printed logs, which is helpful to understand the event distribution process. So if you want to, you can just use this framework to debug all sorts of touch event distributions like this.

Practice 3.

In practice, there are two aspects to event distribution:

  1. One is to control the distribution of events. This is also the main content of this article
  2. Another aspect is the handling of events. The core content is gesture recognition, such as the recognition of the user’s operation is click, double click, long press, slide, this part can also be handwritten, not too difficult, but we can use the SDK to provide a very useful help class in general scenesGestureDetectorIt is very convenient to use

For the time being, let’s go directly to another lens, See Through > NestedScrolling mechanism, which provides a decent practice scenario.

(If you think it will help you, please click “like” and go on.)

4. The appendix

4.1. Event distribution classical pseudocode

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean consume = false;
    if (onInterceptTouchEvent(event)) {
        consume = onTouchEvent(event);
    } else {
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
Copy the code