This article is for the collation of the past notes, in this only for the record, do not do rigorous technical sharing.

Based on the relevant

The View coordinate system

MotionEvent

When the user touches the screen, a Touch event is generated, and the details of the event (location, time, etc.) are encapsulated into a MotionEvent object

The event type The specific action
MotionEvent.ACTION_DOWN Press View (Start of all events)
MotionEvent.ACTION_MOVE Slide the View
MotionEvent.ACTION_UP Raise View (corresponding to DOWN)
MotionEvent.ACTION_CANCEL The end of the event
MotionEvent.ACTION_OUTSIDE The event occurs outside the scope of the view

Auxiliary class

Auxiliary class – dev

View touch related tools class full solution

ViewConfiguration

Get Android system common distance, speed, time and other constants

VelocityTracker

Track the speed of touch events. This setting is useful for gestures that include speed in the gesture standard, such as swiping.

GestureDetector

Gesture detection. Some gestures supported by this class include onDown(), onLongPress(), onFling(), and so on. The GestureDetector can be used in conjunction with the onTouchEvent() method.

OverScroller

Springback tool class, different springback effects can be customized for different animation interpolators

TouchDelegate

Extend the touchable area of the subview

view1.post(new Runnable() {
    @Override
    public void run(a) {
        Rect bounds = new Rect();
        // Get the relative coordinates of the rectangle occupied by View2 in its parent View
        view2.getHitRect(bounds);
        // Calculate the coordinates of the expanded rectangle Bounds relative to View1
        bounds.left -= 100;
        bounds.top -= 50;
        bounds.right += 100;
        bounds.bottom += 50;
        TouchDelegate touchDelegate = new TouchDelegate(bounds, view2);
        // Set the TouchDelegate for View1view1.setTouchDelegate(touchDelegate); }});Copy the code

The event processing

  • Each DOWN/MOVE/UP/CANCLE is an event, not a chain of events
  • Events are consumed by returning true/false, not by processing
  • Activity, ViewGroup, View
    • Both have the ability to distribute and consume events
    • Only ViewGroup has the ability to intercept events

Dispatching events

The View in a window is a tree structure that may overlap. When we click on an area that has multiple views that can respond, the event distribution mechanism determines who should handle the click event.

Distribution mechanism like onion model, responsibility chain model, bubbling…

Distribute: Activity -> PhoneWindow -> DecorView -> ViewGroup -> @1- >... -> View consumption: Activity < -phoneWindow < -decorview < -viewGroup < -@1< -... < - ViewCopy the code
  • If the event is consumed, that means that the event is not transmitted and if the event is consumed at sign 1, it’s not transmitted, it’s just returned
  • If the event is never consumed, it is eventually passed to the Activity, and if the Activity does not need it, it is discarded

View

Priority:

  1. OnTouchListener.onTouch
  2. onTouchEven

If onTouchListener. onTouch returns false, it does not mean that the View does not consume events

public boolean dispatchTouchEvent(MotionEvent event) {...// Is covered and does not respond to events
        if (onFilterTouchEventForSecurity(event)) {
            ...
            //setOnTouchListener Specifies the listener with a high priority
            ListenerInfo li = mListenerInfo;
            if(li ! =null&& li.mOnTouchListener ! =null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            
            // The system is implemented well, with low priority.
            if(! result && onTouchEvent(event)) { result =true; }}...return result;
    }
Copy the code

OnTouchEvent:

  • A View will consume events as long as it is clickable, even if setEnable(false) is set, but does not respond
  • As soon as CLICKABLE is checked, return true consumption time
The event To deal with
DOWN Send a LongClick delay message, expiration triggered
MOVE Remove the LongClick message
CANCLE Remove the LongClick message
UP Remove the LongClick message

Trigger the Click event
<! -- Focus only on event distribution, do not focus on other state changes -->public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int action = event.getAction();
    
    // If the View is disabled, return true if it is clickable, indicating that the event was consumed. They just don't respond.
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    
    // Delegate: expand click events and delegate other processing
    if(mTouchDelegate ! =null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true; }}/** * returns true as soon as the if is entered, consuming event */
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (isInScrollingContainer) {
                } else {
                    // Long press the event to send a delayed message to the queue
                    checkForLongClick(0, x, y);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if(! pointInView(x, y, mTouchSlop)) {if((mPrivateFlags & PFLAG_PRESSED) ! =0) {
                        // Remove the message for the long press event.
                        removeLongPressCallback();
                        setPressed(false); }}break;
            case MotionEvent.ACTION_UP:
                if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
                    if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// Remove the message for the long press event
                        removeLongPressCallback();

                        // The onclick event is triggered when UP
                        if(! focusTaken) {if(! post(mPerformClick)) { performClick(); }}}}break;
            case MotionEvent.ACTION_CANCEL:
                // Remove the long press event
                removeLongPressCallback();
                mHasPerformedLongPress = false;
                break;
        }
        return true;
    }
    
    return false;
}
Copy the code

