Android Touch Event Distribution Mechanism Explains the Click-triggered War

Keywords: Android Windows Java

Share more: http://www.cherylgood.cn
- We were there before. Detailed usage of Scroller In the onMeasure method, you may see childView.setClickable(true); why set childView to true? If not, you will find that ACTION_MOVE is not executed. Why is there such a problem? At this time, I am confused, to understand thoroughly, the Android event distribution mechanism is essential.

  • Let's start with a test.

    As shown above, there is only one button in our activity

The code is as follows:

package guanaj.com.scrollerdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private Scroller scroller;
private LinearLayout llContent;
private Button mButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    mButton = (Button) findViewById(R.id.m_button);
    mButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.i(TAG,"#On Click I was clicked.
        }
    });
    mButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:{
                    Log.i(TAG,"#onTouch ACTION_DOWN ");
                    break;
                }
                case MotionEvent.ACTION_MOVE:{
                    Log.i(TAG,"#onTouch ACTION_MOVE ");
                    break;
                }
                case MotionEvent.ACTION_UP:{
                    Log.i(TAG,"#onTouch ACTION_UP ");
                    break;
                }
                default:{
                    Log.i(TAG,"#onTouch");
                }
            }
            return false;
        }
    });
}

Then we click:

04-18 16:23:20.022 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:23:20.052 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.062 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.082 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.152 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.162 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.172 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP
04-18 16:23:20.192 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onClick I got clicked

  • -|| The hand shook and several MOVE s appeared.
  • From the log we can get the following information:
    1. OnTouch events take precedence over OnClick events, that is, click events are passed to OnTouch first, and then to OnClick.
    2. ACTION_DOWN and ACTION_UP will only occur once, that is, when the finger depresses ACTION_DOWN and the finger loosens ACTION_UP, it happens that the hand trembles and multiple ACTION_MOVE s appear.
    3. OnClick events are triggered only after ACTION_UP.

You may find that the onClick callback method does not return a value, and the OnTouch callback method returns a false value? I tried to turn false into true... ... Something muddled happened.

log

04-18 16:35:42.672 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:35:42.702 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.722 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.732 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.762 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP

In order to satisfy my little curiosity, I went to the Internet to check some information and the source code.

Source chase war begins:

1. First of all, we start with the mechanism of android system: our buttons are in the activity, and how did our activity come from? Here we will introduce a class, Window: Official Description:

  • Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc. The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
  • An abstract base class that defines top-level window appearance and behavior policies. Instances of this class should be used as top-level views added to the window manager. It provides standard UI policies, such as background, Title area, default key processing, etc. The only existing implementation of this abstract class is android.view.PhoneWindow, which should be instantiated when you need a window.

2. Clue shifting. Now we start tracking PhoenWindow until Activity.java. At this time, we can see the following code in Activity.java's source code:

public class Activity extends ContextThemeWrapper
    implements LayoutInflater.Factory2,
    Window.Callback, KeyEvent.Callback,
    OnCreateContextMenuListener, ComponentCallbacks2,
    Window.OnWindowDismissedCallback, WindowControllerCallback {

When you see the word "Window s", it feels like it should be him, and then you can see from the attach function:

  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) {
    attachBaseContext(context);

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

    mWindow = new PhoneWindow(this, window);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);

CONCLUSION: PhoneWindow is actually created after the activity is created. PhoneWindow exists as the top class of the activity window, and the content displayed by the activity is also set through the PhoneWindow. At this time, you can recall the setContentView(R.layout.activity_main) in the activity; you can continue to trace the source code and see that it has been invoked. Windows's setContentView method, and as mentioned earlier, the only implementation class for Windows is PhoneWindow.

  public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);  //Call the getWindow method and return mWindow
 initWindowDecorActionBar();
}
...
public Window getWindow() {   
 return mWindow;
}  

