ViewPager2 Sliding Conflict Resolution

  • 2021-12-11 09:06:44
  • OfStack

Since the official release of ViewPager2 in December last year, ViewPager2 has gradually begun to replace the old version of ViewPager. Many developers have also used ViewPager2 in their projects. Compared with ViewPager, ViewPager2 is not powerful. I wrote an article before, "Learn if you can't move! The use of ViewPager2 is explained in detail in "In-depth understanding of ViewPager2". However, because there was not much actual combat at that time, no serious sliding conflict was found in the nested use of ViewPager2. This problem was not discovered until ViewPager2 was reconstructed in March this year. Therefore, in BVP version 3.0, ViewPager2 was additionally handled for sliding collision, and the effect was not satisfactory. In addition, I have seen many help posts about ViewPager2 sliding conflict in the forum, and even some students found Github homepage of BannerViewPager because of searching for ViewPager2 sliding conflict. In this case, it's better to write an article to share BVP's experience in dealing with sliding conflicts, and maybe you can know (f μ n) * * (s: and) * *, hey hey hey.

1. Why does ViewPager have no sliding collision?

I don't know if you have this question. In the era of ViewPager, there was no sliding conflict between ViewPager and ViewPager. But why is there a sliding conflict in ViewPager2, an upgraded version of ViewPager? To understand this problem, we need to go deep into the internal analysis of ViewPager and ViewPager2.

As we know, sliding collisions need to be handled in onInterceptTouchEvent method, and it is decided whether to intercept events according to its own conditions. See the following code in the source code of ViewPager (easy to read, the code has been deleted):


@Override
 public boolean onInterceptTouchEvent(MotionEvent ev) {

  final int action = ev.getAction() & MotionEvent.ACTION_MASK;
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
  	//  Reset the state after the event is cancelled or the finger is raised 
   resetTouch();
   return false;
  }


  switch (action) {
   case MotionEvent.ACTION_MOVE: {
    //  Here, it is judged that the sliding distance in the horizontal direction is larger than that in the vertical direction 2 Times, it is considered to be effective to switch the sliding of the page 
    if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { 
     mIsBeingDragged = true;
     //  Prohibit Parent View Intercept events, that is, events should be able to be passed to ViewPager
     requestParentDisallowInterceptTouchEvent(true);
     setScrollState(SCROLL_STATE_DRAGGING);
    } else if (yDiff > mTouchSlop) {
     mIsUnableToDrag = true;
    }
    break;
   }

   case MotionEvent.ACTION_DOWN: {  
    if (mScrollState == SCROLL_STATE_SETTLING
      && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
    		//  In Down Prohibited in the event Parent View Intercept events so that the sequence of events can be passed to ViewPager
     requestParentDisallowInterceptTouchEvent(true);
     setScrollState(SCROLL_STATE_DRAGGING);
    } else {
     completeScroll(false);
     mIsBeingDragged = false;
    }
    break;
   }

   case MotionEvent.ACTION_POINTER_UP:
    onSecondaryPointerUp(ev);
    break;
  }
  return mIsBeingDragged;
 }

It can be seen that requestParentDisallowInterceptTouchEvent (true) method is called according to one judgment condition in ACTION_DOWN and ACTION_MOVE to prohibit Parent View interception event, that is to say, ViewPager has helped us deal with sliding collision, so we can just use it without worrying about sliding collision.

Now, let's go to ViewPager2, look through the source code and find that there are only onInterceptTouchEvent related methods in the implementation class of RecyclerView, and this code only handles the logic that disables user input!


private class RecyclerViewImpl extends RecyclerView {

  .... //  Omit part of the code 

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
  }
 }

That is to say, ViewPager2 does not actually help us deal with sliding conflicts! Why is this? Did the developers of ViewPager2 forget this matter? I can assure you that this is definitely not the case here. In fact, as long as we look at the structure of ViewPager2, we can probably know. ViewPager2 is declared final, meaning that we cannot modify ViewPager2 as we inherited ViewPager1. If the official handles the sliding conflict by itself within ViewPager2, if there are special requirements and we need to deal with the sliding of ViewPager2 according to our own situation, will the code written by the official to deal with the sliding conflict affect our own needs? Therefore, I think it is precisely because of this that I simply don't do any processing, and give it to the developers to solve it by themselves.

2. Solution of sliding conflict

Since the government doesn't deal with it for us, we need to do it ourselves. Before we begin, let's look at the following two schemes for dealing with sliding conflicts. Since there is a sliding conflict, 1 must be caused by the nesting of two layouts. Since there are two layouts, we can deal with them in two directions. The so-called external interception method and internal interception method.

1. External interception