ViewGroup

  1. The DOWN event:
    • Clear the previous state, mFirstTouchTarget = null
    • Go to logic 1 and 2 to find the child View that received the event
      • MFirstTouchTarget = null, enter logic 3
      • mFirstTouchTarget ! = null, enter logic 4
  2. MOVE/UP events:
    • MFirstTouchTarget = null, comment 1 does not meet the judgment condition of logic 1, enter logic 3
    • mFirstTouchTarget ! = null. If the conditions of logic 2 are not met, the system enters logic 4
  3. CANCLE events:
    • MFirstTouchTarget = null, comment 2 does not meet the judgment condition of logic 1, enter logic 3
    • mFirstTouchTarget ! = null, the judgment condition of logic 1 is not met at comment 2, and logic 4 is entered

Summing up,

  • The DOWN event is used to clean up the state and find new event subviews

  • Subsequent events of the DOWN event:

    • If no child View is found, handle it yourself
    • Find the child View to receive the case, directly to the child View
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...// If the View is shaded and does not respond to click events when shaded, the touch event is not distributed, that is, false is returned.
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            /** * step1: Indicates that the event was first started and the previous state is cleared. * /
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Key: Clear the previous gesture's mFirstTouchTarget = null each time the gesture is DOWN
                cancelAndClearTouchTargets(ev);
                // Clear state
                resetTouchState();
            }

            
            /** * step2: intercept judgment */
            final boolean intercepted;
            // ACTION_DOWN (initial state) or there is a child View to handle the event: determine whether to intercept
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
                // There is no flag bit by default, return false
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                if(! disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
                    // Returns false by default and is not called every time
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);
                } else {//requestDisallowInterceptTouchEvent(true)
                    intercepted = false; }}else {
                //[comment 1], no child View receives event, intercept
                intercepted = true;
            }


            /** * step3: Find a subview that can receive the event and assign it to mFirstTouchTarget */
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL; //[注释2]
            // ***** initializes both variables **** each time
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // if you do not satisfy the judgment condition at this level, you can go directly to logic 3,4.
/ / logic [1]
            if(! canceled && ! intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() :null;
        			/** * mFirstTouchTarget = null; /** * mFirstTouchTarget = null; /* mFirstTouchTarget = null; ==null does not satisfy the if condition), * directly into [logic 3] to the ViewGroup itself. * /
/ / logic [2]
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null&& childrenCount ! =0) {
                        for (int i = childrenCount - 1; i >= 0; i--) {/ / reverse
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // Look for the child in the view list that received the event, break it if there is one, because it is already found
                            newTouchTarget = getTouchTarget(child);
                            if(newTouchTarget ! =null) {
                                break;
                            }

                            // Call the dispatch method of the child View after entering.
                            if (dispatchTransformedTouchEvent(ev, false, child, 
                                                              idBitsToAssign)) {
                                // If you can access this, you can use the child consumption event
                                // Set mFirstTouchTarget in addTouchTarget to logic 4.
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // Since the down event has already been consumed, it returns true in logic 4.
                                alreadyDispatchedToNewTouchTarget = true;
                                break; }}}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; }}/ / logic [2]
            }/ / logic [1]

            /* step4: Get out of the big nested judgment! The large nesting above is used to find the child View that receives the event. Once you determine that a receiver is found or not, the following events are: 1. Check the intercepte status. 2. Go to the logic below, and the following events directly determine to whom */ is distributed
            // The View receiving the event was not found. Subsequent moves/Up are also given to the ViewGroup through this step[logic3]     if (mFirstTouchTarget == null) {
             // Call its own dispatchTouchEvent without a child View receiving the event
                handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); [logic4]}else {// The View that received the event was found
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while(target ! =null) {
                    final TouchTarget next = target.next;
                    / / found in the DOWN to accept events of sub View, assignment alreadyDispatchedToNewTouchTarget = true
                    // The event has already been consumed, so return true
                    / / at the back of the other event, alreadyDispatchedToNewTouchTarget reset, not meet the conditions
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                         // Determine whether the CANCEL event was received or whether the event needs to be intercepted
                         final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                 || intercepted;
                         // Child View consumption event
                         // If cancelChild is true, send the cancle event to the child View[logic5]                  if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                         get.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // Modify mFirstTouchTarget so that the original child View no longer receives events
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                }
            }
        }
        return handled;
    }
