Read Android TouchEvent Event Event Distribution, Interception, Processing

Posted by jrd on Mon, 09 Sep 2019 09:39:15 +0200

What is an event? Events are a series of TouchEvent s caused by the user touching the screen of the mobile phone, including ACTION_DOWN, ACTION_MOVE, ACTION_UP, ACTION_CANCEL, etc. These actions are combined into click events, long press events and so on.

In this article, we use Log testing to understand the distribution, interception and processing of Android TouchEvent events. Although I have read some other articles and source codes and related information, I still feel that I need to lay down logs and drawings to understand it, otherwise it is easy to forget the whole process of event transmission. So write down this article, so that after reading this article, you can basically understand the whole process, and you can draw your own pictures for others to see.

First look at several classes, mainly draw a superimposed interface of three ViewGroup s, and lay logs in event distribution, interception and processing.

GitHub address: https://github.com/libill/TouchEventDemo

I. Analyzing Event Distribution by Logging

Here, three ViewGroup s are added to an activity to analyze. It is noteworthy that there is no onInterceptTouchEvent method for Activity and View.

I. Understanding Activity, ViewGroup 1, ViewGroup 2, ViewGroup 3

  1. activity_main.xml

     <?xml version="1.0" encoding="utf-8"?>
         <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:context="com.touchevent.demo.MyActivity">
         <com.touchevent.demo.ViewGroup1
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:background="@color/colorAccent">
         <com.touchevent.demo.ViewGroup2
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_margin="50dp"
             android:background="@color/colorPrimary">
             <com.touchevent.demo.ViewGroup3
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:layout_margin="50dp"
                 android:background="@color/colorPrimaryDark">
             </com.touchevent.demo.ViewGroup3>
         </com.touchevent.demo.ViewGroup2>
         </com.touchevent.demo.ViewGroup1>
     </android.support.constraint.ConstraintLayout>  
  2. Main interface: MainActivity.java

     public class MyActivity extends AppCompatActivity {
         private final static String TAG = MyActivity.class.getName();
    
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
         }
    
         @Override
         public boolean dispatchTouchEvent(MotionEvent ev) {
             Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
             boolean superReturn = super.dispatchTouchEvent(ev);
             Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
             return superReturn;
         }
    
         @Override
         public boolean onTouchEvent(MotionEvent ev) {
             Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
             boolean superReturn = super.onTouchEvent(ev);
             Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
             return superReturn;
         }
     }
  3. The three ViewGroups have exactly the same code: ViewGroup1.java, ViewGroup2.java, and ViewGroup3.java. Because the code is the same, only one class is posted.

     public class ViewGroup1 extends LinearLayout {
         private final static String TAG = ViewGroup1.class.getName();
    
         public ViewGroup1(Context context) {
             super(context);
         }
    
         public ViewGroup1(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
    
         @Override
         public boolean dispatchTouchEvent(MotionEvent ev) {
             Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
             boolean superReturn = super.dispatchTouchEvent(ev);
             Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
             return superReturn;
         }
    
         @Override
         public boolean onInterceptTouchEvent(MotionEvent ev) {
             Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
             boolean superReturn = super.onInterceptTouchEvent(ev);
             Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
             return superReturn;
         }
    
         @Override
         public boolean onTouchEvent(MotionEvent ev) {
             Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
             boolean superReturn = super.onTouchEvent(ev);
             Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
             return superReturn;
         }
     }

2. Do not intercept and handle any incidents

Add code that does not intercept any events to see how they are passed. Select Info and view Log.

As can be seen from the flow chart, event distribution starts with Activity and then distributes to ViewGroup. In this process, as long as ViewGroup is not intercepted, it will eventually return to the onTouchEvent method of Activity.

3. ViewGroup 2 dispatch TouchEvent returns true

Modify ViewGroup2.java's dispatchTouchEvent to return true so that events are not distributed

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
 Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
 Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
 return true;
}

Log at this time

As can be seen from the picture, when the dispatch TouchEvent of ViewGroupon2 returns to true, the event will not be redistributed to ViewGroup3, nor will it be distributed to the onTouchEvent of Activity. Instead, the event stopped after the dispatch TouchEvent of ViewGroup 2. The return of dispatch TouchEvent to true indicates that the event is no longer distributed.

IV. onInterceptTouchEvent of ViewGroup 2 returns true

Modify ViewGroup2.java's onInterceptTouchEvent and return true to intercept the event

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.i(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev));
    boolean superReturn = super.dispatchTouchEvent(ev);
    Log.d(TAG, "dispatchTouchEvent    action:" + StringUtils.getMotionEventName(ev) + " " + superReturn);
    return superReturn;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