The exterior in the so-called "external interception method" refers to the outer layer of these two layouts where sliding conflicts occur. As we know, an event sequence is first obtained by Parent View, and if Parent View does not intercept events, it will be handled by sub-View. Since the outer layer knows the event first, can't the outer layer View decide whether to intercept the event according to its own situation? Therefore, the implementation of the external interception method is very simple, and the general idea is as follows:


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 (needIntercept) { //  Here, whether interception is needed is judged according to the demand 
     intercepted = true;
    } else {
     intercepted = false;
    }
    break;
   }
   case MotionEvent.ACTION_UP: {
    intercepted = false;
    break;
   }
   default:
    break;
  }
  mLastXIntercept = x;
  mLastYIntercept = y;
  return intercepted;
 }

2. Internal interception

The so-called "internal interception method" refers to making a fuss about the internal View and letting the internal View decide whether to intercept the event or not. But now there is a problem. How do you know if the external View is going to intercept events? ? If the external View intercepts the incident, can't the internal View even drink the northwest wind? Don't worry, Google officials certainly consider this situation. In ViewGroup, there is a method called requestDisallowInterceptTouchEvent, which accepts an boolean value, meaning whether to disable ViewGroup from intercepting the current event. If it is an true, then the ViewGroup cannot intercept events. With this method, we can make the internal View show its magical power. Let's look at the code of the internal interception method:


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

  switch (event.getAction()) {
   case MotionEvent.ACTION_DOWN: {
   	//  Prohibit parent Intercept down Events 
    parent.requestDisallowInterceptTouchEvent(true);
    break;
   }
   case MotionEvent.ACTION_MOVE: {
    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    if (disallowParentInterceptTouchEvent) { //  According to the demand conditions, decide whether to let Parent View Intercept events. 
     parent.requestDisallowInterceptTouchEvent(false);
    }
    break;
   }
   case MotionEvent.ACTION_UP: {
    break;
   }
   default:
    break;
  }

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

After this treatment, the two nested View can work harmoniously.

The following is a conversation from the external View and the internal View.

External View: "I want to intercept events!"

Internal View: "No, you don't. I'm going to settle this matter. Jesus can't keep him.

3. Handling slip collisions for ViewPager2

In the previous chapter, we talked about two schemes to deal with sliding collision, so in this chapter, we will solve the sliding collision of ViewPager2. First, we should determine which boundary conditions exist under 1 that need to be intercepted and which do not need to be intercepted. Before writing this article, I searched Google for the sliding conflict handling scheme of ViewPager2 under 1. There are not few articles on this aspect, but most articles are not perfect for the sliding conflict handling of ViewPager2.

Let's analyze 1 in detail:

If userInputEnable=false is set, ViewPager2 should not block any events; If there is only one Item, then ViewPager2 should not intercept events; If there are multiple Item and it is the first page at present, only the left sliding event can be intercepted, and the right sliding event should not be intercepted by ViewPager2; If there are multiple Item and it is the last page at present, only the right sliding event can be intercepted, and the left sliding event should not be intercepted by the current ViewPager2; If it is multiple Item and it is an intermediate page, both left and right events should be intercepted by ViewPager2; Finally, since ViewPager2 supports vertical sliding, vertical sliding should also consider the above conditions.

After analyzing the boundary conditions, let's see which scheme should be used to deal with sliding conflicts. Obviously, the internal interception method should be used here. However, since ViewPager2 is set to final, we can't handle it by inheritance, so we need to add a layer of custom Layout outside ViewPager2. This layer of Layout is actually sandwiched between the inner layer of View and the outer layer of View. In fact, this layer of Layout becomes the inner layer. Ok, no more nonsense, just stick the code.


class ViewPager2Container @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) {

 private var mViewPager2: ViewPager2? = null
 private var disallowParentInterceptDownEvent = true
 private var startX = 0
 private var startY = 0