Copy the code

Activity

The Touch event is passed first to the Activity, then from the Activity to the outermost layout, and then to the View

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // Interactive empty implementation
            onUserInteraction();
        }
        // DecorView is actually the dispatchTouchEvent method of the ViewGroup
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // Down click to the outer area, consume the event, finish
        return onTouchEvent(ev);
    }
Copy the code

onUserInteraction()

This is an empty implementation that is rarely used and doesn’t go into too much detail: this method is an activity method that is triggered when the activity is at the top of the stack by tapping the home, back, Menu key, etc. Pulling down statubar, rotating the screen, and locking the screen do not trigger this method. So it’s used in screensaver applications, because when you touch the screen, the machine immediately triggers an event, and it’s not very clear what the event is, and the screensaver meets this need; Or for an Activity, control how long it takes to disappear without a response from the user.

onTouchEvent(event)

    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }
Copy the code

MWindow Even PhoneWindow, this method is @hide and is defined in the Window class.

    / * *@hide* /
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        if(mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() ! =null) {
            return true;
        }
        return false;
    }
Copy the code
  • MCloseOnTouchOutside is a Boolean variable, it is by the Window of the android: windowCloseOnTouchOutside decided attribute values.
  • IsOutOfBounds (context, Event) determines whether the event’s coordinates are outside the context(for this article, the current Activity). If yes, return true; Otherwise, return false.
  • PeekDecorView () returns the mDecor of the PhoneWindow.

In general: if set android: windowCloseOnTouchOutside is true, and click on the Activity is DOWN events outside area (for example, the Activity is a Dialog), returns true, consumption, and finish.

ACTION_CANCEL

When the child View receives an event, it is interrupted, and the parent View passes the child a CANCEL event

[logic4]}else {// The View that received the event was found
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while(target ! =null) {
                    if (alreadyDispatchedToNewTouchTarget && target ==  newTouchTarget) {
                    } else {
                         // Determine whether the CANCEL event was received or whether the event needs to be intercepted
                         final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                 || intercepted; / / comment 1

                         // If cancelChild is true, send the cancle event to the child View[logic5]                  if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                         get.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        // Modify mFirstTouchTarget so that the original child View no longer receives events
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                       / /...}}}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 {
                // Send the CANCEL event to the child View
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        / /...
    }
