Android View event system

Posted by Kevin3374 on Fri, 04 Mar 2022 08:42:13 +0100

View event system

Although View does not belong to the four components, its role is comparable to the four components, and even more important than Receiver and Provider. In Android development, Activity undertakes this visualization function. At the same time, Android system provides many basic controls, such as Button, Textview, CheckBox, etc.

View Basics

What is View

View is an abstraction of the control in the interface layer, which represents a control. In addition to the view, there is also the ViewGroup. The ViewGroup contains many controls, that is, a group of views. The ViewGroup also inherits the view, which means that the view itself can be a single control or a group of controls composed of multiple controls. Through this relationship, the structure of the view tree is formed.

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-UhcAOmGE-1646358386607)(img/331079-20170122173619254-1135218575.jpg)]

Location parameters of View

The position of View is mainly determined by its four vertices, which correspond to the four attributes of View: top, left, right and bottom. Top is the ordinate of the upper left corner, left is the abscissa of the upper left corner, right is the abscissa of the lower right corner and bottom is the ordinate of the lower right corner. It should be noted that these coordinates are relative to the parent container of View, because it is a relative coordinate.

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-xaet1rxd-164635838668) (IMG / 331079-201701221735769-1540262867. JPG)]

In Android, the positive directions of the X-axis and Y-axis are right and down respectively. Then we can get the following relationship:

width = right - left
height = bottom - top

MotionEvent and

After the finger touches the screen, a series of events will be generated. The typical types of events are as follows:

  • ACTION_DOWN - the finger just touches the screen
  • ACTION_MOVE - finger moves on the screen
  • ACTION_UP -- the moment when the finger is released from the screen

Normally, a finger touching the screen will trigger a series of click events, as follows:

  • Click the screen to leave and release, and the event sequence is down - > up
  • Click the screen to slide for a while and then release it. The event sequence is down - > move - > up

At the same time, we can get the X and Y coordinates of the click event through the MotionEvent object. For this purpose, the system provides two groups of methods: getX/getY and getRawX/getRawY. The difference is very simple. The former returns the X and Y coordinates relative to the upper left corner of the current View, while the latter returns the X and Y coordinates relative to the upper left corner of the mobile phone screen.

TouchSlop

Touchslope is the minimum distance recognized by the system and considered to be sliding. In other words, when the finger slides on the screen, if the distance between two slides is less than this constant, the system will not think you are sliding. Because the sliding distance is too short, the system will not think it is sliding. This is a constant, which is related to the device. It can be obtained in the following ways:

ViewConfiguration.get(getContext()).getScaledTouchSlop()

You can do some filtering through this constant

In frameworks / base / core / RES / RES / values / config This constant is defined in XML

 <dimen name="config_viewConfigurationTouchSlop">8dp</dimen>

Sliding of View

In Android devices, sliding is almost the standard configuration of applications. Whether it's pull-down refresh or SlidingMenu, their foundation is sliding. Therefore, mastering the sliding method is the basis for realizing gorgeous custom controls. There are three ways to realize the sliding of View:

  • scrollTo/scrollBy method
  • Use animation
  • Change layout parameters

scrollTo/scrollBy method

Source code

public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}
public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

First, we need to get the two attributes mScrollX and mScrollY in the View. During the sliding process, the value of mScrollX is always equal to the horizontal distance between the left edge of the View and the left edge of the View content, while the value of mScrollY is always equal to the vertical distance between the upper edge of the View and the upper edge of the View content. The View edge refers to the position of the View, which is composed of four vertices. The View content edge refers to the edge of the content in the View. scrollTo/scrollBy can only change the position of the View content, not the position of the View in the layout.

If you slide from left to right, mscollx is negative and vice versa. If you slide from top to bottom, mscolly is negative and vice versa.

[the external chain picture transfer fails. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-U7yBouU3-1646358386609)(img/331079-20170122173652285-574067978.jpg)]

Use animation

Using animation, we can make a View translate, and translation is a sliding. For example:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

However, there is a problem with animation movement, that is, the View animation can not really change the position of the View, which will bring a very serious problem, that is, when you move to a new position, you find that the event cannot be triggered, but when you click the original position, it can be triggered. So from Android 3 Starting from 0, the above problem can be solved by using attribute animation.

