“Learning event distribution, will it affect my CV dafa?”

“Affecting my time with my girlfriend”

“…”

preface

The Android event distribution mechanism is now in its fourth article, and in the first three:

  • Android Event Distribution Mechanism 1: How do events arrive in an Activity? : Analyzes the overall process of event distribution and the real starting point of event distribution from the window mechanism
  • Android event distribution mechanism two: viewGroup and view on the event processing: source analysis of the viewGroup and view is how to distribute events
  • Android event distribution mechanism three: event distribution workflow: analysis of touch events in the control tree distribution process model

So the knowledge about event distribution is almost analyzed in the above three articles, and then it will analyze how to apply it to the actual development after learning, and briefly elaborate on the author’s thinking.

A View in Android generally consists of two important parts: drawing and touch feedback. How to provide accurate feedback for users’ operations is the most important goal of event distribution.

There are two common scenarios for using event distribution: setting up listeners for a view and customizing a view. Next, we will analyze these two aspects, and finally give some thoughts and summaries of the author.

The listener

Touch event listeners are our first step into the Android event system. Listeners usually have:

  • OnClickListener: Click event listener
  • OnLongClickListener: Long press event listener
  • OnTouchListener: Touch event listener

These are the most frequently used listeners, and the relationship between them is as follows:

if(mOnTouchListener! =null && mTouchListener.onTouch(event)){
    return true;
}else{
    if(Click event){monClickListener.onclick (view); }else if(long press events) {mOnLongClickListener. OnLongClick (view). }}Copy the code

The pseudo-code above makes it obvious that onTouchListener takes over the MotionEvent object directly and calls it first, while the other two listeners are called separately after the view determines the click type.

In addition, another common listener is the double-click listener, which is not supported by view by default and needs to be implemented ourselves. Double click listener implementation ideas can refer to view implementation of long press listener ideas to achieve:

When we receive a click event, we can send a click delay task. If another click event is not received by the delay time, it is a click event; If another click event is received within the delay time, it is a double click event and the delayed task is cancelled.

We can implement the view.OnClickListener interface to accomplish the above logic, the core code is as follows:

// Implement the onClickListener interface
class MyClickListener() : View.OnClickListener{
    private var isClicking = false
    private var singleClickListener : View.OnClickListener? = null
    private var doubleClickListener : View.OnClickListener? = null
    private var delayTime = 1000L
    private var clickCallBack : Runnable? = null
    private var handler : Handler = Handler(Looper.getMainLooper())

    override fun onClick(v: View?). {
        // Create a click delay task that triggers a click event when the delay expiresclickCallBack = clickCallBack? : Runnable { singleClickListener? .onClick(v) isClicking =false
        }
		// If the click has already been done once, the click is received again within the delay time
        // means this is a double click event
        if (isClicking){
            // To remove the deferred task, callback the double-click listenerhandler.removeCallbacks(clickCallBack!!) doubleClickListener? .onClick(v) isClicking =false
        }else{
            // The first click sends the delayed task
            isClicking = truehandler.postDelayed(clickCallBack!! ,delayTime) } } ... }Copy the code

The code has created a view. OnclickListener interface implementation class, and in the type of click and double-click logical judgment. We can use this class as follows:

val c = MyClickListener()
// Set the click listening event
c.setSingleClickListener(View.OnClickListener {
    Log.d(TAG, "Button: Click event")})// Set the double-click listening event
c.setDoubleClickListener(View.OnClickListener {
    Log.d(TAG, "Button: Double click event")})// Set the listener to the button
button.setOnClickListener(c)
Copy the code

This enables the double click of the button to listen.

Other types of listeners, such as triple – click, double – click, and long – press, can be implemented in this way.

The custom view

In custom views, we can use event distribution more flexibly to address actual needs. A few examples:

Sliding nesting problem: the outer layer is viewPager, the inner layer is recyclerView, to achieve sliding around switch viewPager, sliding up and down recyclerView. This is also known as the sliding conflict problem. Similar to the outer viewPager, the inner layer is also a recyclerView that can slide around. Real-time touch feedback problems: for example, design a button so that it shrinks and lowers its height when pressed, and returns to its original size and height when released, and if in a sliding container, sliding after pressed does not trigger the click event but sends the event to the outer sliding container.