Copy the code

ACTION_OUTSIDE

With FLAG_WATCH_OUTSIDE_TOUCH set, events occur outside the scope of the current view

For example, clicking the area outside the volume key undisplays the volume key:

//frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java        
		// Set the volume key Window to FLAG_WATCH_OUTSIDE_TOUCH
        mDialog = new CustomDialog(mContext);
        mWindow = mDialog.getWindow();
        mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH // Set Window Flag| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); .// Override onTouchEvent and handle ACTION_OUTSIDE events
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (mShowing) {
                if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                    dismissH(Events.DISMISS_REASON_TOUCH_OUTSIDE);
                    return true; }}return false;
        }

Copy the code

Events to intercept

Resolving Android View sliding conflicts

Only ViewGroup has the ability to intercept events. A View can apply for a parent View to intercept events according to the situation

View

View without the ability to intercept events, can according to the different demand calls mParent. RequestDisallInterceptTouchEvent (true/false) to apply for the parent View whether to intercept.

Note: If the child View receives an event that is blocked by the parent View, the parent View will give the child View a CANCEL event.

ViewGroup

onInterceptTouchEvent
  • FLAG_DISALLOW_INTERCEPT is not called when the flag is set
  • It’s called every other time
    /** * ViewGroup event distribution interception check mechanism */
 	// There is no flag bit by default, return false
    final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;/ / comment 1
    if(! disallowIntercept) {//requestDisallowInterceptTouchEvent(false)
        intercepted = onInterceptTouchEvent(ev);// Returns false by default
    } else {
        intercepted = false;//requestDisallowInterceptTouchEvent(true)
    }
    
    
    /** * Returns false */ by default
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
    
    /* * disallowIntercept = true, disallowIntercept = false, false */
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        // We're already in this state, assume our ancestors are too
        if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0)) {
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;// Add a flag to make comment 1 true
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;// Clear the flag so that comment 1 is false
        }

        if(mParent ! =null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code
requestDisallowInterceptTouchEvent
  • True, intercepting is not allowed. Comment 1 is true and onInterceptTouchEvent is not called
  • False, allow intercepting, comment 1 is false(default), call onInterceptTouchEvent

Note: call requestDisallowInterceptTouchEvent intercept (false) application, will not really is blocked by the parent View. It is simply a flag that causes the parent View to check the onInterceptTouchEvent method (which is also called by default). It only affects the mGroupFlags & FLAG_DISALLOW_INTERCEPT value. The real decision is to look at the return value of onInterceptTouchEvent. If true:

CancelChild = true at comment 1 causes the CANCEL event to be sent to the subclass, and then changes the mFirstTouchTarget to no longer pass the event to the child View.

[logic4]}else {// The View that received the event was found
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while(target ! =null) {
                    if (alreadyDispatchedToNewTouchTarget && target ==  newTouchTarget) {
                    } else {
                         // Determine whether the CANCEL event was received or whether the event needs to be intercepted
                         final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                 || intercepted; / / comment 1
                         // Child View consumption event
                         // If cancelChild is true, send the cancle event to the child View[logic5]                  if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                         get.child, target.pointerIdBits)) {
                            handled = true;
                        }
                       // Modify mFirstTouchTarget so that the original child View no longer receives events
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue; }}}}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;
        }
        / /...
    }
Copy the code

Activity

The Activity has no onInterceptTouchEvent method, no mParent, and no active or passive intercepting capability

Sliding conflict

Common scenarios:

  1. Internal and external layer sliding direction is inconsistent (such as: Vertical sliding RecyclerView nested in ViewPager)
  2. Consistent sliding direction of inner and outer layers (e.g. RecyclerView nesting)

Generally, it starts from two perspectives: the parent View actively intercepts, or the child View applies for the parent View to intercept

The parent View

Event sender, parent View intercepts.