Change layout parameters

Changing the layout parameters, i.e. changing LayoutParams, is easier to understand. For example, if we want to shift a Button to the right by 100px, we only need to increase the value of the marginLeft parameter in the LayoutParams of the Button by 100px.

MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams;
params.width += 100;
parms.leftMargin += 100;
mButton1.requestLayout();

Comparison of three sliding modes:

  • scrollTo/scrollBy: easy to operate, suitable for sliding View content
  • Animation: simple operation, mainly used in View without interaction and to realize complex animation effects
  • Change layout parameters: the operation is slightly complex and applicable to interactive views

Elastic sliding

There is a common idea on how to realize elastic sliding: divide a large sliding into several small sliding and complete it in one time period. There are many common ways to realize elastic sliding, such as Scroller, Handler#postDelayed, Thread#Sleep and so on.

VelocityTracker

It is mainly used to track the speed of fingers in the sliding process.

private VelocityTracker mVelocityTracker;
   @Override
    public boolean onTouchEvent(MotionEvent event) {
    if (mVelocityTracker == null) { 
            mVelocityTracker = VelocityTracker.obtain();//Get VelocityTracker class instance 
    } 
          //Add the event to the VelocityTracker class instance 
    mVelocityTracker.addMovement(ev);
    }
//Set the parameter value to 1000, which means how many pixels are moved in one second 
    velocityTracker.computeCurrentVelocity(1000); 
//Gets the speed in the horizontal direction
    float xVelocity = mVelocityTracker.getXVelocity();
  1. VelocityTracker.obtain() method to obtain an instance object of the velocitytracker class
  2. In the onTouchEvent callback function, use the addMovement(MotionEvent) function to pass the current movement event to the VelocityTracker object
  3. Use the computeCurrentVelocity (int units) function to calculate the current speed
  4. Use getXVelocity() and getYVelocity() functions to obtain the current speed

GestureDetector

It is used to assist in detecting the user's click, slide, long press, double click and other behaviors.

Source code

public interface OnGestureListener {

                // Triggered when touching down, e is the MotionEvent when down

                boolean onDown(MotionEvent e);

                // Triggered a certain time (115ms) after Touch down, and e is the MotionEvent when down

                void onShowPress(MotionEvent e);

                // Triggered when touching up, e is MotionEvent when up

                boolean onSingleTapUp(MotionEvent e);

                // Triggered when sliding, e1 is the MotionEvent when down, and e2 is the MotionEvent when move

                boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);

                // Triggered after a certain time (500ms) after Touch down, and e is the MotionEvent when down

                void onLongPress(MotionEvent e);

                // Slide for a distance and trigger when up, e1 is the MotionEvent when down, and e2 is the MotionEvent when up

                boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);

}

 

public interface OnDoubleTapListener {

                // Trigger after completing one click and confirming that there is no second click event (300ms), and e is the MotionEvent when it is down

                boolean onSingleTapConfirmed(MotionEvent e);

                // Triggered when you click down for the second time, and e is the MotionEvent when you click down for the first time

                boolean onDoubleTap(MotionEvent e);

                // It is triggered when you click down,move and up for the second time. e is the MotionEvent under the machine at different times, indicating that double-click behavior occurs during double-click

                boolean onDoubleTapEvent(MotionEvent e);

}
  • Create an object of GestureDetector, pass in the listener object, pass the event to GestureDetector in the onTouchEvent you receive for analysis, and the listener will call back to our corresponding action.

        private MyGestureListener mgListener;
        private VelocityTracker mVelocityTracker;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mgListener = new MyGestureListener();
            mDetector = new GestureDetector(this, mgListener);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            return mDetector.onTouchEvent(event);
        }
    
  • Create MyGestureListener class to inherit gesturedetector Simpleongesturelistener. This class implements the above classes of OnGestureListener and OnDoubleTapListener interfaces. We only need to inherit it and rewrite the callback we need.

     private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
    
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float v, float v1) {
                if (e1.getY() - e2.getY() > MIN_MOVE) 
            		startActivity(new Intent(MainActivity.this, MainActivity.class));
                    Toast.makeText(MainActivity.this, "Start by gesture Activity", Toast.LENGTH_SHORT).show();
                } else if (e1.getY() - e2.getY() < MIN_MOVE) {
                    finish();
                    Toast.makeText(MainActivity.this, "Close by gesture Activity", Toast.LENGTH_SHORT).show();
                }
                return true;
            }
    