As we can see, event distribution is basically used flexibly based on actual development needs. In terms of code implementation, there are three key methods: dispatchTouchEvent, onIntercepterTouchEvent and onTouchEvent. These three methods already have default implementations in the View and viewGroup, and we need to base our requirements on the default implementations. Let’s look at how several common scenarios are implemented.

Implement block press shrink

Let’s take a look at the implementation:

After the block is pressed, it will reduce the height, decrease the transparency and increase the release will be restored.

This requirement can be implemented in conjunction with property animation. The button block itself has a height and rounded corner, so we can consider inheriting cardView to implement it, overwriting cardView’s dispatchTouchEvent method, shrinking when pressed, i.e. when a Down event is received, and restoring when up and Cancel events are received. Note that the Cancel event may be ignored here, causing the button block’s state to fail to recover, and the Cancel event must be taken into account. Then look at the code implementation:

public class NewCardView extends CardView {

    // Click the event when it arrives
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // Get the event type
        int actionMarked = ev.getActionMasked();
        // Determine which method to call to display the animation based on the time type
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN :{
                clickEvent();
                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                upEvent();
                break;
            default: break;
        }
        // Call back to the default event distribution method
        return super.dispatchTouchEvent(ev);
    }

    // An event triggered when the finger is pressed; The size, height and transparency are reduced
    private void clickEvent(a){
        setCardElevation(4);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(this."scaleX".1.0.97 f),
                ObjectAnimator.ofFloat(this."scaleY".1.0.97 f),
                ObjectAnimator.ofFloat(this."alpha".1.0.9 f)); set.setDuration(100).start();
    }

    // An event triggered when the finger is lifted; Size height restored, transparency restored
    private void upEvent(a){
        setCardElevation(getCardElevation());
        AnimatorSet set = new AnimatorSet();
        set.playTogether(
                ObjectAnimator.ofFloat(this."scaleX".0.97 f.1),
                ObjectAnimator.ofFloat(this."scaleY".0.97 f.1),
                ObjectAnimator.ofFloat(this."alpha".0.9 f.1)); set.setDuration(100).start(); }}Copy the code

Animation content will not be analyzed, does not belong to the scope of this article. So you can see we’re just animating the cardView, listening for events and we can set it to the ImageView inside the cardView or we can set it directly to the cardView. Note that if you set it to cardView, you need to override the cardView intercepTouchEvent method to always return true, preventing the event from being consumed by the view without firing the listener event.

Resolving sliding conflicts

Sliding conflict is the most frequently used scenario for event distribution, and it is also a difficult and important one (knock on the blackboard, exam will be asked). There are three basic scenarios for sliding conflicts:

  • Case 1: Inside and outside view sliding direction is different, for example, viewPager nested ListView
  • Case two: inside and outside view sliding direction is the same, such as viewPager nested horizontal sliding recyclerView
  • Case 3: A combination of case 1 and case 2

There are generally two steps to solve this kind of problem: determining the final implementation effect and code implementation.

The solution of sliding conflict needs to be combined with specific implementation requirements, not a set of solutions can solve all sliding conflict problems, which is not realistic. Therefore, when solving such problems, you need to determine the final implementation effect, and then think about the code implementation in terms of that effect. I’m going to focus on case one and case two, and case three are the same.

Is a

Case one is that the inside and outside sliding direction is not consistent. The general solution for this is to determine whether to slide left or right or up or down based on the Angle of your finger sliding the line and the horizontal line:

If the Angle is less than 45 degrees, it is considered to be sliding left and right; if it is more than 45 degrees, it is considered to be sliding up and down. So now that you have the solution, think about how to code it.

The sliding Angle can be calculated from the coordinates of two consecutive MotionEvent objects, and we can then choose whether to send the event to the external container or the internal View based on the Angle. According to the location of event processing, it can be divided into internal interception and external interception.

  • External interception method: judge the sliding Angle in the viewGroup, and intercept the event if it meets its sliding direction consumption
  • Internal interception method: judge the sliding Angle in the internal view, if it is in line with its sliding direction continue to consume events, otherwise request external viewGroup interception event processing

From the perspective of implementation complexity, the external interception method will be more excellent, do not need to cooperate with the inside and outside view, just need the viewGroup itself to do a good job of event interception processing. The difference between the two is who has the initiative. Internal interception can be used if the view needs to make more judgments, while external interception is generally easier.

Now think about the code implementation for both methods.


In the external intercepting method, the focus is on whether to intercept events, so our focus is onInterceptTouchEvent. In this method, the slide Angle is calculated and the intercept is determined. Here is an example of ScrollView (external vertical slide, internal horizontal slide), code as follows:

public class MyScrollView extends ScrollView {
    // Record the coordinates of the last event
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int actionMasked = ev.getActionMasked();
        // Do not intercept the down event, otherwise the sub-view will never get the event
        // The up event cannot be intercepted, otherwise the child view's click event cannot be triggered
        if (actionMasked == MotionEvent.ACTION_DOWN || actionMasked == MotionEvent.ACTION_UP){
            lastX = ev.getX();
            lastY = ev.getY();
            return false;
        }   

        // Get the slope and judge
        float x = ev.getX();
        float y = ev.getY();
        returnMath.abs(lastX - x) < Math.abs(lastY - y); }}Copy the code

The implementation of the code is very simple, recording the position of the two touch points, and then calculate the slope to determine whether the slide is vertical or horizontal. There is one caveat in the code: viewGroups cannot intercept up and Down events. If a Down event is intercepted, the child view will never receive the event; If the up event is blocked, the child view will never trigger the click event.

The above code is the core code of event distribution. The more specific code needs to be improved according to the actual needs, but the overall logic remains unchanged.


The idea of internal interception is similar to that of external interception, except that the location of the judgment is placed in the internal view. Internal interception means that the internal view must have the ability to control the flow of events in order to process them. Here is an important method of using the internal view: requestDisallowInterceptTouchEvent.

This method forces the outer viewGroup not to intercept events. Therefore, we can make the viewGroup intercept all events except down events by default. When the child view needs to handle an event, you only need to call this method to get the event; When you want to hand an event to a viewGroup, you simply remove the flag and the outer viewGroup intercepts all events. So as to achieve the internal view control event flow direction.

Set the external viewGroup to intercept all events except the Down event (here we use viewPager and ListView to demonstrate the code) :

public class MyViewPager extends ViewPager {
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getActionMasked()==MotionEvent.ACTION_DOWN){
            return false;
        }
        return true; }}Copy the code

Next we need to override the dispatchTouchEvent method of the internal view:

public class MyListView extends ListView {
    float lastX = 0;
    float lastY = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            // The move event must not be intercepted. Otherwise, the move event cannot be detected
            case MotionEvent.ACTION_DOWN:{
                requestDisallowInterceptTouchEvent(true);
                break;
            }
            // move event to determine whether to process the event
            case MotionEvent.ACTION_MOVE:{
                float x = ev.getX();
                float y = ev.getY();
                // Handle the event yourself if the slide Angle is greater than 90 degrees
                if (Math.abs(lastY-y)<Math.abs(lastX-x)){
                    requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            default:break;
        }
        // Save the coordinates of this touch point
        lastX = ev.getX();
        lastY = ev.getY();
        // Call ListView's dispatchTouchEvent method to handle the event
        return super.dispatchTouchEvent(ev); }}Copy the code

The code ideas are basically the same, but internal interception is a bit more complex, so in general, it is better to use external interception.

So now that we’ve solved the slide conflict solution for case one, let’s look at the slide conflict solution for case two.

Case 2

The second case is that the sliding direction of the inside and outside containers is the same. There are two mainstream solutions to this case. One is that the outer container slides first, and then the outer container slides to the boundary and then the inner view, such as JINGdong APP (pay attention to the situation when sliding down) :

In the second case, the inner view slides first, and then slides the outer viewGroup after the inner view slides to the boundary, such as ele. me app (note the situation when sliding down) :

There is no one better than the other, but a specific solution needs to be determined based on specific business requirements. The second scheme mentioned above is analyzed below. The first scheme is similar.