The parent View chooses when to return true on the onInterceptTouchEvent based on its own needs, so that the event is distributed directly to the parent View. Child View is not set requestDisallowInteceptTouchEvent (true) or wouldn’t pass onInterceptTouchEvent method).

  • DOWN do not intercept, otherwise according to the event distribution logic, the event is directly handled by the parent View itself
  • UP does not intercept, otherwise the child View will not be able to launch the Click event and remove the longClick message
  • Determine whether to intercept in MOVE based on logical requirements
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if(meets the interception requirements of the parent container) {intercepted =true;
                } else {
                    intercepted = false;
                }
                break; }}return intercepted;
    }
Copy the code

The child View

Event receiver, internal intercept

The event has been passed to the child View, which can only choose whether to consume the event or request that the parent View intercept the event.

Note: applying to intercept events does not mean that you will not receive events in the future. Request simply clears the FLAG_DISALLOW_INTERCEPT flag, causing the parent View to check the onInterceptTouchEvent method, and that’s it (back to the default). Basically see the parent View. The return value of onInterceptTouchEvent.

    public boolean dispatchTouchEvent(MotionEvent event) {/ / or onTouchEvent
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                parent.requestDisallowInterceptTouchEvent(true);// No interception allowed
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if(the parent container need such click events) {parent. RequestDisallowInterceptTouchEvent (false);// Request interception
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break; }}return super.dispatchTouchEvent(event);
    }
Copy the code

: Cry: multi-touch

Android custom View advanced – multi-touch details

Freely zoom and move the image

Multi-touch related events:

The event Introduction to the
ACTION_DOWN The first onefingerYour first exposure to a screenWhen triggered.
ACTION_MOVE Triggered by a finger swiping across the screen, multiple times (One or more fingers).
ACTION_UP The last oneTriggered when the finger leaves the screen.
ACTION_POINTER_DOWN There are non-major finger presses (That is, there is already a finger on the screen before pressing).
ACTION_POINTER_UP There are non-primary finger lifts (That is, there are still fingers on the screen after lifting).
The following event types are not recommended The following events start in 2.0 and are deprecated after 2.2
ACTION_POINTER_1_DOWN Press the second finger, it is obsolete, not recommended.
ACTION_POINTER_2_DOWN Press the third finger, it is obsolete, not recommended.
ACTION_POINTER_3_DOWN Press the 4th finger. Deprecated. Not recommended.
ACTION_POINTER_1_UP Second finger raised, deprecated, not recommended.
ACTION_POINTER_2_UP Third finger raised, deprecated, not recommended.
ACTION_POINTER_3_UP Fourth finger raised, deprecated, not recommended.

Multi-touch related methods:

methods Introduction to the
getActionMasked() withgetAction()A similar,Multi-touch requires this method to get the event type.
getActionIndex() Gets which pointer (finger) generated the event.
getPointerCount() Gets the number of fingers on the screen.
getPointerId(int pointerIndex) Gets the unique identifier ID of a pointer (finger) that remains the same between when the finger is pressed and lifted.
findPointerIndex(int pointerId) The PointerId is used to get the current PointIndex, and then the PointIndex is used to get other content.
getX(int pointerIndex) Gets the X coordinate of a pointer
getY(int pointerIndex) Gets the Y coordinate of a pointer

The index and pointId

In version 2.2 and above, we can easily obtain the Index of the event by using getActionIndex(). The Index changes have the following characteristics:

1. Grow automatically from 0. 2. If you lift up the fingers that fell before, the Index of the back fingers will decrease accordingly. (0, 1, 2 –> second finger lifts –> third finger becomes 1 –> 0, 1) 4. Invalid for move events. **getActionIndex()** always gets the value 0

The same The difference between
1. The value starts from 0 and grows automatically.

2. Priority is given to fill the vacancy when the finger is dropped (fill in the number of the previously lifted finger).
Index will change, pointId will never change.

PointerIndex and pointerId