Scroller

Scroller class source code

public class Scroller  {  
  
    private int mStartX;    //Starting coordinate point, X-axis direction  
    private int mStartY;    //Starting coordinate point, Y-axis direction  
    private int mCurrX;     //The X-axis of the current coordinate point, that is, the value reached after a certain time after calling the startScroll function  
    private int mCurrY;     //The Y axis of the current coordinate point, that is, the value reached after a certain time after calling the startScroll function  
     
    private float mDeltaX;  //The distance that should continue to slide, in the X-axis direction  
    private float mDeltaY;  //The distance that should continue to slide, Y-axis direction  
    private boolean mFinished;  //Whether the sliding operation has been completed. If it is completed, it is true  
  
    //Constructor  
    public Scroller(Context context) {  
        this(context, null);  
    }  
    public final boolean isFinished() {  
        return mFinished;  
    }  
    //Forcibly end this sliding screen operation  
    public final void forceFinished(boolean finished) {  
        mFinished = finished;  
    }  
    public final int getCurrX() {  
        return mCurrX;  
    }  

    //Calculate the current coordinate point according to the time that has elapsed and save it in the mCurrX and mCurrY values  
    public boolean computeScrollOffset() {  
        if (mFinished) {  //This animation control has been completed, and the direct return is false  
            return false;  
        }  
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);  
        if (timePassed < mDuration) {  
            switch (mMode) {  
            case SCROLL_MODE:  
                float x = (float)timePassed * mDurationReciprocal;  
                ...  
                mCurrX = mStartX + Math.round(x * mDeltaX);  
                mCurrY = mStartY + Math.round(x * mDeltaY);  
                break;  
            ...  
        }  
        else {  
            mCurrX = mFinalX;  
            mCurrY = mFinalY;  
            mFinished = true;  
        }  
        return true;  
    }  
    //Start an animation control by (startx, starty) advancing (dx,dy) units in the duration time, that is, the arrival coordinate is (startx + DX, starty + dy)  
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
        mFinished = false;  
        mDuration = duration;  
        mStartTime = AnimationUtils.currentAnimationTimeMillis();  
        mStartX = startX;       mStartY = startY;  
        mFinalX = startX + dx;  mFinalY = startY + dy;  
        mDeltaX = dx;            mDeltaY = dy;  
        ...  
    }  
  • computeScrollOffset() method

    Function Description: calculate the current coordinate point according to the current elapsed time and save it in mCurrX and mCurrY values

    The computeScrollOffset () method is called when you want to know the new location. If true is returned, the animation is not yet complete.

  • startScroll(int startX, int startY, int dx, int dy, int duration) method

Function Description: start an animation control, advance (dx,dy) units by (startx, starty) within the duration time, and reach the coordinates (startx + DX, starty + dy).

computeScroll() method

In order to control the sliding screen control easily, the Android framework provides * * computeScroll() * * method to control this process. When drawing a View, this method is called in the draw() procedure. Therefore, by using the Scroller instance, we can obtain the current offset coordinates and manually offset the View/ViewGroup there. The prototype of computescroll () method is as follows, which is located in View Java class

private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    invalidate();
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

Calling invalidate() in the smoothScrollBy method will lead to View repainting. In computeScroll's draw method, computeScroll will be called. By implementing computeScroll method, the method can be slid through scrollTo, and then postInvalidate () is redrawn second times. This process is the same as the first process, so that the whole sliding process is finished.

In order to control the sliding screen control easily, the Android framework provides * * computeScroll() * * method to control this process. When drawing a View, this method is called in the draw() procedure. Therefore, by using the Scroller instance, we can obtain the current offset coordinates and manually offset the View/ViewGroup there. The prototype of the computescroll () method is as follows, which is located in ViewGroup Java class

Finally, let's mention the identification process of double-click and three Click: when clicking down for the first time, a message with a delay of 300ms is sent to Hanlder. If the down event of the second click occurs within 300ms, it is considered to be a double-click event, and the previously sent delay message is removed. If there is no second down message after 300ms, it is determined as SingleTapConfirmed event (of course, the user's finger should have completed the up process of the first click). The three click decision is similar to the double-click decision, but there is an additional process of sending delay messages. Interesting ~ hehe~

The trigger effect of the most recent three scenes needs to be studied. Triple click, that is, click the screen three times, but it is a double-click effect in the sense. Therefore, I need to study how to recognize the action of three combos.

We know that the general View can only respond to Click and long press events. This is because the View only exposes these listener s for us to use. In essence, View analyzes the user's actions in onTouchEvent(MotionEvent event), so as to inform us whether there are Click or long press events.

The callback provided in View cannot meet the requirements in the scenario I described. Therefore, GestureDetector appeared. I need to chew through it to write my own ActionDetector.

GestureDetector class can help us analyze user actions, which is similar to the processing method of onTouchEvent in View, but the action types analyzed are more detailed. The following is its callback interface:

Event distribution mechanism of View

Transmission rules of click events

The distribution process of click events is completed by three very important methods: dispatchTouchEvent, onInterceptTouchEvent and onTouchEvent.

  • dispatchTouchEvent(MotionEvent ev) is used to distribute events. If the event can be passed to the current view, this method will be called. The returned results are affected by the onTouchEvent method of the current view and the dispatchTouchEvent method of the subordinate view
  • onInterceptTouchEvent(MotionEvent event) is used to judge whether to intercept an event. If the current View intercepts an event, this method will not be called again in the same event sequence, and the returned result indicates whether to intercept the current event.
  • onTouchEvent(MotionEvent event), called in the dispatchTouchEvent method, is used to process the current click event, and the returned result indicates whether to consume the current event. If it is not consumed, the current View cannot receive events again in the same event sequence.
public boolean dispatchTouchEvent(MotionEvent ev){
  boolean consume = false;
  if(onInterceptTouchEvent(ev)){
    consume = onTouchEvent(ev);
  }
  else{
    consume = child.dispatchTouchEvent(ev);
  }
  return consume;
}

Simply put, for a root ViewGroup, after the click event is generated, it will be passed to it first, and then its dispatchTouchEvent will be called. If the onInterceptTouchEvent method of the ViewGroup returns true, it means that it wants to intercept the current event, and then the event will be handed over to the ViewGroup for processing, that is, its onTouchEvent method will be called, If the onInterceptTouchEvent method of the ViewGroup returns false, it means that it does not intercept the current event. At this time, the current event will continue to be passed to its child elements, and then the dispatchTouchEvent method of the child elements will be called. This will be repeated until the event is finally processed.

In addition, when the View needs to process events, if OnTouchListener is set, the onTouch method in OnTouchListener will be called back, and OnTouchListener takes precedence over onTouchEvent. In onTouchEvent, if OnClickListener is set, its OnClick method will be called. It can be seen that our commonly used OnClickListener has the lowest priority, onTouch > OnClick

View does not process:

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-iW17NdES-1646358386609)(img/331079-20170122173716848-1439912649.jpg)]

View processing flow

It is suggested to save the picture directly from the external link (ykp7354g-107358jpg-17358b) (upload the picture directly from the external link)

Summary:

  • When a click event is generated, its delivery process follows the following sequence: Activity - > window - > view. If the onTouchEvent of a view returns false, the onTouchEvent of its parent container will be called, and so on. If all elements do not handle this event, they will eventually be handed over to the Activity
  • The same sequence of events is from the moment the finger touches the screen to the moment the finger leaves the screen. Generally, it starts with a down event, with an indefinite number of move events in the middle, and ends with an up event.
  • Normally, an event sequence can only be intercepted and consumed by one View. Once the event is handed over to the View for processing, it must be consumed (onTouchEvent=true). Otherwise, other events in the same event sequence will not be handed over to it for processing, and the event will be handed over to its parent element for processing again, that is, the onTouchEvent of the parent element will be called.
  • If View does not consume action_ For other events other than down, the click event will disappear. At this time, the onTouchEvent of the parent element will not be called and will eventually be handed over to the Activity for processing.
  • ViewGroup does not intercept any events by default.
  • There is no onInterceptTouchEvent method in View.
  • The onTouchEvent of View will be consumed by default unless it is not clickable (clickable and longClickable are false at the same time). The default property of longClickable of View is false. Clickable needs to be divided into different situations. For example, Button defaults to true and TextView defaults to false.
  • The event delivery process is from the outside to the inside, that is, the event is first delivered to the parent element, and then distributed by the parent element to the child View.

ViewGroup event distribution process

Source code

  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 actionmasked = = motionevent ACTION_DOWN|| mFirstTouchTarget != Null determines whether to intercept. After the event is successfully processed by the child element of ViewGroup, mfirsttouchtarget will be assigned and pointed to the child element. If mfirsttouchtarget is null, actionmasked motionevent ACTION_ MOVE|| MotionEvent. ACTION_ Up onintercepttouchevent will not be called and will be intercepted. You can reset the flag through the requestDisallowInterceptTouchEvent() method_ DISALLOW_ The interrupt flag bit. Once set, the ViewGroup will not be able to intercept the action_ Events other than down

You can reset the flag through the requestDisallowInterceptTouchEvent() method_ DISALLOW_ The interrupt flag bit is generally used in the sub view

@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

Once set, the Viewgroup cannot intercept the action_ Events other than down because of ACTION_DOWN resets the FLAG_DISALLOW_INTERCEPT

// 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;
}

View's handling of click events

 if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
        if (sDebugDispatchInput) {
            Log.d(VIEW_LOG_TAG, "dispatchTouchEvent to the view: " + this
                    + ", which has listener, so we call its onTouch");
        }
    }

    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

As can be seen from the source code, we will first judge whether OnTouchListener is set, if M OnTouchListener Ontouch returns true, and onTouchEvent will not be called, montouchlistener Ontouch takes precedence over onTouchEvent

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;
}

Clickable disabled views still consume touch events, and it just doesn't respond to them.

Processing of click action

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;

        case MotionEvent.ACTION_DOWN:
            if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
            }
            mHasPerformedLongPress = false;

            if (!clickable) {
                checkForLongClick(0, x, y);
                break;
            }

            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:
            if (clickable) {
                setPressed(false);
            }
            removeTapCallback();
            removeLongPressCallback();
            mInContextButtonPress = false;
            mHasPerformedLongPress = false;
            mIgnoreNextUpEvent = false;
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            break;

        case MotionEvent.ACTION_MOVE:
            if (clickable) {
                drawableHotspotChanged(x, y);
            }

            // Be lenient about moving outside of buttons
            if (!pointInView(x, y, mTouchSlop)) {
                // Outside button
                // Remove any future long press/tap checks
                removeTapCallback();
                removeLongPressCallback();
                if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            }
            break;
    }

Long in View_ The default value of CLICKABLE is false. Whether it is false is related to the specific View. The CLICKABLE View is true and the non CLICKABLE View is false. For example, the Button is true and the TextView is false. The methods setOnClickListener and setOnLongClickListener will automatically set CLICKABLE and LONG_CLICKABLE is set to true

setOnClickListener

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

setOnLongClickListener

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

Sliding conflict of View

1, Common sliding conflicts

Scenario 1: external sliding and internal sliding are inconsistent

Scenario 2: external sliding and internal sliding are consistent

Scenario 3: nesting of the above two cases

[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-8c9Hzk5i-1646358386610)(img/331079-20170122173803191-145084598.jpg)]

Scenario 1: it mainly talks about the problems caused by the page sliding effect composed of ViewPager and Fragment.

Scenario 2: during development, the inner and outer layers can slide up and down at the same time, or the inner and outer layers can slide left and right at the same time.

Scenario 3: it is the nesting of scenario 1 and scenario 2.

How to deal with

