Please indicate the source in the form of link:

This post is from the blog 103Style

“Android Development art Exploration” learning record

base on Android-29

In this paper, Scroller is used to realize elastic sliding. If you don’t understand, you can first look at the sliding implementation mode of View.

Demo source address.


directory

  • Common sliding conflict scenarios
  • Handling rules for sliding conflicts
  • Sliding conflict resolution
  • To verify
    • Handle horizontal and vertical slip conflicts
    • Handle horizontal slide, vertical slide, horizontal slide together

Common sliding conflict scenarios

The main conflict scenarios are:

  • The external sliding direction is inconsistent with the internal sliding direction
  • The external sliding direction is consistent with the internal sliding direction
  • The above two cases are nested

As shown in figure:

  • In the first scene, the external sliding direction is inconsistent with the internal sliding direction, which mainly occurs in:

    • The home page ViewPager and Fragment are used together to form the page sliding effect. In this case, swipe left or right to switch the Fragment, and the Fragment is basically RecyclerView.
    • Vertical sliding RecyclerView item nested inside the horizontal sliding RecyclerView.

    There should be a sliding conflict between the two types, but the RecyclerView and ViewPager help us with that.

  • Sliding inside the second scenario outside sliding direction and in the same direction, this kind of situation is a little bit complicated, both layer are the horizontal slip or vertical sliding, fingers sliding, which layer doesn’t know what the user wants to slide, slide so when there will be a problem, or only a sliding layer, or two layers are on the slide.

  • The third scenario, where the external and internal sliding directions are inconsistent and the external and internal sliding directions are nested in the same direction, is more complicated. Like now “mobile QQ” Android side of the message column, there is a sliding up and down the message list, each message can be left to delete, the message list can be right to pull out the user menu. As complicated as it may seem, there are actually several single conflicts stacked on top of each other, and we just have to fight them one by one.


Handling rules for sliding conflicts

In general, sliding conflicts, no matter how complex they are, have established rules so that we can choose the appropriate way to handle them.

For scenario 1 above: the external sliding direction is inconsistent with the internal sliding direction, we only need to let the external View intercept the click event when the user slides left and right, and let the internal View intercept and process when the user slides up and down. That is, the direction of the slide is determined by the coordinates between the two points during the slide to determine who should intercept.

For scenario 2: the external sliding direction is the same as the internal sliding direction, which is special, because the internal and external sliding direction is the same, we can not handle the same as scenario 1, which requires us to find a breakthrough from the business, according to the specific requirements of the business to determine whether the external or internal View to intercept and process events.

Scenario 3 is a mixture of scenario 1 and Scenario 2. You can refer to the processing rules of scenario 1 and Scenario 2 directly.


Sliding conflict resolution

There are two main solutions: external interception and internal interception.

External interception

The onInterceptTouchEvent method is overwritten by the onInterceptTouchEvent method, as shown in the following example:

private float lastEventX,lastEventY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept = false;
    float x = ev.getX();
    float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if(The parent container requires the current click event) {intercept =true;
            } else {
                intercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercept = false;
            break;
        default:
            intercept = false;
            break;
    }
    lastEventX = x;
    lastEventY = y;
    return intercept;
}
Copy the code

If the parent intercepts an event in onInterceptTouchEvent (returning true), subsequent events will not be passed to the child View. But if we consume the MOVE event directly in dispatchTouchEvent, the child element that previously handled the DOWN event will still receive the UP event.

Internal interception method

Is refers to the parent container not intercept any events, all events are passed to the child elements, if the child element to deal with direct consume, or to the parent container, here the child elements need to cooperate with requestDisallowInterceptTouchEvent (true) can work normally, use a little bit complicated, The following is an example:

private float lastEventX,lastEventY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    float x = ev.getX();
    float y = ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            float dx = x - lastEventX;
            float dy = y - lastEventY;
            if(the parent container handling) {getParent () requestDisallowInterceptTouchEvent (false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    lastEventX = x;
    lastEventY = y;
    return super.dispatchTouchEvent(ev);
}
Copy the code

FLAG_DISALLOW_INTERCEPT is reset for DOWN events, so for DOWN events, A ViewGroup is always checked by onInterceptTouchEvent. Therefore, the DOWN event cannot be intercepted.

Let’s verify the above two methods with an example.


To verify

Let’s simply implement a HorizontalScrollerView that slides horizontally and a VerticalScrollerView that slides vertically to verify that.

First we will simply implement the next HorizontalScrollerView and VerticalScrollerView, the following is attached to the event processing logic, complete source can click on the above two links:

//HorizontalScrollerView.java
public class HorizontalScrollerView  extends ViewGroup {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ...
        switch (event.getAction()) {
            ...
            caseMotionEvent.ACTION_MOVE: int dx = (int) (x - lastX); // scrollBy(-dx, 0);break;
            caseMotionEvent.ACTION_UP: int scrollX = getScrollX(); / / the speed of calculation within 1 s velocityTracker.com puteCurrentVelocity (1000); // Get the horizontal sliding speedfloat xVelocity = velocityTracker.getXVelocity();

                if (Math.abs(xVelocity) > 50) {
                    childIndex = xVelocity > 0 ? childIndex - 1 : childIndex + 1;
                } else{ childIndex = (scrollX + mChildWidth / 2) / mChildWidth; } childIndex = Math.max(0, Math.min(childIndex, mChildSize - 1)); Int sx = childIndex * McHildwidth-scrollx; // Use Scroller to smooth the slide smoothScrollBy(sx); / / remove velocityTracker. The clear ();break;
            default:
                break;
        }
        return true; }}Copy the code