There is no significant difference between pointerIndex and actionIndex. The value of pointerIndex is the same as that of actionIndex.

type Introduction to the
pointerIndex Used to retrieve specific events that may change as other fingers rise and fall
pointerId Used to identify the finger, produced when the finger is pressed, recovered when the finger is lifted, the period is always the same

These two values are converted to each other using the following two methods:

methods Introduction to the
getPointerId(int pointerIndex) Gets the unique identifier ID of a pointer (finger) that remains the same between when the finger is pressed and lifted.
findPointerIndex(int pointerId) The pointerId is used to get the current pointIndex, and then the pointIndex is used to get other content.

Custom View examples

/** * Created by Varmin * on 2017/7/5 16:16. ** Created by Varmin * on 2017/7/5 16:16. Used to sort child views within the ViewGroup. * Function: default all off left and right sliding. Set to open */ separately
public class SlideView extends ViewGroup implements View.OnClickListener.View.OnLongClickListener {
    private static final String TAG = "SlideView";
    public final String LEFT = "left";
    public final String CONTENT = "content";
    public final String RIGHT = "right";
    private Scroller mScroller;
    /** * scroller sliding time. The default 250 ms * /
    public static final int DEFAULT_TIMEOUT = 250;
    public static final int SLOW_TIMEOUT = 500;
    /** * Width of left and right views */
    private int leftWidth;
    private int rightWidth;
    private GestureDetector mGesture;
    private ViewConfiguration mViewConfig;

    public SlideView(Context context) {
        super(context);
        init(context);
    }