Judge who will intercept the event according to whether the sliding is horizontal or vertical. Several treatment methods:

  • External interception method. Click events are intercepted by the parent container first. If the parent container needs an event, it will be intercepted. If it does not need this event, it will not be intercepted. In this way, the problem of sliding conflict can be solved.
  • Internal interception method. The parent container does not intercept any events. All events are passed to the child element. If the child element needs this event, it will directly consume points, otherwise it will be handled by the parent container.

Scene 1

  • External interception method
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            intercepted = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;

            break;
        }
        default:
            break;
    }


    mLastX = x;
    mLastY = y;
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

MotionEvent.ACTION_MOVE: the event can be intercepted as needed. If the parent container needs to be intercepted, it returns true. If it does not need to be intercepted, it returns false. In this instance, if the horizontal sliding distance is greater than the vertical sliding distance, it will be intercepted

MotionEvent.ACTION_DOWN: generally return false, otherwise ACTION_MOVE and action_ The up will be intercepted by the parent container. In this instance, it is set to true to solve the problem that when the horizontal sliding is not over, if the user slides vertically quickly, the interface will not be able to slide to the end point in the horizontal direction and will be in an intermediate state

MotionEvent.ACTION_UP: false must be returned here. It is assumed that the event is handled by the child element if the parent container is in action_ If true is returned when up, the child element cannot receive ACTION_UP event. At this time, the onclick event in the child element will not be triggered, but the parent container is special. Once any event is intercepted, the subsequent events will be handed over to him for processing, even if it is set to false in onInterceptTouchEvent

  • Internal interception method
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

When a child element calls parent The parent element can only be intercepted when requestdisallowintercepttouchevent (false)

Scene 2

External interception method

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:public class CustomScrollView extends ScrollView {

    ListView listView;
    private float mLastY;
    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        super.onInterceptTouchEvent(ev);
        boolean intercept = false;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                listView = (ListView) ((ViewGroup)getChildAt(0)).getChildAt(1);
                   //The ListView slides to the top and continues to slide, allowing the scrollView to intercept events
                if (listView.getFirstVisiblePosition() == 0 && (ev.getY() - mLastY) > 0) {
                    //scrollView intercepts events
                    intercept = true;
                }
                //Slide listView to the bottom. If you continue to slide up, let scrollView intercept events
                else if (listView.getLastVisiblePosition() ==listView.getCount() - 1 && (ev.getY() - mLastY) < 0) {
                    //scrollView intercepts events
                    intercept = true;
                } else {
                    //scrollView is not allowed to intercept events
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:
                break;
        }
        mLastY = ev.getY();
        return intercept;
    }
}
            if(The parent container needs to handle the modification event by itself){
                intercepted = true;
            }else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
            default:
            break;
    }
    return intercepted;
}

Internal interception method

public class CustomListView extends ListView {

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

    //For listview/Y, set the initial value. The default value is 0.0 (one position of listview entry)
    private float mLastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //The ScrollView of the upper layer is not allowed to intercept events
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                //Satisfy listView to slide to the top. If it continues to slide, scrollView is allowed to intercept events
                if (getFirstVisiblePosition() == 0 && (ev.getY() - mLastY) > 0) {
                    //Allow ScrollView to intercept events
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                //The listView slides to the bottom. If you continue to slide up, the scrollView is allowed to intercept events
                else if (getLastVisiblePosition() == getCount() - 1 && (ev.getY() - mLastY) < 0) {
                    //Allow ScrollView to intercept events
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    //ScrollView is not allowed to intercept events under other circumstances
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        mLastY = ev.getY();
        return super.dispatchTouchEvent(ev);
    }
}

How View works

First of all, we need to understand some basic concepts of View, so as to better understand the measure ment, layout and draw process of View.

ViewRoot corresponds to the ViewRootImpl class. It is the link between WindowManager and DecorView. The three processes of View are completed through VeiwRoot. In the ActivityThread, after the Activity object is created, the DecorVeiw will be added to the Window, the ViewRootImpl object will be created, and the ViewRootImpl object will be associated with the DecorView.

The drawing process of View starts from the performTraversals method of ViewRoot. It goes through three processes: measure, layout and draw to finally draw a View. As shown in the figure:

  • performTraversals will call performMeasure, performLayout and performDraw in turn. These three methods complete the measure, layout and draw methods of the top-level View respectively,

  • In the onMeasure method, the measure process will be performed on all child elements. At this time, the measure process will be transferred from the parent container to the child element, so that the measure process will be completed once, and then the child element will repeat the process of the parent container, so as to complete the traversal of the whole View tree. Similarly, the other two steps are similar processes.

  • After the height and width of the measurement are measured, the height and width of the getmeasure view can be determined. In almost all cases, this width and height is the final width and height of the view. The layout process determines the coordinates and actual width and height of the four points of the view. The coordinates of four points can be obtained through getTop, getRight, getLeft and getBottom, and the actual width and height can be obtained through getWidth and getHeight. The process of drawing is the process of display. It can be finally displayed on the screen only after drawing is completed.

  • DecorView is actually a FrameLayout. The events of View layer pass through DecorView first and then pass to View.

MeasureSpec

MeasureSpec, measurement specification and measurement description are known from the name. Its function is to determine the measurement process of View, or it determines the size specification of View to a great extent. In addition, there are parent containers that also affect the creation process of View. During the measurement, the system will convert the LayoutParams of the View into the corresponding MeasureSpex according to the rules of the parent container, and then measure the width and height of the View according to the MeasureSpec.

MeasureSpec represents a 32-bit int value. The upper 2 bits represent SpecMode and the lower 30 bits represent SpecSize. SpecMode refers to the measurement mode, and SpecSize refers to the specification size in a certain measurement mode. MeasureSpec avoids excessive memory allocation by packaging SpecMode and SpecSize into an int value. In order to facilitate operation, it provides packaging and unpacking methods. The source code is as follows:

//Package SpecMode and SpecSize to obtain MeasureSpec  
public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
//Unpack MeasureSpec to obtain SpecMode
public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
//Unpack MeasureSpec to obtain SpecSize
 public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

SpecMode has three categories, each of which represents a special meaning:

  1. UNSPECIFIED parent containers do not have any restrictions on the size of the View. In this case, they are generally used inside the system to represent a measurement state.
  2. The exact parent container has detected the exact size required by the View. At this time, the final size of the View is the value specified by SpecSize, which corresponds to the match in LayoutParams_ Parent and specific numerical values.
  3. AT_ The most parent container specifies an available size, i.e. SpecSize. The size of the View cannot be greater than this value. The specific value depends on the specific implementation of different views. It corresponds to wrap in LayoutParams_ content.

Corresponding relationship between MeasureSpec and LayoutParams

  • For DecorView, its MeasureSpec is determined by the size of the window and its own LayoutParams;
  • For an ordinary View, its MeasureSpec is determined by the MeasureSpec of the parent container and its own LayoutParams. Once the MeasureSpec is determined, the width and height of the View can be determined in onMeasure.

For an ordinary View, the measure process of the View is passed from its ViewGroup. Here, take a look at the measureChildWithMargins method of the ViewGroup:

 @param child To be measured View
 * @param parentWidthMeasureSpec Parent container WidthMeasureSpec
 * @param widthUsed The space occupied by the horizontal direction of the parent container, such as other children of the parent container view Space occupied
 * @param parentHeightMeasureSpec Parent container HeightMeasureSpec
 * @param heightUsed The space occupied by the parent container, such as other children of the parent container view Space occupied
 */
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {

   //The first step is to obtain the LayoutParams of the child View
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

   //The second step is to obtain the WidthMeasureSpec of the sub view, in which several parameter descriptions are passed in:
   //Parent WidthMeasureSpec widthmeasurespec of parent container
   //Mpaddingleft + the Padding value of mpaddingright view itself, that is, the inner margin value
   //lp. leftMargin + lp. The Margin value of rightmargin view itself, that is, the outer Margin value
   //widthUsed parent container has occupied space value
   // lp.width view is the expected width with value
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
     //Get the hightmeasurespec of the child view
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

// Step 3: according to the obtained WidthMeasureSpec and hightmeasurespec of the child veiw 
   //Measure the sub view
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measure procedure of View

The workflow of View mainly refers to the three processes of measure, layout and draw, namely measurement, layout and drawing. Measure determines the measured width and height of View, layout determines the final width and height of View and the positions of four vertices of View, and draw draws the View to the screen.

4.1 measure process

The measure process should be divided into situations. If it is only an original View, the measurement process is completed through the measure method. If it is ViewGroup, you need to measure your own process first, and then traverse the measure method of calling sub elements. Each sub element executes the process in the basement. The following is a discussion of these two cases respectively.

4.1.1 Measure process of view

The measure process of View is completed by the measure method. The measure method is a final type, and subclasses cannot be overridden. However, the onMeasure method will be called in the measure() method of View. Therefore, we only need to analyze the onMeasure method. The source code is as follows:

/**
  * @param widthMeasureSpec Horizontal constraints imposed by the parent container
  * @param heightMeasureSpec Vertical constraints imposed by the parent container
  */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //Set the measured value of the height and width of the view
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

The setMeasuredDimension method is to set the measurement value of height and width for the View, which is obtained through the getDefaultSize method

/**
   * @param size view The default size of, which generally indicates that the android:minHeight attribute is set
   *  Or the size value of the View background picture 
   * @param measureSpec Constraint measureSpec of parent container
   * @return Returns the measured size of the view
   */
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
  //Get measurement mode
    int specMode = MeasureSpec.getMode(measureSpec);
  //Get dimensions
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
            //If the measurement mode is UNSPECIFIED, it means that there are no restrictions on the parent container and the child view, and the measurement size of the view is
          //Default size
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        //If the measurement mode is AT_MOST maximum measurement mode or actual precision measurement mode,
        //Then the measured size of View is the specSize of MeasureSpec
        //Given the size of the parent container (i.e. the current size of the parent container).
        result = specSize;
        break;
    }
    return result;
}

The logic of the getDefaultSize method is very simple. If the measurement mode is UNSPECIFIED, it means that there is no restriction on the parent container and the child view, then the measurement size of the view is the default size. If the measurement mode is at_ In the most maximum measurement mode or the actual accurate measurement mode, the measurement size of view is the specSize of MeasureSpec, that is, the given size of the parent container (the size of all the space currently remaining in the parent container).

Here, let's analyze the measurement height and width of View under the condition of UNSPECIFIED. The default size is obtained through getSuggestedMinimumWidth() and getSuggestedMinimumHeight() functions

protected int getSuggestedMinimumHeight() {
  return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

protected int getSuggestedMinimumWidth() {
  return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}

As can be seen from the getSuggestedMinimumHeight code, if the View has no background, the height of the View is mMinHeight, which is controlled by the android: minHeight attribute and can be 0 by default. If there is a background, the maximum value of mMinHeight and the minimum height of the background will be returned; Similarly, getSuggestedMinimumWidth is the same.

public  int  getMinimumWidth(){
    final  int  intrinsicWidth =  getInstrinsicWidth();
    return intrinsicWidth >0?intrinsicWidth  : 0;
}

The getMinimumWidth method returns the original width of the Drawable, provided that it is the original width of the Drawable, otherwise it returns 0.

measure procedure for ViewGroup

After that, the whole measurement process is summarized as follows:

  1. The first traversal measurement of the child view in the LinearLayout is mainly through the measureChildBeforeLayout method. The measureChildWithMargins method will be called inside this method, and the child will be called inside the measureChildWithMargins method Measure (childwidth measurespec, childheight measurespec) method. In this measurement process, if the sub view that meets the measurement conditions in step 1.1 does not need to be measured, it will be measured in step 5.1 later.
  2. According to the measurement of the height of each sub View, a preliminary total height mTotalLength value of LinearLayout will be obtained.
  3. If android:measureWithLargestChild = "true" attribute is set for LinearLayout and the measurement mode is AT_MOST or UNSPECIFIED recalculates the total height of mTotalLength.
  4. Determine the final LinearLayout height and status according to the measurement mode of highmeasurespec of LinearLayout and the measured total height mTotalLength.
  5. Re measure and determine the size of each sub View according to the measured LinearLayout height.
  6. Finally, execute the setMeasuredDimension method to set the measured height and width of LinearLayout.

Topics: Java Android