Log at this time


As you can see, ViewGroup2 intercepts events and will not continue to distribute them to ViewGroup3; moreover, ViewGroup3 intercepts events and does not process them, passing them to the onTouchEvent method of Activity.

5. onInterceptTouchEvent and onTouchEvent of ViewGroup 2 return true

Modify onTouchEvent in ViewGroup2.java, return to true and process the event

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onInterceptTouchEvent action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    Log.i(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev));
    Log.d(TAG, "onTouchEvent          action:" + StringUtils.getMotionEventName(ev) + " " + true);
    return true;
}


It can be concluded from the process that when onInterceptTouchEvent and onTouchEvent of ViewGroup 2 return to true, the event will eventually go to the onTouchEvent method of ViewGroup 2 to handle the event, and subsequent events will come here.

It's clear from log analysis. Is that enough? In fact, not yet, but also from the point of view of the source code to analyze why the event will be distributed in this way.

2. Distribution of Events through Source Code Analysis

I. Activity's dispatch TouchEvent

First look at dispatch TouchEvent under Activity

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

onUserInteraction method

public void onUserInteraction() {
}

You can see from the code

  1. Invoke the Activity's onUserInteraction method, which goes in when action is down, but this is an empty method that does nothing and can be ignored.

  2. Call the superDispatchTouchEvent method of window s, and when true is returned, the event distribution process ends, otherwise the onTouchEvent method of Activity will be called.

  3. The onTouchEvent method of Activity is called, and the method entering this condition is the superDispatchTouchEvent method of Windows that returns false. From the above analysis (2, do not intercept and handle any events), we can know that all the dispatch TouchEvent, onIntercept TouchEvent and onTouchEvent of the sub-View will mobilize the onTouchEvent method of Activity when they return to false. At this time, it will also make the super Dispatch TouchEvent method of window s return to false.

2. Windows Super Dispatch TouchEvent

Activity's getWindow Method

public Window getWindow() {
    return mWindow;
}

How does mWindow s assign values?
It is assigned in the attach method of Activity. In fact, mWindow is PhoneWindow.

Attachment Method of Activity

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    ...
}

Phone Windows Super Dispatch TouchEvent Method

private DecorView mDecor;

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

DevorView's Super Dispatch TouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

mDecor is a DecorView that inherits FrameLayout and distributes events to ViewGroup.

3. ViewGroup's dispatch TouchEvent

3.1 ViewGroup intercepts events

        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

There are two situations to determine whether interception is needed, that is, when a condition is established, onInterceptTouchEvent is executed to determine whether interception events are needed.

  1. When actionMasked = MotionEvent. ACTION_DOWN.
  2. When mFirstTouchTarget!= null. MFirstTouchTarget is a sub-View of ViewGroup that successfully handles events. That is, when the sub-View of ViewGroup returns true in the following cases, this can be easily obtained in the log analysis flow chart:

    2.1 dispatch TouchEvent returns true

    2.2 If the child View is ViewGroup, onInterceptTouchEvent and onTouchEvent return true

Another case is that when disallowIntercept is true, intercepted directly assigns false without interception. FLAG_DISALLOW_INTERCEPT is set up by request Disallow Intercept TouchEvent method, which is used to set up in sub-View. After setting, ViewGroup can only intercept down events, but can not intercept other move, up and cancel events. Why can ViewGroup intercept down events? Because ViewGroup resets during the down event, look at the following code

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

As you can see from the source code, after the ViewGroup intercepts the event, it no longer calls onInterceptTouchEvent, but handles it directly to onTouchEvent of mFirstTouchTarget. If the onTouchEvent does not process, it will eventually be handed to onTouchEvent of Activity.

3.2 ViewGroup does not intercept events

When ViewGroup does not intercept an event, it traverses the child View to distribute the event to the child View for processing.

