Since ViewPager2 was officially released last December, ViewPager2 has gradually begun to replace the older version of ViewPager. Many developers have also used ViewPager2 in their projects. ViewPager2 is pretty powerful compared to ViewPager, which I wrote in a previous articleLearn more about ViewPager2ViewPager2 is explained in detail in ViewPager2. However, since there wasn’t much actual combat at the time, we didn’t see any serious sliding conflicts with the nested use of ViewPager2. Until March of this year refactoring with ViewPager2BannerViewPagerI discovered this problem. As a result, in BVP 3.0, additional sliding conflict handling was done on ViewPager2, and the effect was not satisfactory. In addition, had seen a lot of ViewPager2 slide conflict help post on the forum, and even students because of the search ViewPager2 slide conflict and foundBannerViewPagerGithub home page. In this case, why not write an article to share BVP’s experience with sliding conflicts(f ě n) 识 And (s)Hey, hey, hey.
Why does ViewPager have sliding collisions?
I don’t know if you’re wondering, but in the ViewPager era, ViewPager nested ViewPager didn’t have sliding collisions. Why is there a sliding conflict in ViewPager2, the updated version of ViewPager? To figure this out, we need to dig inside ViewPager and ViewPager2 and analyze their source code.
As we know, sliding collisions are handled in the onInterceptTouchEvent method, which determines whether to intercept the event based on its own conditions. See the following code in the ViewPager source code (it’s easy to read, the code has been truncated) :
@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: {
// If the sliding distance in the horizontal direction is greater than 2 times of the vertical direction, it is considered to be an effective slide to switch pages
if (xDiff > mTouchSlop && xDiff * 0.5 f > yDiff) {
mIsBeingDragged = true;
// Disallow the Parent View from intercepting events that can be passed to the 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) {
// Disallow the Parent View from intercepting the Down event so that the sequence of events can be passed to the ViewPager
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
} else {
completeScroll(false);
mIsBeingDragged = false;
}
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return mIsBeingDragged;
}
Copy the code
You can see in ACTION_DOWN and ACTION_MOVE according to some judging conditions call requestParentDisallowInterceptTouchEvent (true) method to prohibit the Parent View to intercept events, In other words, the ViewPager already handles swipe collisions for us, so we can just use it and not worry about swipe collisions.
Now, we turn to ViewPager2, read the source code found only in RecyclerView implementation classes have onInterceptTouchEvent related methods, and this code is only processing disabled user input logic!
private class RecyclerViewImpl extends RecyclerView {...// Omit some code
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isUserInputEnabled() && super.onInterceptTouchEvent(ev); }}Copy the code
So ViewPager2 doesn’t actually help us with sliding collisions! Why is that? Did the developers of ViewPager2 forget that? I’m sure it’s not like that here. Actually, if we look at the structure of ViewPager2, we can probably see that. ViewPager2 is declared final, which means we can’t modify ViewPager2 the way we inherited ViewPager. If there is a special need to handle ViewPager2 sliding according to our own situation, then the official code to handle sliding conflicts will not affect our own needs? So I think that’s why I left it to the developers to figure it out.
Second, sliding conflict processing scheme
Since the authorities won’t let us handle it, we’ll have to do it ourselves. Before we get started, let’s look at the following two ways to handle sliding collisions. Since sliding conflicts occur, they must be caused by two layouts nested within each other. Since there are two layouts, we can treat them in two directions. The so-called external interception and internal interception.
1. External interception
The outer in the so-called “outer intercept method” refers to the outer layer of the two layouts that now slide into conflict. We know that a sequence of events is retrieved by the Parent View, and if the Parent View does not intercept the event, the child View will handle it. Since the outer View gets the event first, the outer View decides whether to intercept the event based on its situation. Therefore, the implementation of external interception method is very simple, 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) { // Determine whether interception is required based on requirements
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
Copy the code
2. Internal interception
The so-called “internal interception method” is to manipulate the internal View and let the internal View decide whether or not to intercept the event. How do you know if the external View is going to intercept the event? If the external View blocks the event, won’t the internal View even drink the northwest wind? Don’t worry, Google officials are certainly thinking about this situation. There is a call in the ViewGroup requestDisallowInterceptTouchEvent method, this method accepts a Boolean value, mean whether to ban ViewGroup intercept the current event. If true, the ViewGroup cannot intercept events. With this method we can make the inner View work. Take a look at the code for internal interception:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
// Disables parent from intercepting down events
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (disallowParentInterceptTouchEvent) { // Whether the Parent View intercepts events depends on the requirements.
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
Copy the code
Once done, the two nested Views work in harmony.
The following is a conversation between the external View and the internal View.
External View: “I want to intercept events! “
Internal View: “No, you don’t. I’m gonna fix this. Jesus can’t keep him. “
Handle sliding conflicts in ViewPager2
The previous chapter looked at two ways to handle slide conflicts, so in this chapter we’re going to solve slide conflicts for ViewPager2. First, determine what boundary conditions exist where interception is required and where interception is not required. Before writing this article, I did a Google search on sliding conflict handling for ViewPager2. There are not many articles on this topic, but most of them don’t give a good idea of sliding conflict handling for ViewPager2.
Let’s analyze it in detail:
- If userInputEnable=false is set, ViewPager2 should not intercept any events;
- If there is only one Item, ViewPager2 should not intercept events either;
- If there are multiple items and the current page is the first page, then only the left slide event should be intercepted, and the right slide event should not be intercepted by ViewPager2.
- If there are multiple items and the current page is the last, then only the right slide event should be intercepted, and the left slide event should not be intercepted by the current ViewPager2.
- If there are multiple items and it is an intermediate page, then both left and right events should be intercepted by ViewPager2;
- Finally, since ViewPager2 supports vertical sliding, vertical sliding should also take into account the above conditions.
After analyzing the boundary conditions, let’s see which scheme should we use to handle sliding conflicts? It is obvious that internal interception should be used here. However, since ViewPager2 is set to final, we cannot inherit it, so we need to add a custom Layout around ViewPager2. This layer of Layout is sandwiched between the inner View and the outer View. This layer of Layout becomes the inner View. Well, no more nonsense to say, directly paste 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(a) {
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 {in the current View@linkMotionEvent#ACTION_DOWN} disallows the parent View from intercepting the event, The method * is used to solve the slide conflict problem of CoordinatorLayout+CollapsingToolbarLayout when nesting ViewPager2Container. * * Sets whether to allow {in ViewPager2Container@linkMotionEvent#ACTION_DOWN} disallows the parent View from intercepting the event, The method * is used to solve the slide conflict problem of CoordinatorLayout+CollapsingToolbarLayout when nesting ViewPager2Container. * *@paramDisallowParentInterceptDownEvent whether to allow ViewPager2Container in {@linkMotionEvent#ACTION_DOWN} disallows the parent View from intercepting events. The default is false * true.@linkMotionEvent#ACTION_DOWN} time disables parent View time blocking, CollapsingToolbarLayout false Allows ViewPager2Container to collapse in {@linkMotionEvent#ACTION_DOWN} disables time blocking for parent View, */
fun disallowParentInterceptDownEvent(disallowParentInterceptDownEvent: Boolean) {
this.disallowParentInterceptDownEvent = disallowParentInterceptDownEvent
}
}
Copy the code
So I’m not going to go into this code too much because of the space, but notice that in the onFinishInflate we are looping through all the child views of the ViewPager2Container, and if we don’t find ViewPager2 we throw an exception. In addition, disallowParentInterceptDownEvent method comparatively detailed annotations to write much.
Use ViewPager2Container to wrap ViewPager2 in 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>
Copy the code
This is how ViewPager2 slides out of conflict. Of course, BannerViewPager slides out of conflict are a little more difficult because BannerViewPager supports circular rotation. If you are interested, you can click on the source code of BannerViewPager.
At the same time, ViewPager2Container source CODE I also put on Github, need to use can be taken.