Introduction:

This chapter mainly introduces the View event distribution and sliding conflict problem solution, and can be compared with the Android event interception mechanism analysis.

The main content

  • View basics
  • The View of sliding
  • The elastic sliding
  • View event distribution mechanism
  • Sliding conflict

The specific content

View basics

View position parameters, MotionEvent and TouchSlop objects, VelocityTracker, GestureDetector and Scroller objects.

What is the view

View is the base class of all controls in Android. View itself can be a single space or a group of controls composed of multiple controls, namely ViewGroup. ViewGroup inherits from View and can have sub-views inside, thus forming the structure of View tree.

View position argument

The position of a View is determined by its four vertices: top, left, right, and bottom, representing the top, left, and right coordinates of the View, respectively. Also, we can get the size of the View:

width = right - left
height = bottom - top
Copy the code

These four parameters can be obtained as follows:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
Copy the code

With Android3.0, the View parameters x, y, translationX, and translationY were added. Where x and y are the coordinates of the top left corner of the View, and translationX and translationY are the offset of the top left corner of the View relative to the container. The conversion between them is as follows:

x = left + translationX;
y = top + translationY;
Copy the code
MotionEvent and TouchSlop
MotionEvent

Event type:

  • ACTION_DOWN The finger just touched the screen
  • ACTION_MOVE Indicates finger movement on the screen
  • ACTION_UP release finger from screen

Click event type:

  • Click the screen and leave the release, the event sequence is DOWN->UP
  • Click the screen to slide for a while and then release it. The sequence of events is DOWN->MOVE->… ->MOVE->UP

From the MotionEven object we can get the x and y coordinates of the event, which we can get by getX/getY and getRawX/getRawY. The difference is that getX/getY returns x and Y coordinates relative to the top left corner of the current View, while getRawX/getRawY returns X and Y coordinates relative to the top left corner of the phone screen.

TouchSlop

TouchSlop is the minimum distance recognized by the system that is considered to be sliding. This is a constant, device-dependent and can be obtained by:

ViewConfiguration.get(getContext()).getScaledTouchSlop()
Copy the code

When we handle slippage, such as slippage distances less than this value, we can filter the event (which is filtered by default) for a better user experience.

VelocityTracker, GestureDetector and Scroller
VelocityTracker

Speed tracking is used to track the speed of fingers in the sliding process, including horizontal speed and vertical speed. Usage:

  • Track the speed of the current click event in the View’s onTouchEvent method
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
Copy the code
  • Compute the velocity to get the horizontal velocity and the vertical velocity
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
Copy the code

Note that the speed must be calculated before the speed can be obtained, i.e., the method computeCurrentVelocity is called, where the speed refers to the number of pixels that the finger slides over a period of time, and 1000 refers to 1000 milliseconds, which is the number of pixels that the finger slides over in 1000 milliseconds. Speed can be positive or negative: speed = (terminal position – starting position)/time period

  • Finally, the clear() method is called to reset and reclaim memory when it is no longer needed
velocityTracker.clear();
velocityTracker.recycle();
Copy the code
GestureDetector

Gesture detection is used to help detect user behaviors such as clicking, sliding, long pressing, and double clicking. Usage:

  • Create a GestureDetector object and implement the OnGestureListener interface. If necessary, you can also implement the OnDoubleTapListener interface to listen for double-clicking behavior:
GestureDetector mGestureDetector = new GestureDetector(this);
// Solve the problem that the screen cannot be dragged after long pressing
mGestureDetector.setIsLongpressEnabled(false);
Copy the code
  • Add the following implementation to the OnTouchEvent method of the target View:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
Copy the code
  • Implement the OnGestureListener and OnDoubleTapListener interface methods:

OnSingleTapUp, onScroll, onLongPress and onDoubleTap are some of the most common ways to do this: click, onScroll, onLongPress and onDoubleTap. Suggestion: If you are only listening for slip-related, you can implement it yourself in onTouchEvent. If you are listening for double-clicking, use GestureDetector.

Scroller

Elastic sliding object, used to realize the elastic sliding of View. It does not allow the View to slide by itself, and needs to be used in conjunction with the View’s computeScroll method to accomplish this function. Usage:

Scroller scroller = new Scroller(mContext);
// Slowly move to the specified position
private void smoothScrollTo(int destX,int destY){
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    // Slide to destX in 1000ms
    mScroller.startScroll(scrollX,0,delta,0.1000);
    invalidata();
} 
@Override
public void computeScroll(a){
    if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX,mScroller.getCurrY()); postInvalidate(); }}Copy the code

The View of sliding

There are three ways to implement View sliding.

Use the scrollTo/scrollBy

ScrollBy actually calls scrollTo, which implements relative sliding based on the current position, while scrollTo implements absolute sliding.

ScrollTo and scrollBy can only change the position of the View’s contents, not its position in the layout. The positive and negative values of sliding offset mScrollX and mScrollY are opposite to the actual sliding direction, that is, sliding from left to right, mScrollX is negative, sliding from up, mScrollY is negative.

Use animation

TranslationX and translationY properties of the View can be used either in traditional View animation or in property animation. If you use property animation, in order to be compatible with versions below 3.0, You need to use the open source animation library NineolddAndroids. As with property animation :(View moves 100 pixels to the right at 100ms).

ObjectAnimator.ofFloat(targetView,"translationX".0.100).setDuration(100).start();
Copy the code
Changing layout properties

To move the View by changing the layout properties, change LayoutParams.

Comparison of various sliding modes
  • ScrollTo /scrollBy: Simple operation, suitable for sliding View content;
  • Animation: simple operation, mainly suitable for the View without interaction and achieve complex animation effects;
  • Change layout parameters: The operation is slightly more complex and is suitable for interactive views.

The elastic sliding

Use the Scroller

Typical use of Scroller for elastic sliding is as follows:

Scroller scroller = new Scroller(mContext);
// Slowly move to the specified position
private void smoothScrollTo(int destX,int dextY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    // Slide destX in 1000ms, the effect is slow slide
    mScroller.startSscroll(scrollX,0,deltaX,0.1000);
    invalidate();
} 
@override
public void computeScroll(a){
    if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); }}Copy the code

We will first construct a Scroller object and call its startScroll method, which does not make the view slide, but saves the parameters. Let’s look at the implementation of the startScroll method:

public void startScroll(int startX,int startY,int dx,int dy,int duration){
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAminationTimeMills();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0 f / (float)mDuration;
}
Copy the code

We can know the meanings of several parameters of startScroll method, startX and startY represent the starting point of sliding, dx and dy represent the distance of sliding, and duration represents the sliding time. Note that sliding here refers to the sliding of View content. Immediately after the startScroll method is called, the invalidate method is called, which is the start of the slide. The invalidate method causes the View to be redrawn. The computeScroll method is called in the View’s draw method, ComputeScroll will go to Scroller to get the current scrollX and scrollY; The slide is then performed using the scrollTo method, followed by a second redraw using the postInvalidate method, which loops until computeScrollOffset() returns a value of false. We can see how the computeScrollOffset method gets the current scrollX and scrollY:

public boolean computeScrollOffset(a){...int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime);
    if(timePassed < mDuration){
        switch(mMode){
        case SCROLL_MODE:
        final float x = mInterpolator.getInterpolation(timePassed * mDuratio
        nReciprocal);
        mCurrX = mStartX + Math.round(x * mDeltaX);
        mCurrY = mStartY + Math.round(y * mDeltaY);
        break; . }}return true;
}
Copy the code

Here we will see, basic computeScroll to Scroller for current scrollX and scrollY is acquired through calculating the percentage of the time, every time a redraw slide from the starting time there will be a time interval, Through this time interval, Scroller can get the current sliding position of the View, and then can complete the sliding of the View through the scrollTo method.

Through the animation

Animation is inherently an asymptotic process, so sliding through animation is inherently elastic. The implementation is also simple:

ObjectAnimator.ofFloat(targetView,"translationX".0.100).setDuration(100).start()
;    
// Of course, we can also use animation to simulate the Scroller elastic sliding process:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0.1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @override
    public void onAnimationUpdate(ValueAnimator animator){
    float fraction = animator.getAnimatedFraction();
    mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0); }}); animator.start();Copy the code

The above animation is essentially not applied to any object, it just completed the entire animation process in 1000ms. Using this feature, we can get the proportion of animation completion at the arrival of each frame of the animation, and calculate the distance of the View sliding according to the proportion. Other animation effects can be achieved using this method, and we can add custom actions to the onAnimationUpdate method.

Use a delay strategy

The core idea of the delay strategy is to achieve an asymptotic effect by sending a series of delay information, specifically through the postDelayed method of Hander and View, or the sleep method of the thread. The following uses Handler as an example:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
    public void handleMessage(Message msg){
    switch(msg.what){
        case MESSAGE_SCROLL_TO:
        mCount ++ ;
        if (mCount <= FRAME_COUNT){
            float fraction = mCount / (float) FRAME_COUNT;
            int scrollX = (int) (fraction * 100);
            mButton1.scrollTo(scrollX,0);
            mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
            } 
        break;
        default : break; }}}Copy the code

View event distribution mechanism

Delivery rules for click events

Click events are motionEvents. First let’s take a look at the following pseudocode to understand the click event passing rule:

public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
    consume = onTouchEvent(ev);
} else {
    consume = child.dispatchTouchEnvet(ev);
} 
return consume;
}
Copy the code

The above code mainly involves the following three methods:

  • public boolean dispatchTouchEvent(MotionEvent ev);

This method is used for event distribution. This method is called if the event is passed to the current View. The return result indicates whether or not the event is consumed, influenced by onTouchEvent and the dispatchTouchEvent method of the subordinate View.

  • public boolean onInterceptTouchEvent(MotionEvent ev);

This method is used to determine whether to intercept events. Called in the dispatchTouchEvent method. The result is returned indicating whether to intercept.

  • public boolean onTouchEvent(MotionEvent ev);

This method is used to handle click events. Called in the dispatchTouchEvent method and returns a result indicating whether the event is consumed. If not, the current View cannot receive the event again in the same sequence of events.

Click event delivery rules: For a root ViewGroup, the click event is first passed to the root ViewGroup and its dispatchTouchEvent method is called. If the onInterceptTouchEvent method returns true, the root ViewGroup wants to intercept the event. The event is then handed over to the ViewGroup, calling the ViewGroup’s onTouchEvent method; If the ### onInterceptTouchEvent method returns false, the ViewGroup does not intercept the event. Then the event is passed to its child View. The child View’s dispatchTouchEvent method, This is repeated until the event is finally processed. When a View needs to handle an event, if it has OnTouchListener set, the onTouch method is called. If onTouch returns false, the onTouchEvent method of the current View is called. If onTouch returns true, the onTouchEvent method is not called. If OnClickListener is set in the onTouchEvent method, its onClick method will be called.

This shows the priority relationship for handling events: onTouchListener > onTouchEvent >onClickListener

Here are some conclusions about the mechanism of event transmission:

  • An event sequence begins with a Down event, contains an indefinite number of move events, and ends with an up event.
  • Normally, a sequence of events can only be intercepted and consumed by one View.
  • When a View intercepts an event, the sequence of events can only be handled by it, and its onInterceptTouchEvent

Will not be called again.

  • Once a View has started processing events, if it does not consume ACTION_DOWN events (onTouchEvnet returns false), then no other events in the same sequence of events are handed to it and the event is redirected to its parent, whose onTouchEvent is called.
  • If the View consumes no events other than ACTION_DOWN, the event will disappear, the parent element’s onTouchEvent will not be called, and the current View will continue to receive subsequent events, and the final missing click event will be passed to the Activity for processing.
  • ViewGroup does not intercept any events by default.
  • The View does not have an onInterceptTouchEvent method; its onTouchEvent method will be called once the event is passed to it.
  • The View’s onTouchEvent consumes events by default, unless it is unclickable (both clickable and longClickable are false). A View’s longClickable property defaults to false, and clickable defaults to false (TextView is false, button is true).
  • The Enable property of the View does not affect the default return value of onTouchEvent.
  • OnClick can only happen if the current View is clickable and receives down and Up events.
  • Events always outside-in process, namely event always passed to the first parent element, and then distributed to the View, by the parent element through requestDisallowInterceptTouchEvent method can intervene in the parent element in child elements distribution, except ACTION_DOWN events.
Source code parsing for event distribution

slightly

Sliding conflict

In an interface, sliding conflicts occur whenever both the inner and outer layers can slide simultaneously. There are fixed ways to solve sliding conflicts.

Common sliding conflict scenarios

  1. External sliding and internal sliding direction is not consistent;

For example, viewPager and ListView are nested, but in this case the ViewPager itself already handles sliding conflicts. 2. The external sliding direction is consistent with the internal sliding direction; 3. The nesting of the above two cases can be solved by solving 1 and 2.

Handling rules for sliding conflicts

For scenario 1, the rules are: when the user swipes left and right (up and down), you need to have the external View intercept the click event, and when the user swipes up and down (left and right), you need to have the internal View intercept the click event. Determine who intercepts the event based on the direction of the slide.

For scenario 2, because the sliding direction is consistent, the service can only find a breakthrough point at this time. According to the service requirements, it is stipulated when the external View blocks the event, and when the internal View blocks the event.

Scenario 3 is relatively complex, and the service breakthrough is also found based on the requirements.

Sliding conflict resolution
External interception

The so-called external interception method means that click events are first intercepted by the parent container, if the parent container needs this event to be intercepted, otherwise not intercepted. Here is the pseudocode:

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;
    break;
case MotionEvent.ACTION_MOVE:
    if(The parent container requires the current event) {intercepted =true;
    } else {
    intercepted = false;
    } 
    break;
case MotionEvent.ACTION_UP:
    intercepted = false;
    break;
default : 
    break;
} 
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
Copy the code

For different conflicts, just modify the condition that the parent container requires the current event. Others do not need to be modified and cannot be modified.

  • ACTION_DOWN: False must be returned. Because if true is returned, subsequent events are intercepted and cannot be passed to child views.
  • ACTION_MOVE: Decide whether to intercept as required
  • ACTION_UP: False must be returned. If blocked, the child View cannot accept the up event and cannot complete the click operation. If it is the parent container that needs this event, it has already been intercepted in ACTION_MOVE. According to conclusion 3 in the previous section, ACTION_UP will not go through the onInterceptTouchEvent method and will be directly handled by the parent container.
Internal interception method

Internal interception means that the parent container does not intercept any events, all events are passed to the child element, if the child element needs the event directly consumed, otherwise the parent container processing. This method does not agree with Android events distribution mechanism, need to match the requestDisallowInterceptTouchEvent method to work properly. Here is the pseudocode:

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

In addition to the child element needs to be done outside the processing, the parent element will default to intercept besides ACTION_DOWN of other events, such child elements will call the parent. RequestDisallowInterceptTouchEvent (false) approach, the parent can continue to intercept the events. Therefore, the parent element is modified as follows:

public boolean onInterceptTouchEvent (MotionEvent event) {
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true; }}Copy the code

External interceptor instance: HorizontalScrollViewEx