preface

Before reading this blog, if you do not know enough about the View event distribution process, you can read and understand my first few published a View event distribution source parsing article (mainly analyzed the source View event distribution process). The article lacks the relevant introduction of View sliding conflict, so today we will talk about the relevant content of sliding conflict and give a specific example of sliding conflict and its solution. Think about and practice what this article example and View event distribution source code can do: Mom no longer has to worry that I can’t read the source code or someone else wrote the event distribution code, and can solve common sliding conflicts.

Brief Analysis of common Scenes

To help you better absorb the knowledge of this blog, I will describe a common scenario to help you understand the event distribution process. You must have seen this scenario: set a click event for RecyclerView Item. There are two situations when clicking on this Item:

  1. Quick click, directly trigger the click event of Item
  2. Press the finger to this Item and then start to slide, at this time RecyclerView starts to slide with the finger

Scenario 1 can be simply understood as: when a quick click is performed, the onTouchEvent of the View returns true by default due to the click event set on Item, and then the sliding distance is smaller than the scaledTouchSlop(minimum sliding distance) of the device, so the click event is triggered.

In scenario 2, when we press our finger down, it is clear that the ItemView has already consumed the event in ACTION_DOWN (returning true). At this point we guess: The initial ACTION_DOWN event was passed to the ItemView, and the subsequent ACTION_MOVE event was intercepted by the RecyelerView because the slider distance was larger than scaledTouchSlop. And then the subsequent events are RecyclerView to handle. In order to confirm our guess, the old rules or find the answer from the source code, first of all we analyze the RecyclerView onInterceptTouchEvent method

//RecyclerView.java./ / 0
private intmScrollState = SCROLL_STATE_IDLE; .@Override
public boolean onInterceptTouchEvent(MotionEvent e) {.../ / 1
   final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
   final booleancanScrollVertically = mLayout.canScrollVertically(); .switch (action) {
       ...
       case MotionEvent.ACTION_MOVE: {
               	...
                final int x = (int) (e.getX(index) + 0.5 f);
                final int y = (int) (e.getY(index) + 0.5 f);
         				/ / 2
                if(mScrollState ! = SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    boolean startScroll = false;
                  	/ / 2
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = x;
                        startScroll = true;
                    }
                    / / 3
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = y;
                        startScroll = true;
                    }
                  	/ / 4
                    if(startScroll) { setScrollState(SCROLL_STATE_DRAGGING); }}}break; . }/ / 5
   return mScrollState == SCROLL_STATE_DRAGGING;
}
Copy the code

Let’s rearrange the logic of the above code:

  • The code at annotation 5 is the return value of onInterceptTouchEvent. Since the initial value of mScrollState is SCROLL_STATE_IDLE, So we can see that RecyclerView does not block ACTION_DOWN events when it receives them. If ACTION_DOWN events are blocked, then all its child views can’t get touch events. That’s why the ItemView gets the event when ACTION_DOWN goes in.
  • The two variables in note 1 are used to indicate whether the RecyclerView is sliding vertically or horizontally
  • If RecyclerView is sliding horizontally and the sliding value exceeds mTouchSlop, set startScroll to true, then the code at mark 4 below will set scrollState to SCROLL_STATE_DRAGGING, This then causes the onInterceptTouchEvent method to return true. The same is true for the code at 3.

OnInterceptTouchEvent summary

RecyclerView intercepts touch events when the finger’s sliding distance is greater than the minimum.

DispatchTouchEvent method analysis

After understanding the logic of RecyclerView onInterceptTouchEvent method, we then analyze the process of RecyclerView dispatchTouchEvent method. Because RecyclerView did not rewrite dispatchTouchEvent method, so we can directly analyze ViewGroup dispatchTouchEvent method, the relevant core source code is as follows:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {...// Check for interception.
    final boolean intercepted;
    / / 1
    if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
        / / if the child call requestDisallowInterceptTouchEvent (true) disallowIntercept to true
        final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
        if(! disallowIntercept) {/ / 2
          intercepted = onInterceptTouchEvent(ev);
          ev.setAction(action); // restore action in case it was changed
        } else {
          intercepted = false; }}... TouchTarget target = mFirstTouchTarget;while(target ! =null) {
        finalTouchTarget next = target.next; ./ / 3
        final boolean cancelChild = resetCancelNextUpFlag(target.child)
          || intercepted;
        / / 4
        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                          target.child, target.pointerIdBits)) {
          handled = true;
        }
        / / 5
        // If cancelChild is true then the mFirstTouchTarget header is removed and reclaimed
        if (cancelChild) {
          if (predecessor == null) {
            mFirstTouchTarget = next;
          } else {
            predecessor.next = next;
          }
          target.recycle();
          target = next;
          continue; } predecessor = target; target = next; }}//View.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
   final boolean handled;

   final int oldAction = event.getAction();
   // Enter the code block if cancel is true
   if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
     // Resetting event to MotionEvent.ACTION_CANCEL triggers the child's ACTION_CANCEL
     event.setAction(MotionEvent.ACTION_CANCEL);
     if (child == null) {
       handled = super.dispatchTouchEvent(event);
     } else {
       handled = child.dispatchTouchEvent(event);
     }
     event.setAction(oldAction);
     returnhandled; }... }Copy the code

If you have seen my previous post of View event distribution source parsing article, the above code should be familiar, have not seen the recommendation to take a look at this piece of content. Here I will briefly outline the flow of the current scene again:

  1. The ACTION_DOWN event comes into the onInterceptTouchEvent method of RecyclerView. RecyclerView does not intercept it, and it is processed by ItemView. Then ItemView can click, so it consumes the Down event by default. This also results in mFirstTouchTarget not being null and the Down event flow terminates.
  2. Then a series of ACTION_MOVE events come, because at this time, mFirstTouchTarget is not empty, so we still go into the RecyclerView 2 onInterceptTouchEvent method, at this time, if our sliding distance exceeds the minimum sliding event, Then RecyclerView will return true internally, causing intercepted to be true
  3. After step 2, 3 and then leads to the labeling of cancelChild also is true, to mark four dispatchTransformedTouchEvent method, caused the ItemView ACTION_CANCEL trigger
  4. CancelChild: True cancelChild: true cancelChild: True cancelChild: True cancelChild: True cancelChild: True cancelChild: Null Directly RecyclerView itself began to consume the follow-up events series.

At this point we have basically figured out the overall flow of event delivery in Scenario 2. At this point one might ask what’s the use of talking about the flow of this scenario? In fact, once this scene is sorted out, we will have a clearer picture of the overall event distribution process, and whether the change of event flow direction is also involved in the middle, which can be a paving for us to solve the sliding conflict.

Summary:

From the above analysis, we can know that:

  1. While the child view consumes the event, the parent view can still use the onInterceptTouchEvent to intercept the event when appropriate and give it to itself
  2. If the child view gets the event and doesn’t want the parent view to intercept it, By calling the requestDisallowInterceptTouchEvent (true) to disable the parent view intercept events (parent view won’t call her onInterceptTouchEvent method)

ViewPager2 nested RecyclerView sliding conflict analysis and solution

ViewPager2 is Google’s new control to replace the ViewPager in the last two years, ViewPager2 support functions ViewPager2 are supported, and ViewPager2 also supports vertical direction. This blog post does not introduce the use of ViewPager2. If you want to know the use of ViewPager2, you can see the ViewPager2Sample at the end of the article. Here’s a quick look at the general structure of ViewPager2:

//ViewPager2.java
//ViewPager2 inherits from ViewGroup
public final class ViewPager2 extends ViewGroup {
  
    public ViewPager2(@NonNull Context context) {
        super(context);
        initialize(context, null);
    }

    public ViewPager2(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs); initialize(context, attrs); }...private void initialize(Context context, AttributeSet attrs) {...// Create a RecyclerView
      	 mRecyclerView = newRecyclerViewImpl(context); .// Add recyclerView to ViewPager2
         attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams()); }}Copy the code

ViewPager2 inherited from ViewGroup and added a RecyclerView, so we can basically simply think of ViewPager2 as a RecyclerView. Now let’s imagine a scenario, if the horizontal slide ViewPager2 one of the pages wrapped in a horizontal slide RecyclerView, horizontal slide who consumption? Through the analysis of RecyclerView at the beginning of the article, when the horizontal sliding distance exceeds the minimum sliding distance, the RecyclerView inside ViewPager2 will intercept events, so basically we can not change the RecyclerView of sub-page, we can try. Now that we know the cause of the sliding conflict, it’s time to think about general solutions… Em… Ok, ten minutes have passed. The solution can be solved by following the following steps:

  1. Since RecyclerView does not intercept Down events, we can request the parent View not to intercept events when receiving down events
  2. When the subsequent move event comes to us and the horizontal sliding distance is greater than the minimum sliding distance, then ask our child RecyclerVIew can slide in this sliding direction, if so, continue to forbid the parent class to intercept events. If not, the parent class is allowed to intercept the event