 override fun onFinishInflate() {
  super.onFinishInflate()
  for (i in 0 until childCount) {
   val childView = getChildAt(i)
   if (childView is ViewPager2) {
    mViewPager2 = childView
    break
   }
  }
  if (mViewPager2 == null) {
   throw IllegalStateException("The root child of ViewPager2Container must contains a ViewPager2")
  }
 }

 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
  val doNotNeedIntercept = (!mViewPager2!!.isUserInputEnabled
    || (mViewPager2?.adapter != null
    && mViewPager2?.adapter!!.itemCount <= 1))
  if (doNotNeedIntercept) {
   return super.onInterceptTouchEvent(ev)
  }
  when (ev.action) {
   MotionEvent.ACTION_DOWN -> {
    startX = ev.x.toInt()
    startY = ev.y.toInt()
    parent.requestDisallowInterceptTouchEvent(!disallowParentInterceptDownEvent)
   }
   MotionEvent.ACTION_MOVE -> {
    val endX = ev.x.toInt()
    val endY = ev.y.toInt()
    val disX = abs(endX - startX)
    val disY = abs(endY - startY)
    if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_VERTICAL) {
     onVerticalActionMove(endY, disX, disY)
    } else if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_HORIZONTAL) {
     onHorizontalActionMove(endX, disX, disY)
    }
   }
   MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false)
  }
  return super.onInterceptTouchEvent(ev)
 }

 private fun onHorizontalActionMove(endX: Int, disX: Int, disY: Int) {
  if (mViewPager2?.adapter == null) {
   return
  }
  if (disX > disY) {
   val currentItem = mViewPager2?.currentItem
   val itemCount = mViewPager2?.adapter!!.itemCount
   if (currentItem == 0 && endX - startX > 0) {
    parent.requestDisallowInterceptTouchEvent(false)
   } else {
    parent.requestDisallowInterceptTouchEvent(currentItem != itemCount - 1
      || endX - startX >= 0)
   }
  } else if (disY > disX) {
   parent.requestDisallowInterceptTouchEvent(false)
  }
 }

 private fun onVerticalActionMove(endY: Int, disX: Int, disY: Int) {
  if (mViewPager2?.adapter == null) {
   return
  }
  val currentItem = mViewPager2?.currentItem
  val itemCount = mViewPager2?.adapter!!.itemCount
  if (disY > disX) {
   if (currentItem == 0 && endY - startY > 0) {
    parent.requestDisallowInterceptTouchEvent(false)
   } else {
    parent.requestDisallowInterceptTouchEvent(currentItem != itemCount - 1
      || endY - startY >= 0)
   }
  } else if (disX > disY) {
   parent.requestDisallowInterceptTouchEvent(false)
  }
 }

 /**
  *  Sets whether to allow the current View Adj. {@link MotionEvent#ACTION_DOWN} Parent is prohibited in the event View Interception of events, the method 
  *  Used to solve CoordinatorLayout+CollapsingToolbarLayout In nested ViewPager2Container The sliding conflict problem caused by. 
  *
  *  Sets whether to allow the ViewPager2Container Adj. {@link MotionEvent#ACTION_DOWN} Parent is prohibited in the event View Interception of events, the method 
  *  Used to solve CoordinatorLayout+CollapsingToolbarLayout In nested ViewPager2Container The sliding conflict problem caused by. 
  *
  * @param disallowParentInterceptDownEvent  Is it allowed ViewPager2Container In {@link MotionEvent#ACTION_DOWN} Parent is prohibited in the event View Intercept events, the default value is false
  *       true  Not allowed ViewPager2Container In {@link MotionEvent#ACTION_DOWN} Parent is forbidden in time View Time interception, 
  *        Settings disallowIntercept For true Can be solved CoordinatorLayout+CollapsingToolbarLayout Sliding conflict of 
  *       false  Allow ViewPager2Container In {@link MotionEvent#ACTION_DOWN} Parent is forbidden in time View Time interception, 
  */
 fun disallowParentInterceptDownEvent(disallowParentInterceptDownEvent: Boolean) {
  this.disallowParentInterceptDownEvent = disallowParentInterceptDownEvent
 }
}

I won't explain too much because of the limited space of the above code. Note that in onFinishInflate, we traverse all the children View of ViewPager2Container through a loop, and throw an exception if ViewPager2 is not found. In addition, disallowParentInterceptDownEvent method comments are written in more detail.

The use method is also very simple, and ViewPager2 can be wrapped directly with ViewPager2Container:


<com.zhpan.sample.viewpager2.ViewPager2Container
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintLeft_toLeftOf="parent"
  app:layout_constraintRight_toRightOf="parent"
  app:layout_constraintTop_toTopOf="parent">
  
  <androidx.viewpager2.widget.ViewPager2
   android:id="@+id/view_pager2"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />

  <com.zhpan.indicator.IndicatorView
   android:id="@+id/indicatorView"
   android:layout_centerHorizontal="true"
   android:layout_alignParentBottom="true"
   android:layout_margin="@dimen/dp_20"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

 </com.zhpan.sample.viewpager2.ViewPager2Container>

This is about the handling scheme of ViewPager2 sliding out collision. Of course, because BannerViewPager supports cyclic carousel, the handling of sliding collision of BannerViewPager will be relatively more troublesome. If interested students can click to view the source code of BannerViewPager.

At the same time, I also put the source code of ViewPager2Container into Github, which can be used by myself.

These are the details of ViewPager2 Sliding Conflict Resolution. For more information about ViewPager2 Sliding Conflict Resolution, please pay attention to other related articles on this site!


Related articles: