Android View event distribution mechanism source code analysis (I)

Source: Internet
Author: User

Android View event distribution mechanism source code analysis (I)

I have always wanted to write an article about the event distribution mechanism. Whatever the case, I have to study the source code of event distribution and write my own experiences ~

First, let's write a simple example to test the event forwarding process of the View ~

1. Case studies

To better study View event forwarding, We customize a MyButton to inherit the Button, then rewrite the methods related to event propagation, and add the log ~

MyButton

package com.example.zhy_event03;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.Button;public class MyButton extends Button{private static final String TAG = MyButton.class.getSimpleName();public MyButton(Context context, AttributeSet attrs){super(context, attrs);}@Overridepublic boolean onTouchEvent(MotionEvent event){int action = event.getAction();switch (action){case MotionEvent.ACTION_DOWN:Log.e(TAG, "onTouchEvent ACTION_DOWN");break;case MotionEvent.ACTION_MOVE:Log.e(TAG, "onTouchEvent ACTION_MOVE");break;case MotionEvent.ACTION_UP:Log.e(TAG, "onTouchEvent ACTION_UP");break;default:break;}return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent event){int action = event.getAction();switch (action){case MotionEvent.ACTION_DOWN:Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");break;case MotionEvent.ACTION_MOVE:Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");break;case MotionEvent.ACTION_UP:Log.e(TAG, "dispatchTouchEvent ACTION_UP");break;default:break;}return super.dispatchTouchEvent(event);}}

Logs are printed in onTouchEvent and dispatchTouchEvent ~

Then add the Custom button to the main layout file;

Layout file:

     
  
 

Finally, let's take a look at the MainActivity code.

package com.example.zhy_event03;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnTouchListener;import android.widget.Button;public class MainActivity extends Activity{protected static final String TAG = "MyButton";private Button mButton ;@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mButton = (Button) findViewById(R.id.id_btn);mButton.setOnTouchListener(new OnTouchListener(){@Overridepublic boolean onTouch(View v, MotionEvent event){int action = event.getAction();switch (action){case MotionEvent.ACTION_DOWN:Log.e(TAG, "onTouch ACTION_DOWN");break;case MotionEvent.ACTION_MOVE:Log.e(TAG, "onTouch ACTION_MOVE");break;case MotionEvent.ACTION_UP:Log.e(TAG, "onTouch ACTION_UP");break;default:break;}return false;}});}}

In MainActivity, we also set the OnTouchListener listener for MyButton ~

Okay, generally there are three places related to the View event, one onTouchEvent, one dispatchTouchEvent, and one setOnTouchListener;

Run the following command and click the button to view the log output:

08-31 06:09:39.030: E/MyButton(879): dispatchTouchEvent ACTION_DOWN08-31 06:09:39.030: E/MyButton(879): onTouch ACTION_DOWN08-31 06:09:39.049: E/MyButton(879): onTouchEvent ACTION_DOWN08-31 06:09:39.138: E/MyButton(879): dispatchTouchEvent ACTION_MOVE08-31 06:09:39.138: E/MyButton(879): onTouch ACTION_MOVE08-31 06:09:39.147: E/MyButton(879): onTouchEvent ACTION_MOVE08-31 06:09:39.232: E/MyButton(879): dispatchTouchEvent ACTION_UP08-31 06:09:39.248: E/MyButton(879): onTouch ACTION_UP08-31 06:09:39.248: E/MyButton(879): onTouchEvent ACTION_UP

I rubbed it when I clicked it intentionally, otherwise the MOVE will not be triggered, and the hand shake may print a bunch of MOVE logs ~~~

Now, we can see that, whether it is DOWN, MOVE, or UP, it will be executed in the following order:

1. dispatchTouchEvent

2. setOnTouchListener onTouch

3. onTouchEvent

The following describes how to start source code exploration following the log steps ~

2. dispatchTouchEvent

First, go to View's dispatchTouchEvent

/**     * Pass the touch screen motion event down to the target view, or this     * view if it is the target.     *     * @param event The motion event to be dispatched.     * @return True if the event was handled by the view, false otherwise.     */    public boolean dispatchTouchEvent(MotionEvent event) {        if (!onFilterTouchEventForSecurity(event)) {            return false;        }        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }

Look at 13 rows: first, judge that mOnTouchListener is not null, and view is enable, then mOnTouchListener. onTouch (this, event) returns true. If all three conditions are met, return true directly; that is, the following onTouchEvent will not be executed;

Then mOnTouchListener is and Fang shenghuo. Let's take a look:

   /**     * Register a callback to be invoked when a touch event is sent to this view.     * @param l the touch listener to attach to this view     */    public void setOnTouchListener(OnTouchListener l) {        mOnTouchListener = l;    }
It is actually the setOnTouchListener we set in the Activity.

That is to say: if setOnTouchListener is set and return true, the onTouchEvent of the View itself will not be executed. Of course, in this example, we return false, and we have to explore it further;

A common problem has been solved: The call relationship between onTouchListener and onTouchEvent of View should be understood ~ Let's go; continue.

3. onTouchEvent of the View:

Next is the onTouchEvent of the View:

/**     * Implement this method to handle touch screen motion events.     *     * @param event The motion event.     * @return True if the event was handled, false otherwise.     */    public boolean onTouchEvent(MotionEvent event) {        final int viewFlags = mViewFlags;        if ((viewFlags & ENABLED_MASK) == DISABLED) {            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return (((viewFlags & CLICKABLE) == CLICKABLE ||                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));        }        if (mTouchDelegate != null) {            if (mTouchDelegate.onTouchEvent(event)) {                return true;            }        }        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            switch (event.getAction()) {                case MotionEvent.ACTION_UP:                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;                    if ((mPrivateFlags & PRESSED) != 0 || prepressed) {                        // take focus if we don't have it already and we should in                        // touch mode.                        boolean focusTaken = false;                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {                            focusTaken = requestFocus();                        }                        if (!mHasPerformedLongPress) {                            // This is a tap, so remove the longpress check                            removeLongPressCallback();                            // Only perform take click actions if we were in the pressed state                            if (!focusTaken) {                                // Use a Runnable and post this rather than calling                                // performClick directly. This lets other visual state                                // of the view update before click actions start.                                if (mPerformClick == null) {                                    mPerformClick = new PerformClick();                                }                                if (!post(mPerformClick)) {                                    performClick();                                }                            }                        }                        if (mUnsetPressedState == null) {                            mUnsetPressedState = new UnsetPressedState();                        }                        if (prepressed) {                            mPrivateFlags |= PRESSED;                            refreshDrawableState();                            postDelayed(mUnsetPressedState,                                    ViewConfiguration.getPressedStateDuration());                        } else if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                        removeTapCallback();                    }                    break;                case MotionEvent.ACTION_DOWN:                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPrivateFlags |= PREPRESSED;                    mHasPerformedLongPress = false;                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                    break;                case MotionEvent.ACTION_CANCEL:                    mPrivateFlags &= ~PRESSED;                    refreshDrawableState();                    removeTapCallback();                    break;                case MotionEvent.ACTION_MOVE:                    final int x = (int) event.getX();                    final int y = (int) event.getY();                    // Be lenient about moving outside of buttons                    int slop = mTouchSlop;                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||                            (y < 0 - slop) || (y >= getHeight() + slop)) {                        // Outside button                        removeTapCallback();                        if ((mPrivateFlags & PRESSED) != 0) {                            // Remove any future long press/tap checks                            removeLongPressCallback();                            // Need to switch from pressed to not pressed                            mPrivateFlags &= ~PRESSED;                            refreshDrawableState();                        }                    }                    break;            }            return true;        }        return false;    }

The code is still relatively long,

10-15 rows. If the current View is in the Disabled status and can be clicked, the event will be consumed (return true); can be ignored, not our focus;

Line 17-21: If mTouchDelegate is set, the event will be handed over to the agent for processing and return true directly. If you want your View to increase its touch range, you can try TouchDelegate, this is not an important point. You can ignore it;

Next we will focus on:

Row 23: IF our View can be clicked or long-pressed, pay attention to the IF range and finally return true;

If (viewFlags & CLICKABLE) = CLICKABLE |
(ViewFlags & LONG_CLICKABLE) = LONG_CLICKABLE )){
//...
Return true;
}

Next, the switch (event. getAction () determines the event type, DOWN, MOVE, UP, and so on;

We will first read case MotionEvent. ACTION_DOWN (lines-78) according to the execution order of the example ):

1. MotionEvent. ACTION_DOWN

Line 75: Set a PREPRESSED identifier for mPrivateFlags.

Row 76: Set mHasPerformedLongPress to false, indicating that the long-press event has not been triggered;

Row 77: Send a delayed message with a delay of ViewConfiguration. getTapTimeout (). After the delay is reached, the run method in CheckForTap () will be executed:

1. ViewConfiguration. getTapTimeout () is 115 ms;

2. CheckForTap

  private final class CheckForTap implements Runnable {        public void run() {            mPrivateFlags &= ~PREPRESSED;            mPrivateFlags |= PRESSED;            refreshDrawableState();            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {                postCheckForLongClick(ViewConfiguration.getTapTimeout());            }        }    }

In the run method, cancel the mPrivateFlags PREPRESSED and set the pressed id to refresh the background. If the View supports long-PRESSED events, a delayed message is sent, and the detection is long-PRESSED;

 private void postCheckForLongClick(int delayOffset) {        mHasPerformedLongPress = false;        if (mPendingCheckForLongPress == null) {            mPendingCheckForLongPress = new CheckForLongPress();        }        mPendingCheckForLongPress.rememberWindowAttachCount();        postDelayed(mPendingCheckForLongPress,                ViewConfiguration.getLongPressTimeout() - delayOffset);    }

class CheckForLongPress implements Runnable {        private int mOriginalWindowAttachCount;        public void run() {            if (isPressed() && (mParent != null)                    && mOriginalWindowAttachCount == mWindowAttachCount) {                if (performLongClick()) {                    mHasPerformedLongPress = true;                }            }        }

As you can see, when you press the button, the user will first set the identifier to PREPRESSED. If it is lifted UP within 115 milliseconds, The CheckForTap callback will be removed when it is UP (it will be analyzed when it is UP );

If no value is raised after 115, the View identifier is set to PRESSED and the PREPRESSED identifier is removed. Then, a latency task is triggered to detect long-PRESSED threads. The latency is ViewConfiguration. getLongPressTimeout ()-delayOffset (500 ms-115 ms), this 115ms is used to detect the amount of PREPRESSED time, that is, the user starts from the trigger of DOWN, if it is not lifted within ms, it is deemed that the long-press event is triggered:

1. If a long-pressed callback is set at this time, the long-pressed callback is performed on time, and true is returned if the long-pressed callback is set to true;

2. Otherwise, if the value returned by the long-pressed callback or long-pressed callback is not set to false, mhasprovided medlongpress is still false;

Now the analysis is complete. Let's go back to the gods and return to ACTION_MOVE in onTouchEvent of VIEW:

2. MotionEvent. ACTION_MOVE

Lines 86-105:

Line 8-88: Get the x and y coordinates of the current touch;

Line 91 determines whether the touch point has been removed from our View. If it has been removed:

1. Execute removeTapCallback ();

2. then determine whether or not the pressed id is included. If so, remove the long-PRESSED check: removeLongPressCallback ();

3. Remove the PRESSED mark in mPrivateFlags and refresh the background;

 private void removeTapCallback() {        if (mPendingCheckForTap != null) {            mPrivateFlags &= ~PREPRESSED;            removeCallbacks(mPendingCheckForTap);        }    }
This is the removal of the PREPRESSED detection set during the DOWN Trigger; that is, when the current trigger time does not reach Ms when the DOWN Trigger is triggered, you have already removed it from the control;

If you are not removed from the control after 115ms, your current mPrivateFlags must be PRESSED and the long-PRESSED detection is sent;

The removeLongPressCallback () is removed first ()
Private void removeLongPressCallback (){
If (mPendingCheckForLongPress! = Null ){
RemoveCallbacks (mPendingCheckForLongPress );
}
}

Remove the pressed id from mPrivateFlags and refresh the background;

Okay, MOVE. We have also completed the analysis. To sum up, as long as the user has removed our control, the mPrivateFlags will be taken out of the pressed id, and all the detection and long-PRESSED settings set in the DOWN will be removed;

Next, let's go back to the ACTION_UP of onTouchEvent of View:

3. MotionEvent. ACTION_UP

26 to 69 rows:

Line 27: Determine whether mPrivateFlags contains PREPRESSED

Row 28: if it contains PRESSED or PREPRESSED, it enters the execution body, that is, whether it is within 115ms or after being lifted, it will enter the execution body.

Row 36: IF mhas#medlongpress is not executed, enter IF

Row 38: removeLongPressCallback (); remove long-pressed Detection

Lines 45-50: If mPerformClick is set to null, initialize an instance and add it to the end of the message queue through handler immediately. If the addition fails, execute performClick, in the run method of mPerformClick, it is to execute partial mclick ();

Finally, our click event is executed. Let's take a look at the merge mclick () method:

 public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        if (mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            mOnClickListener.onClick(this);            return true;        }        return false;    }

If (mOnClickListener! = Null ){
MOnClickListener. onClick (this );
Return true;
}

Long overdue ~ Our mOnClickListener;

Don't be excited. It's not over yet. Return to ACTION_UP,

Row 58: IF prepressed is true, enter the IF body:

Set mPrivateFlags to PRESSED, refresh the background, and run mUnsetPressedState 125 milliseconds later.

Otherwise: mUnsetPressedState. run (); execute immediately; that is, mUnsetPressedState. run () will be executed at last;

Let's see what this UnsetPressedState mainly does:

Private final class UnsetPressedState implements Runnable {
Public void run (){
SetPressed (false );
}
}

Public void setPressed (boolean pressed ){
If (pressed ){
MPrivateFlags | = PRESSED;
} Else {
MPrivateFlags & = ~ PRESSED;
}
RefreshDrawableState ();
DispatchSetPressed (pressed );
}

Cancel the PRESSED in our mPrivateFlags, refresh the background, and forward setPress.

The last row of ACTION_UP: removeTapCallback (). If mPendingCheckForTap is not null, remove it;

4. Summary

Well, the Code span is still quite large. The following should be summarized:

1. The entire View event forwarding process is:

View. dispatchEvent-> View. setOnTouchListener-> View. onTouchEvent

In dispatchTouchEvent, OnTouchListener is judged. If OnTouchListener is not null and returns true, the event is consumed and onTouchEvent is not executed. Otherwise, onTouchEvent is executed.

2. DOWN, MOVE, UP in onTouchEvent

When DOWN:

A. First, set the flag to PREPRESSED, set mHasPerformedLongPress to false, and then send an mPendingCheckForTap after 115ms. if you raise your finger within 115ms and trigger UP, the click event is not triggered, in addition, the final execution is the UnsetPressedState object, and setPressed (false) will pass the setPress; this rarely happens and may only find that the click event cannot be triggered during stress testing;

B. If UP is not triggered within 115ms, the flag is set to PRESSED, The PREPRESSED flag is cleared, and a message with a latency of-MS is sent to detect long-PRESSED task messages;

C. If within ms (starting from the DOWN Trigger), The LongClickListener is triggered:

At this time, if LongClickListener is not null, a callback is executed. If LongClickListener. onClick returns true, mhas#medlongpress is set to true; otherwise, mhas#medlongpress is still false;

MOVE:

It mainly checks whether the user has drawn out the control. If it is:

Directly remove mPendingCheckForTap within 115ms;

After 115ms, remove the PRESSED in the flag and remove the long-PRESSED check: removeLongPressCallback ();

When UP:

A. If UP is triggered within 115ms and the flag is PREPRESSED, UnsetPressedState and setPressed (false) are executed. setPress is forwarded and can be received by repeatedly writing the dispatchSetPressed method in View;

B. if the data is between 115ms-500ms, that is, long-pressed data has not yet occurred, remove the long-pressed data and perform the onClick callback;

C. If it is Ms later, there are two situations:

I. If onLongClickListener is set and onLongClickListener. onClick returns true, The OnClick event cannot be triggered;

Ii. If onLongClickListener is not set or onLongClickListener. onClick returns false, The OnClick event can still be triggered;

D. Execute mUnsetPressedState. run (), pass setPressed, and then remove the expression;


Finally, let's ask a question, and then run another example to end:

1. Can setOnLongClickListener and setOnClickListener only execute one

No, as long as the onClick in setOnLongClickListener returns false, both will be executed. If the return value is true, the setOnClickListener will be displayed.

Finally, set setOnClickListener and setOnLongClickListener for MyButton at the same time. Run the following command:

package com.example.zhy_event03;import android.app.Activity;import android.os.Bundle;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.view.View.OnClickListener;import android.view.View.OnLongClickListener;import android.view.View.OnTouchListener;import android.widget.Button;import android.widget.Toast;public class MainActivity extends Activity{protected static final String TAG = "MyButton";private Button mButton ;@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mButton = (Button) findViewById(R.id.id_btn);mButton.setOnTouchListener(new OnTouchListener(){@Overridepublic boolean onTouch(View v, MotionEvent event){int action = event.getAction();switch (action){case MotionEvent.ACTION_DOWN:Log.e(TAG, "onTouch ACTION_DOWN");break;case MotionEvent.ACTION_MOVE:Log.e(TAG, "onTouch ACTION_MOVE");break;case MotionEvent.ACTION_UP:Log.e(TAG, "onTouch ACTION_UP");break;default:break;}return false;}});mButton.setOnClickListener(new OnClickListener(){@Overridepublic void onClick(View v){Toast.makeText(getApplicationContext(), "onclick",Toast.LENGTH_SHORT).show();}});mButton.setOnLongClickListener(new OnLongClickListener(){@Overridepublic boolean onLongClick(View v){Toast.makeText(getApplicationContext(), "setOnLongClickListener",Toast.LENGTH_SHORT).show();return false;}});}}
:


We can see that LongClickListener has been triggered by ClickListener ~


Finally, this blog post describes the entire process of the View event distribution mechanism and analyzes the source code;










Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.