First of all, analyze the specific effect: the sliding direction of the outer viewGroup and the inner view is the same, both are vertical sliding or horizontal sliding; To slide up, slide the viewGroup to the top, then slide the inner View; When sliding down, slide the inner view to the top and then slide the outer viewGroup.

Here we use the external interception method to achieve. First of all, let’s determine our layout:

The outer layer is a ScrollView, and the inner one is a LinearLayout, because a ScrollView can only have one view. Inside the top is a LinearLayout to hold the header layout, and underneath is a ListView. Now we need to determine the interception rule for ScrollView:

  1. When the ScrollView does not slide to the bottom, the ScrollView is handled directly
  2. When ScrollView slides to the bottom:
    • If the LinearLayout doesn’t slide to the top, it’s handed over to the ListView
    • If the LinearLayout slides to the top:
      • If you swipe up, you hand it over to the listView
      • If you’re sliding down, you hand it over to the ScrollView

Now we can determine our code:

public class MyScrollView extends ScrollView {...float lastY = 0;
    boolean isScrollToBottom = false;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        int actionMarked = ev.getActionMasked();
        switch (actionMarked){
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:{
                // These three types of events are not intercepted by default and must be handled by the child view
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                LinearLayout layout = (LinearLayout) getChildAt(0);
                ListView listView = (ListView)layout.getChildAt(1);
                // If there is no slide to the bottom, it is handled by ScrollView to intercept
                if(! isScrollToBottom){ intercept =true;
                    // If you slide to the bottom and the listView has not yet slid to the top, do not intercept
                }else if(! ifTop(listView)){ intercept =false;
                }else{
                    // Otherwise determine if it is sliding
                    intercept = ev.getY() > lastY;
                }
                break;
            }
            default:break;
        }
        // Finally record the location information
        lastY = ev.getY();
        // Call the parent interceptor method, ScrollView needs to do some work, otherwise it may not slide
        super.onInterceptTouchEvent(ev);
        returnintercept; }... }Copy the code

I also added to the code whether to slide to the bottom if there is a view under the listView. The code for judging listView sliding and scrollView sliding is as follows:

{
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        // Set the slide listener
        setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
            ViewGroup viewGroup = (ViewGroup)v;
            isScrollToBottom = v.getHeight() + scrollY >= viewGroup.getChildAt(0).getHeight(); }); }}// Check whether the listView reaches the top
private boolean ifTop(ListView listView){
    if (listView.getFirstVisiblePosition()==0){
        View view = listView.getChildAt(0);
        returnview ! =null && view.getTop() >= 0;
    }
    return false;
}
Copy the code

The final result is as follows:

This simply resolves a sliding conflict. But be aware that in real problems, there are often more complex details to deal with. The above is just an analysis of an idea to solve the sliding conflict, specific to the business, but also need to carefully polish the code. If you are interested, check out the source code for how NeatedScrollView resolves sliding conflicts.

The last

Event distribution is very important as a base of knowledge for Android. Can not say that learning the event distribution, you can directly soar. But after mastering the event distribution, in the face of some specific needs, there are certain ideas to deal with. Or when you know the source code of some framework, you know what it means.

Learning event distribution process, in-depth study of a lot of source code, some partners think it is unnecessary. There are only three major approaches to actual development, and understanding one major process is sufficient. I want to say: that’s true; But without studying the principles behind it, we can only know why and why. When some exceptions are encountered, the resulting bug cannot be analyzed from the source point of view. In the process of learning the source code, it is also a kind of communication with the author who designed the Android system. If there is no event distribution mechanism, how can I solve the distribution problem of touch information? The process of learning is to think about the solutions given by the authors of the Android system. And after mastering the principle, for the issue of event distribution, a little thinking and analysis, also come in handy. What is called:

You can only master a level 9 enemy if you defeat a level 10 enemy.

Hope this article is helpful.

How about a little thumbs-up to encourage the author?

Full text here, the original is not easy, feel help can like collection comments forward. I have no talent, any ideas welcome to comment area exchange correction. If need to reprint please comment section or private communication.

And welcome to my blog: Portal