final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

    // If there is a view that has accessibility focus we want it
    // to get the event first and if not handled we will perform a
    // normal dispatch. We may do a double iteration but this is
    // safer given the timeframe.
    if (childWithAccessibilityFocus != null) {
        if (childWithAccessibilityFocus != child) {
            continue;
        }
        childWithAccessibilityFocus = null;
        i = childrenCount - 1;
    }

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }

    newTouchTarget = getTouchTarget(child);
    if (newTouchTarget != null) {
        // Child is already receiving touch within its bounds.
        // Give it the new pointer in addition to the ones it is handling.
        newTouchTarget.pointerIdBits |= idBitsToAssign;
        break;
    }

    resetCancelNextUpFlag(child);
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        // Child wants to receive touch within its bounds.
        mLastTouchDownTime = ev.getDownTime();
        if (preorderedList != null) {
            // childIndex points into presorted list, find original index
            for (int j = 0; j < childrenCount; j++) {
                if (children[childIndex] == mChildren[j]) {
                    mLastTouchDownIndex = j;
                    break;
                }
            }
        } else {
            mLastTouchDownIndex = childIndex;
        }
        mLastTouchDownX = ev.getX();
        mLastTouchDownY = ev.getY();
        newTouchTarget = addTouchTarget(child, idBitsToAssign);
        alreadyDispatchedToNewTouchTarget = true;
        break;
    }
}
3.2.1 Find Sub View s of Receivable Events

canViewReceivePointerEvents is used to determine whether the sub-View can receive click events. There must be two situations, one is indispensable: 1. The coordinates of click events fall in the area of sub-View; 2. The sub-View is not playing animation. When the condition is satisfied, the dispatch Transformed TouchEvent is also called as the dispatch TouchEvent of the sub-View.

private static boolean canViewReceivePointerEvents(@NonNull View child) {
    return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
            || child.getAnimation() != null;
}

protected boolean isTransformedTouchPointInView(float x, float y, View child,
        PointF outLocalPoint) {
    final float[] point = getTempPoint();
    point[0] = x;
    point[1] = y;
    transformPointToViewLocal(point, child);
    final boolean isInView = child.pointInView(point[0], point[1]);
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0], point[1]);
    }
    return isInView;
}

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    // Done.
    transformedEvent.recycle();
    return handled;
}

When dispatch Transformed TouchEvent returns true, it ends the for loop traversal and assigns a new TouchTarget, which is equivalent to discovering a View that can receive the event, without further searching.

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;

Apply mFirstTouchTarget to the addTouchTarget method.

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
3.2.2 ViewGroup handles events by itself

Another case is that when mFirstTouchTarget is empty, the ViewGroup handles events by itself, noting that the third parameter is null, and the super.dispatchTouchEvent of the ViewGroup will call the dispatchTouchEvent of the View.

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
}

3.3 View Processing of Click Events

How does View's dispatch TouchEvent handle events?

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}
  1. First, the onFilterTouchEventForSecurity method is used to filter touch events that do not conform to the application security policy.

     public boolean onFilterTouchEventForSecurity(MotionEvent event) {
         //noinspection RedundantIfStatement
         if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                 && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
             // Window is obscured, drop this touch.
             return false;
         }
         return true;
     }
  2. MOnTouchListener!= null judges whether OnTouchEvent is set, executes mOnTouchListener.onTouch when set and returns true, no longer executes onTouchEvent. It is concluded that OnTouchEvent has higher priority than OnTouchEvent, which makes it easy to use setOnTouchListener settings to handle click events.

  3. Another scenario is to go into onTouchEvent for processing.

     public boolean onTouchEvent(MotionEvent event) {
         final float x = event.getX();
         final float y = event.getY();
         final int viewFlags = mViewFlags;
         final int action = event.getAction();
    
         final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                 || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                 || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
         if ((viewFlags & ENABLED_MASK) == DISABLED) {
             if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                 setPressed(false);
             }
             mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
             // A disabled view that is clickable still consumes the touch
             // events, it just doesn't respond to them.
             return clickable;
         }
         ...
     }

When View is unavailable, events are still handled, but they seem unavailable.

Then execute mTouchDelegate.onTouchEvent

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

Let's see how the up event is handled.

/**
 * <p>Indicates this view can display a tooltip on hover or long press.</p>
 * {@hide}
 */
static final int TOOLTIP = 0x40000000;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_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 (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 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)) {
                            performClickInternal();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }

                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;
            ...
    }

    return true;
}

As you can see from the above code, when clickable and TOOLTIP have one true, they consume events and make onTouchEvent return true. The PerformClick method is called internally in PerformClick.

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

If View sets OnClickListener, then PerfmClick calls the internal onClick method.

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}

clickable is set by setOnClickListener and LONG_CLICKABLE by setOnLongClickListener. Set it so that onTouchEvent returns true. Here we have analyzed the distribution process of click events.

This article addresses: http://libill.github.io/2019/09/09/android-touch-event/

This article refers to the following:

1. Exploration of Android Development Art

Topics: Android Java github Windows