Want to clear the steps to solve the problem, so we can directly start to write, first make a general official program, using the following NestedScrollableHost as RecyclerView container can solve the sliding conflict, the specific code and notes are as follows:

class NestedScrollableHost : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f
    
    // Loop through to find viewPager2
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while(v ! =null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }

    // Find the RecyclerView
    private val child: View? get() = if (childCount > 0) getChildAt(0) else null

    init {
        // Minimum sliding distance
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }

    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            // Check whether the RecyclerView can slide deltaX horizontally
            0-> child? .canScrollHorizontally(direction) ? :false
            // Check whether the RecyclerView can slide deltaY in the vertical direction
            1-> child? .canScrollVertically(direction) ? :false
            else -> throw IllegalArgumentException()
        }
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        handleInterceptTouchEvent(e)
        return super.onInterceptTouchEvent(e)
    }

    private fun handleInterceptTouchEvent(e: MotionEvent) {
        valorientation = parentViewPager? .orientation ? :return

        // If the RecyclerView cannot slide in the sliding direction of viewPager2, return directly
        if(! canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return
        }

        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            // The down event directly fordisallows the parent view to intercept the event, and the subsequent events are given to the child RecyclerView to determine whether the event can be consumed
            // If this section does not enforce the parent view, it will cause subsequent events to be directly blocked by the parent view before the child RecyclerView
            // By default, RecyclerView onTouchEvent returns true but viewPager2 will be intercepted by onInterceptTouchEvent
            parent.requestDisallowInterceptTouchEvent(true)}else if (e.action == MotionEvent.ACTION_MOVE) {
            // Calculate the finger sliding distance
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL

            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f

            // The sliding distance exceeds the minimum sliding value
            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                if (isVpHorizontal == (scaledDy > scaledDx)) {
                    // If viewPager2 slides horizontally but gesture slides vertically, all parent classes are allowed to intercept
                    parent.requestDisallowInterceptTouchEvent(false)}else {
                    // Gesture slide direction and viewPage2 is the same direction, need to ask the child RecyclerView can slide in the same direction
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        // Child RecyclerView can slide directly to disable the parent view to intercept events
                        parent.requestDisallowInterceptTouchEvent(true)}else {
                      // The child RecyclerView cannot be used to slide right when the first Item is used or left when the last Item is used
                        parent.requestDisallowInterceptTouchEvent(false)}}}}}}Copy the code

The above code annotations have been annotated in detail, so you can understand and try a wave. But the general idea doesn’t escape what we’ve been saying:

  1. The Down event first disallows the parent from intercepting the event, leaving it up to us to determine whether our child view consumes the event
  2. Through requestDisallowInterceptTouchEvent () to ban and allow the parent view at the right time to intercept events
  3. Through child view whether consumption requestDisallowInterceptTouchEvent events to invoke the method ()

After the above solution is understood, can we also achieve the same effect by rewriting the event distribution process of RecyclerView? The answer is yes. The answer link in me making project ViewPager2Sample NestedScrollableRecyclerView, everybody can see, ha ha ha ha ~ ~ ~ ~

conclusion

Through the overall analysis above, we can come to the conclusion that: Can change the View the touch event flow of the method is mainly the parent View onInterceptTouchEvent method when intercepting events and child View call requestDisallowInterceptTouchEvent method to prohibit or allow the parent View intercept events, Common sliding conflicts can be resolved by using these two methods properly, depending on the business scenario and the classes involved.

Welcome to try to use the library I wrote super easy to use highlighted boot library Github address is as follows: github.com/hyy920109/H… Have what inadequacy place many put forward

Recommended reading

  1. 【View series 】View event distribution source code exploration
  2. [View series] View measure process source code full analysis
  3. 【View Series 】 UNSPECIFIED, MeasureSpec is still available.