//VerticalScrollerView.java
public class VerticalScrollerView extends ViewGroup {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if(! scroller.isFinished()) { scroller.abortAnimation(); }break;
            caseMotionEvent.ACTION_MOVE: int dy = (int) (y - lastY); // Follow the finger to scrollBy(0, -dy);break;
            case MotionEvent.ACTION_UP:
                int scrollY = getScrollY();
                if (scrollY < 0) {
                    smoothScrollBy(-scrollY);
                } else if (mContentHeight <= mHeight) {
                    smoothScrollBy(-scrollY);
                } else if (mContentHeight - scrollY < mHeight) {
                    smoothScrollBy(mContentHeight - scrollY - mHeight);
                } else{// Inertial sliding effect}break;
            default:
                break;
        }
        lastX = x;
        lastY = y;
        return true; }}Copy the code

Both are basically similar, both are the logic for dealing with sliding.

Then we configure writing to XML:

<com.lxk.slidingconflictdemo.HorizontalScrollerView
    android:id="@+id/tvp_test"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginBottom="@dimen/tab_layout_height">
    <com.lxk.slidingconflictdemo.VerticalScrollerView
        android:id="@+id/rsv1"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <com.lxk.slidingconflictdemo.VerticalScrollerView
        android:id="@+id/rsv2"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <com.lxk.slidingconflictdemo.VerticalScrollerView
        android:id="@+id/rsv3"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</com.lxk.slidingconflictdemo.HorizontalScrollerView>
Copy the code

Then dynamically add child controls to each VerticalScrollerView:

private void setupRsv(VerticalScrollerView verticalScrollerView) {
    ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    layoutParams.topMargin = 32;
    for (int i = start; i < count; i++) {
        AppCompatButton button = new AppCompatButton(this);
        button.setLayoutParams(layoutParams);
        button.setText(String.valueOf(i));
        verticalScrollerView.addView(button);
    }
    updateData();
}
Copy the code

It runs like this:

We can see that it slides vertically because the event is consumed by the inner VerticalScrollerView, so the outer HorizontalScrollerView can’t slide.

Let’s deal with this conflict using the external and internal interception methods described above.


External interception handles conflicts

We’ll override the onInterceptTouchEvent method from the HorizontalScrollerView to intercept the event if the horizontal slider is larger than the vertical slider:

public class HorizontalScrollerView extends ViewGroup { @Override public boolean onInterceptTouchEvent(MotionEvent ev) {  boolean intercept;float x = ev.getX();
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                ...
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = x - lastInterceptX;
                floatdy = y - lastInterceptY; // Intercept = math.abs (dx) > math.abs (dy);break;
            case MotionEvent.ACTION_UP:
            default:
                intercept = false;
                break; }...returnintercept; }}Copy the code

Run the program:


Internal interception handles conflicts

Then we’ll try using internal interception, so we’ll override VerticalScrollerView’s dispatchTouchEvent method to disallow the parent control to intercept the event on ACTION_DOWN. Then when the horizontal slide distance is greater than a certain value of the vertical slide distance, the parent control is allowed to intercept, set to 50.

public class VerticalScrollerView extends ViewGroup{
    private float lastX, lastY;
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = x - lastX;
                float dy = y - lastY;
                if (Math.abs(dx) > Math.abs(dy) + 50) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
            default:
                break;
        }
        returnsuper.dispatchTouchEvent(ev); }}Copy the code

And modify the onInterceptTouchEvent method of HorizontalScrollerView to not intercept only ACTION_DOWN events.

public class HorizontalScrollerView extends ViewGroup { @Override public boolean onInterceptTouchEvent(MotionEvent ev) {  switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:
                if(! scroller.isFinished()) { scroller.abortAnimation();return true;
                }
                return false;
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
            default:
                return true; }}}Copy the code

Operation effect:

Now let’s look at a scenario with horizontal and vertical conflict.


Simulation of internal and external sliding is inconsistent and there are also scenarios where external and internal sliding are consistent

Here we do not agree to simulate the sliding inside and outside And there are external and internal slide consistent scenario, we add a horizontal sliding to VerticalScrollerView child View for ItemHorizontalScrollerView, The code and the HorizontalScrollerView, here will not paste, source address point I.

We then add it to the first pane of the original list in HomeActivity, where we disable the event handling of the inner View for testing purposes.