    public SlideView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mScroller = new Scroller(context);
        // All of these are processed by themselves. There is no use of this gesture method here
        // Disadvantages: large error. This kind of precise slide is best judged by yourself
        mGesture = new GestureDetector(context, new SlideGestureDetector());
        mViewConfig = ViewConfiguration.get(context);
        / / default is false
        setClickable(true);
    }

    OnMeasuer/onLayout (childCount); /** * (childCount); /** * (childCount); * /
    @Override
    protected void onFinishInflate(a) {
        super.onFinishInflate();
        initListener();
    }

    private void initListener(a) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            childView.setClickable(true);
            childView.setOnClickListener(this);
            if (CONTENT.equals(childView.getTag())) {
                childView.setOnLongClickListener(this); }}}@Override
    public void onClick(View v) {
        String tag = (String) v.getTag();
        switch (tag) {
            case LEFT:
                Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
                break;
            case CONTENT:
                Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
                closeAll(SLOW_TIMEOUT);
                break;
            case RIGHT:
                Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
                break; }}@Override
    public boolean onLongClick(View v) {
        Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
        return true;
    }

    /** * The size of each View is determined by the parent container passing mode to itself. * The position of each View is determined by the parent container. * Therefore, the container inheriting from the ViewGroup needs to implement the size and position of its child views internally. * /

    /** * Subviews do not measure themselves, so measure the size of each subview * in addition, handle the case of its own wrap, give yourself a certain value. * /
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // Measure the subview
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        // Measure yourself
        // The default is to give the ViewGroup a fixed width and height, assuming that it is not pure in the wrap case, which is not considered in onLayout
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            String tag = (String) childView.getTag();
            switch (tag) {
                case LEFT:
                    leftWidth = childWidth;
                    childView.layout(-childWidth, 0.0, childHeight);
                    break;
                case CONTENT:
                    childView.layout(0.0, childWidth, childHeight);
                    break;
                case RIGHT:
                    rightWidth = childWidth;
                    childView.layout(getMeasuredWidth(), 0, 
                                     getMeasuredWidth() + childWidth, childHeight);
                    break; }}}@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean handled = super.onInterceptTouchEvent(ev);
        if (handled) {
            return true;
        }
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitX = (int) ev.getX();
                mInitY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = (int) (ev.getX() - mInitX);
                int offsetY = (int) (ev.getY() - mInitY);
                /** * the parent ViewGroup does not need to look at the intercepting event. * /
                if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
                    requestDisallowInterceptTouchEvent(true);
                    return true;
                }
                break;
            case MotionEvent.ACTION_UP:
                // Resets the default interception state of ViewGroup
                requestDisallowInterceptTouchEvent(false);
                break;
        }
        return handled;
    }

    private int mInitX;
    private int mOffsetX;
    private int mInitY;
    private int mOffsetY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean handled = false;
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                mOffsetX = (int) (event.getX() - mInitX);
                mOffsetY = (int) (event.getY() - mInitY);
                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {// Horizontal trigger condition
                    // Estimate the size offsetX
                    int mScrollX = getScrollX() + (-mOffsetX);
                    if (mScrollX <= 0) {// Swipe right to display leftView: 110
                        If the estimate is greater than the target: you can't return and give up, adjust the value of mOffsetX so that it is exactly equal to the target
                        if (Math.abs(mScrollX) > leftWidth) {
                            mOffsetX = leftWidth - Math.abs(getScrollX());
                            //return true;}}else {// Swipe left to display rightView: 135
                        if (mScrollX > rightWidth) {
                            mOffsetX = getScrollX() - rightWidth;
                            //return true;}}this.scrollBy(-mOffsetX,0);
                    mInitX = (int) event.getX();
                    mInitY = (int) event.getY();
                    return true;
                }

                break;
            case MotionEvent.ACTION_UP:
                int upScrollX = getScrollX();
                if (upScrollX > 0) {// Swipe left to display rightView
                    if (upScrollX >= (rightWidth/2)) {
                        mOffsetX = upScrollX - rightWidth;
                    }else{ mOffsetX = upScrollX; }}else {// To the right, the leftView is displayed
                    if (Math.abs(upScrollX) >= (leftWidth/2)) {
                        mOffsetX = leftWidth - Math.abs(upScrollX);
                    }else{ mOffsetX = upScrollX; }}// this.scrollBy(-mOffsetX,0); / / too fast
               // startScroll(-mOffsetX, 0, 1000); // Just put it in.
                /** * note startX. Dx is distance, not destination */
                mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
                invalidate();

                break;
        }

        if(! handled) { handled =super.onTouchEvent(event);
        }
        return handled;
    }


    @Override
    public void computeScroll(a) {
        if(mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); }}/** * Even though the passed dx and dy are not the actual points scrollTo is going to reach, dx and dy are just a short distance away. * But computeScroll() we scrollTo is: present position +dx distance = target position * *@paramDx //TODO * distance! Distance! It's not a goal to reach. * *@param dy
     * @paramDuration The default sliding time is 250, and you can set the event if it feels too fast
    private void startScroll(int dx, int dy, int duration) {
        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
        //mScroller.extendDuration(duration); Increase on the basis of 250ms. The constructor, passed in, is the time of duration.
        invalidate();
    }


    /** * Whether to open, reuse in ListView closed *@return* /
    public boolean isOpened(a){
        returngetScrollX() ! =0;
    }
    public void closeAll(int duration){
        mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration); invalidate(); }}Copy the code

Tips

scrollTo/By

There are three ways to slide a View:

  1. ScrollTo/scrollBy methods provided by the View itself;

  2. Animate the Veiw to pan.

  3. By changing the value of the View’s LayoutParams property.

**setScrollX/Y, scrollTo: ** moves to position x, Y

**scrollBy: ** Moves x,y pixel distance

    public void setScrollX(int value) {
        scrollTo(value, mScrollY);
    }

    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

    public void scrollTo(int x, int y) {
        if(mScrollX ! = x || mScrollY ! = y) {int oldX = mScrollX;
            intoldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); }}Copy the code

** If scrollTo(30,10) is positive on the bottom right and negative on the top left, it will slide 30 to the right and 10 to the bottom.