At this point, I began to wonder if the first forwarding of events would be passed to Phone Windows when the elements in the activity were clicked. In fact, there is an internal class DecorView object in Phone Windows. It seems a little off-topic. Okay, give Phone Windows's tracking conclusion directly:

  • The frameLayout on the left is our system status bar, and the frameLayout on the right is our setContentView layout in activity.
  • We can also get accurate information from the activity dispatch TouchEvent source code:

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

- As you can see from the above code, the activity dispatchTouchEvent calls the window superDispatchTouchEvent method first, and if the event is not consumed, it calls its own onTouchEvent method.

  • The focus shifts to getWindow().superDispatchTouchEvent(), and getWindow() returns the top-level window (PhoneWindow) object of the current Activity. Let's look directly at the superDispatchTouchEvent() method of the Window API.

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

- It calls superDispatch TouchEvent () of the DecorView class directly. DecorView is a final internal class of Phone Windows and inherits FrameLayout. It is also the top-level View object of the Windows interface. Actually, decorView is built in the setContentView method of activity.

  • Keep looking at the source code:

    @Override  
       public void setContentView(int layoutResID) {  
          if (mContentParent == null) {  
              installDecor();  
          } else {  
              mContentParent.removeAllViews();  
          }  
          mLayoutInflater.inflate(layoutResID, mContentParent);  
          final Callback cb = getCallback();  
          if (cb != null && !isDestroyed()) {  
              cb.onContentChanged();  
          }  
       }  

- As you can see, installDecor() is called before rendering our layout file.

  • Let's continue with the installDecor source code:

    private void installDecor() {  
            if (mDecor == null) {  
                mDecor = generateDecor();  
                mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  
                mDecor.setIsRootNamespace(true);  
                if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {  
                    mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);  
                }  
            }  
            if (mContentParent == null) {  
                mContentParent = generateLayout(mDecor);  
      
                // Set up decor part of UI to ignore fitsSystemWindows if appropriate.  
                mDecor.makeOptionalFitsSystemWindows();  
      
                mTitleView = (TextView)findViewById(com.android.internal.R.id.title);  
                if (mTitleView != null) {  
                    mTitleView.setLayoutDirection(mDecor.getLayoutDirection());  
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {  
                        View titleContainer = findViewById(com.android.internal.R.id.title_container);  
                        if (titleContainer != null) {  
                            titleContainer.setVisibility(View.GONE);  
                        } else {  
                            mTitleView.setVisibility(View.GONE);  
                        }  
                        if (mContentParent instanceof FrameLayout) {  
                            ((FrameLayout)mContentParent).setForeground(null);  
                        }  
                    } else {  
                        mTitleView.setText(mTitle);  
                    }  
                } else {  
                    mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);  
                    if (mActionBar != null) {  
                        mActionBar.setWindowCallback(getCallback());  
                        if (mActionBar.getTitle() == null) {  
                            mActionBar.setWindowTitle(mTitle);  
                        }  
                        final int localFeatures = getLocalFeatures();  
                        if ((localFeatures & (1 << FEATURE_PROGRESS)) != 0) {  
                            mActionBar.initProgress();  
                        }  
                        if ((localFeatures & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {  
                            mActionBar.initIndeterminateProgress();  
                        }  
      
                        boolean splitActionBar = false;  
                        final boolean splitWhenNarrow =  
                                (mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;  
                        if (splitWhenNarrow) {  
                            splitActionBar = getContext().getResources().getBoolean(  
                                    com.android.internal.R.bool.split_action_bar_is_narrow);  
                        } else {  
                            splitActionBar = getWindowStyle().getBoolean(  
                                    com.android.internal.R.styleable.Window_windowSplitActionBar, false);  
                        }  
                        final ActionBarContainer splitView = (ActionBarContainer) findViewById(  
                                com.android.internal.R.id.split_action_bar);  
                        if (splitView != null) {  
                            mActionBar.setSplitView(splitView);  
                            mActionBar.setSplitActionBar(splitActionBar);  
                            mActionBar.setSplitWhenNarrow(splitWhenNarrow);  
      
                            final ActionBarContextView cab = (ActionBarContextView) findViewById(  
                                    com.android.internal.R.id.action_context_bar);  
                            cab.setSplitView(splitView);  
                            cab.setSplitActionBar(splitActionBar);  
                            cab.setSplitWhenNarrow(splitWhenNarrow);  
                        } else if (splitActionBar) {  
                            Log.e(TAG, "Requested split action bar with " +  
                                    "incompatible window decor! Ignoring request.");  
                        }  
      
                        // Post the panel invalidate for later; avoid application onCreateOptionsMenu  
                        // being called in the middle of onCreate or similar.  
                        mDecor.post(new Runnable() {  
                            public void run() {  
                                // Invalidate if the panel menu hasn't been created before this.  
                                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);  
                                if (!isDestroyed() && (st == null || st.menu == null)) {  
                                    invalidatePanelMenu(FEATURE_ACTION_BAR);  
                                }  
                            }  
                        });  
                    }  
                }  
            }  
         }  

  • As you can see from the code:

  • mContentParent = generateLayout(mDecor); using DecorView to create contentparent, and getting contentparent from mContentParent instance of FrameLayout to inherit from framelayout, then superDispatch TouchEvent () of DecorView class should be the FrameLayout Dispatch TouchEvent method. Let's look at framelayout.
  • ~~ However, framelayout does not have the DispatchTouchEvent method, and framelayout inherits from the viewgroup, so it must be in the viewgroup.

  • Next, let's look at the superDispatchTouchEvent() method of the DecorView class

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

  • In it, the dispatchTouchEvent() method of the parent FrameLayout is called, while there is no dispatchTouchEvent() method in FrameLayout, so we look directly at the dispatchTouchEvent() method of ViewGroup and continue to look at the source code:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
    mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
    
        // 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();
        }
    
        // 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;
        }
    
        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }
    
        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
    
        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
    
            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
    
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
    
                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);
    
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    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;
                        }
    
                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
    
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
    
        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    
        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }
    
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
    }
    
  • Key Statements: First check whether to allow the interception of touch events, disallow Intercept for false means to allow the interception, and then call onInterceptTouchEvent to see if it really wants to intercept. So if you want the parent view not to intercept our touch event, you can generally set the value of disallow Intercept by requestDisallow Intercept TouchEvent.
    // 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;
    }

  • When interception is judged, it facilitates the child view that can receive touch events. Distribution of events:
    for (int i = childrenCount - 1; i >= 0; i–) {
    ...
    // childview forwarded to focus only
    if (childWithAccessibilityFocus != null) {
    if (childWithAccessibilityFocus != child) {
    continue;
    }
    childWithAccessibilityFocus = null;
    i = childrenCount - 1;
    }
    ......
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    .......
    break;
    }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
    

}

  • Let's continue to look at the dispatch Transformed TouchEvent method:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
    View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    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;
    }
    
    // Calculate the number of pointers to deliver.
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    
    // If for some reason we ended up in an inconsistent state where it looks like we
    // might produce a motion event with no pointers in it, then drop the event.
    if (newPointerIdBits == 0) {
        return false;
    }
    
    // If the number of pointers is the same and we don't need to perform any fancy
    // irreversible transformations, then we can reuse the motion event for this
    // dispatch as long as we are careful to revert any changes we make.
    // Otherwise we need to make a copy.
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
    
                handled = child.dispatchTouchEvent(event);
    
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    
    // 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;
    

    }

  • As you can see, if the child view is null, it calls super. dispatch TouchEvent to continue talking about event distribution. That is, if the viewgroup does not have child view, it distributes it to itself. Otherwise, the dispatch TouchEvent method of the child view is called, and then the consumption result is returned.
    - Let's continue to look at the dispatchTouchEvent method of childview:

    public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    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;
    }
    
  • I picked out the important code in the method:
  • 1. Judge first that mOnTouchListener, mViewFlags & ENABLED_MASK== ENABLED, mOnTouchListener.onTouch(this, event) returns true if all are true, otherwise the onTouchEvent(event) method is executed and returned.
  • mOnTouchListener is actually the onTouchListener we set up. So if view sets onTouchListener, onTouchListener method will be invoked first. (mViewFlags & ENABLED_MASK) === ENABLED is to determine whether the control currently clicked is enable or not, if button controls are open by default;

- In conclusion, if onTouch is executed and true is returned in onTouch, the onTouchEvent method will not be executed again.

Summary:

  • 1. The superDispatch TouchEvent method of DecorView is called in the dispatch TouchEvent method after the touch event is received by Activity.
  • 2. DecorView calls viewGroup's 3 and dispatchTouchEvent methods.
  • 3. In view group's dispatch TouchEvent method, judgment will be made:
    • 1) Check Disallow Intercept
    • 2) disallowIntercept allows interception: Check whether onInterceptTouchEvent really wants to intercept, if intercepted, event will not be distributed, call its own dispatchTouchEvent method, if not intercepted, then call ViewGroup's own dispatchTransformed TouchEvent method.
    • 3) Two more things are done in dispatch Transformed TouchEvent. If there is a child view, it calls the dispatch TouchEvent method of child view to distribute the event. If there is no child view, it still calls its own dispatch TouchEvent method.

- 4) So there are two kinds of dispatch TouchEvent calling view, one is intercepted, not passed to child view, the other is no child view;

  • From the test results of our demo, we can see that onClick is executed only after onTouchEvent is executed, and the onClick-related code is not seen in the view dispatch TouchEvent, so onClick must be called in onTouchEvent, so let's continue to look at the source code of the onTouchEvent method:

    public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // 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)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                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)) {
                                performClick();
                            }
                        }
                    }
    
                    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;
    
            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;
    
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
    
                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();
    
                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;
    
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
    
            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);
    
                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();
    
                        setPressed(false);
                    }
                }
                break;
        }
    
        return true;
    }
    
    return false;
    

    }

  • An interesting thing was found in the code: mTouchDelegate:

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

  • That is to say, we can set mTouchDelegate for the control to intercept touch events and make small discoveries.
  • After calling the mTouchDelegate code, if it is not intercepted, the click event is completed.
  • First, determine whether the control is clickable. If so, enter the switch judgment; focus on case MotionEvent.ACTION_UP: after various judgments, the performance Click () will eventually be called;

    public boolean performClick() {
    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);
    return result;
    }

  • As you can see, when we set up onClickListener, we call the onClick method and return true.

  • The key point is that at the end of the switch statement, a ture is returned, so you will find that once the switch is executed, it will eventually return true; why does it return true? In fact, this involves the hierarchical transfer of touch events:

    When a touch event is registered with the control, ACTION_DOWN, ACTION_MOVE, ACTION_UP and other events are touched every time you click on it; however, once you return false when you execute an action, the subsequent action will not execute again. If you return false in ACTION_DOWN, then none of the following will be executed. That is to say, when dispatch TouchEvent distributes events, the next action will be triggered only if the previous action returns true.

  • Let's look at the View.dispatchTouchEvent code again.

    public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    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;
    }

- As you can see, onTouchEvent(event) returns true, and dispatchTouchEvent will eventually return true, so if we want action to continue to pass on, onTouchEvent will return true; while the onTouch method returns true or false only controls whether to call onTouchEvent method and then affects the onclick method call. . That's why our onTouchEvent method always returns true in the end.

Summary:

  • onTouch can get two prerequisites for execution.
    • 1. mOnTouchListener is not empty
    • 2. The enable of the clicked control must be true
  • So enable control for false never responds to onTouch events
  • If the clicked control is not clickable, then it can not enter the switch in onTouchEvent and return false, which results in the subsequent action s can not be received. This is why we need to set clickable to true so that it can be clicked when using some non-clickable controls.
  • You can set whether the parent control intercepts touch events by calling requestDisallowInterceptTouchEvent
  • You can use onInterceptTouchEvent to set up whether you intercept touch events and organize them to continue to be distributed to child view s.

From the figure, we can see that between viewgroup and view is a recursive form, constantly traversing the view hierarchy!

Posted by zeberdeee on Sun, 07 Jul 2019 18:17:15 -0700