private void addItemHorizontalScrollerView(VerticalScrollerView verticalScrollerView) {
    ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    ItemHorizontalScrollerView itemHorizontalScrollerView = new ItemHorizontalScrollerView(this);
    itemHorizontalScrollerView.setLayoutParams(layoutParams);
    int itemCount = 10;
    ViewGroup.MarginLayoutParams itemLP = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    for (int i = 0; i < itemCount; i++) {
        AppCompatButton button = new AppCompatButton(this);
        button.setLayoutParams(itemLP);
        button.setText(String.valueOf(i));
        button.setClickable(false);
        button.setLongClickable(false);
        itemHorizontalScrollerView.addView(button);
    }
    verticalScrollerView.addView(itemHorizontalScrollerView);
}
Copy the code

Run the program:

So let’s deal with this conflict together, and we’re gonna use internal interception to deal with this.

First of all, let’s define the rules: when sliding inside the horizontal sliding child View, let the internal child View horizontal sliding first, when sliding to the left or right side, and then the event to the upper level to deal with.

Let’s work from the outside in:

First let’s take a look at the HorizontalScrollerView, which does not need to be modified and intercepts events other than ACTION_DOWN.

public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if(! scroller.isFinished()) { scroller.abortAnimation();return true;
            }
            return false;
        case MotionEvent.ACTION_MOVE:
        case MotionEvent.ACTION_UP:
        default:
            return true; }}Copy the code

And then VerticalScrollerView, when we were dealing with the conflict with the HorizontalScrollerView, we handled ACTION_DOWN in dispatchTouchEvent and we didn’t allow the parent View to intercept the event, Then allow the parent View to intercept the event in ACTION_MOVE when the horizontal slide is larger than the vertical slide. Obviously this is unreasonable, because we need to make ItemHorizontalScrollerView priority event. So we changed it to only allow the parent View to block events in ACTION_DOWN Settings.

public boolean dispatchTouchEvent(MotionEvent ev) {
    x = ev.getX();
    y = ev.getY();
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        getParent().requestDisallowInterceptTouchEvent(true);
    }
    boolean res = super.dispatchTouchEvent(ev);
    lastX = x;
    lastY = y;
    return res;
}
Copy the code

Finally, we see ItemHorizontalScrollerView, first of all like VerticalScrollerView, set in the dispatchTouchEvent, ACTION_DOWN father does not allow the View to intercept events.

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        getParent().requestDisallowInterceptTouchEvent(true);
    }
    return super.dispatchTouchEvent(ev);
}
Copy the code

And then we’re going to handle that in onTouchEvent and when to hand that event over to the parent View:

  • First we have to consume itACTION_DOWNOr the subsequent events will not be transmitted.
  • Then we need to handle the left and left and right and right sliders in ACTION_MOVE and hand the event over to the parent View. Other circumstances we let ItemHorizontalScrollerView sliding.

GetScrollX () is 0 when it is at the far left, getScrollX() is 0 when it is at the far right, and getScrollX() is the width of the content minus the width of the View.

So we modify the ACTION_MOVE event in onTouchEvent as follows:

Cut back the part of the code / / ItemHorizontalScrollerView. Java public Boolean onTouchEvent (MotionEvent event) {float x = event.getX();
    int scrollX = getScrollX();
    boolean used = false;
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            ....
            break;
        case MotionEvent.ACTION_MOVE:
            int dx = (int) (x - lastX);
            if(scrollX <= 0 && dx > 0) {// when on the left and sliding leftif (scrollX == 0) {
                    dx = 0;
                } else{ dx += scrollX; }}else if(scrollX + mWidth >= mContentWidth && dx < 0) {// In the far right and right sliderif (scrollX + mWidth >= mContentWidth) {
                    dx = 0;
                } else{ dx += scrollX + mWidth - mContentWidth; }}else {
                used = true; } // Follow the finger scrollBy(-dx, 0); // Events are handed to the parent control when left and right slides are not requiredif(dx == 0 && ! used) { getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
    }
    lastX = x;
    return used;
}
Copy the code

Run the program here to see:

Here we can see that the inside item slides normally, but there is a problem. The outer horizontal sliding View does not slide.

Here because we are ItemHorizontalScrollerView given to VerticalScrollerView to deal with the incident, but VerticalScrollerView father did not allow the View to intercept, So we’ll just add the same logic to onTouchEvent that we used to handle ACTION_MOVE with dispatchTouchEvent:

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            ...
            break;
        caseMotionEvent.ACTION_MOVE: int dx = (int) (x - lastX); int dy = (int) (y - lastY); // Follow the finger to scrollBy(0, -dy); // Allow the parent View to block when the horizontal slide distance is greater than the vertical slide distanceif (Math.abs(dx) > Math.abs(dy) + 50) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            ...
            break;
        default:
            break;
    }
    return true;
}
Copy the code

Run the program:

We can see that the sliding effect is basically working.

You can try your hand at dealing with the conflict between the outer vertical direction and the inner vertical direction.

Demo source code address


If there is a description wrong, please remind me, thank you!

The above

If you like it, please give it a thumbs up.


Scan the qr code below, follow my public account Android1024, click attention, don’t get lost.