The | the Abstract in this paper, through the process of solving a bug gradually thorough, the conflict of sliding processing train of thought, shows the thinking process to solve the problem, and further learning and exploration process of source code.
Keyword | Keyword event distribution, sliding, RecyclerView, ViewPager2 conflict
Origin of zero,
Sliding conflicts are a common development problem that isn’t complicated, but can occasionally throw you off guard. The origin of sliding conflicts is that the distribution of touch events does not meet the needs of the developer, and ultimately the View that handles touch events is not what the business wants.
ViewPager and RecyclerView are areas where sliding conflicts occur frequently. Today’s problems occur in the ViewPager of RecyclerView.
I. Problem description
The problem arises from the layout structure:
-- ViewPager2(Landscape) --... RecyclerView(vertical)...... -- ViewPager2(landscape)Copy the code
Without doing anything, the innermost ViewPager2 cannot slide, and all horizontal sliding events are handed over to the outer ViewPager2. The first problem is how to solve this sliding conflict, the specific requirement is “when the internal ViewPager2 content can be sliding, sliding events by internal ViewPager2 processing; when the internal ViewPager2 can no longer slide, Slide events are handled by external ViewPager2.
In the process of reproducing the problem, some additional information was found: there were no sliding conflicts when the ViewPager nested ViewPager2 when debugging with the previous project. The second question arises, why is ViewPager nesting conflict-free?
The next step is to explore and solve the problem, so if you don’t want to read too much, you can fast forward to part 3 and get to the conclusion.
Second, layer by layer exploration
Let’s start with a review of Android’s event distribution mechanism.
[Basic distribution chart of Touch Events]
In order to simplify the expression, the following statement names the two conflicting ViewPager2 by function, calling the outer ViewPager2 “Pager” and the inner ViewPager2 “Banner”.
- Reasoning in a
The processing of the touch event occurs in onTouchEvent. According to the final phenomenon, it can be inferred that the onTouchEvent of the Banner is not triggered, but that of Pager is. The Banner is on top of the Pager. Without interruption, the onTouchEvent of the Banner must be triggered before Pager, so Pager uses the onInterceptTouchEvent to intercept this slip.
- Verification of a
To prove the reasoning above, we need to verify that onInterceptTouchEvent in ViewPager2 is preferentially intercepted, that is, we override onInterceptTouchEvent, and if the touch event is judged to be a horizontal slide, it consumes the touch event itself.
ViewPager2 is based on RecyclerView packaging, but inherits from ViewGroup, and obtains RecyclerView functionality through composition rather than inheritance. This also results in ViewPager2 being final and not extensible by inheritance.
Tracing ViewPager2 source can be learned, ViewPager2 touch event interception processing and RecyclerView basically the same:
// in RecyclerViewImpl
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
}
Copy the code
Does RecyclerView block touch events by default? According to experience, the judgment of sliding requires ACTION_DOWN to record the initial point and make a comparison judgment when ACTION_MOVE moves to distinguish whether it is sliding and the direction of sliding.
// in onInterceptTouchEvent, ACTION_MOVE
if(mScrollState ! = SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) { // Set drag to return true to start intercepting touch eventssetScrollState(SCROLL_STATE_DRAGGING); }}Copy the code
Let’s run the code to verify that since ViewPager2 can’t inherit, I’m going to create a custom FrameLayout layer around ViewPager2, and I’m going to use the FrameLayout touch event callback to determine what events ViewPager2 can receive. Log output method name and event type, the result is as follows:
The onTouchEvent is not triggered at all, and the event is indeed intercepted.
Tips: ACTION_DOWN=0; ACTION_MOVE=2; ACTION_CANCEL=3;
Once you know the cause, you can consider a solution.
- Reason 2
Since the event intercepting query is raised from the root View, the Pager and Banner have the same onInterceptTouchEvent condition, so the Pager intercepts the event first. To handle nested sliding of ViewPager2 properly, you must interfere with the event distribution process.
- Reasoning three
ViewPager2 is not inheritable and can only interfere by modifying a Layout between the Banner and Pager. It is forbidden to intercept touch events when the Banner is sliding.
- Verify two and three
Also use the custom FrameLayout method mentioned above. This can be done in either dispatchTouchEvent or onInterceptTouchEvent, but the onInterceptTouchEvent should not return true. Since we are modifying the parent View of the Banner rather than the Banner itself, blocking the event will also cause the Banner to fail to receive the event.
The specific approach is:
- Determines whether the touch event is sliding and in which direction
- Determines whether the child View can slide in that direction
- Called when a child View can be swiped
requestDisallowInterceptTouchEvent(true)
Blocking events
Tips: RequestDisallowInterceptTouchEvent is a function of a “pass”, when calling the parent requestDisallowInterceptTouchEvent step by step, so there is no need to judge what is the outer concrete.
The code implementation is as follows:
override fun dispatchTouchEvent(ev: MotionEvent?).: Boolean {
Log.e("asdfg"."dispatchTouchEvent ${ev? .action}")
when(ev? .action) { MotionEvent.ACTION_DOWN -> { mInitialTouchX = ev.x mInitialTouchY = ev.y } MotionEvent.ACTION_MOVE -> {val dx = ev.x - mInitialTouchX
val dy = ev.y - mInitialTouchY
var hasScrollView = false
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.canScrollHorizontally(-1) && dx > 0) {
hasScrollView = true
}
if (child.canScrollHorizontally(1) && dx < 0) {
hasScrollView = true}}val r = abs(dy) / abs(dx)
if (r < 0.6 f && hasScrollView) { // The ratio can be adjusted
requestDisallowInterceptTouchEvent(true)
}
}
MotionEvent.ACTION_UP -> {
requestDisallowInterceptTouchEvent(false)
}
MotionEvent.ACTION_CANCEL -> {
requestDisallowInterceptTouchEvent(false)}}Copy the code
Running results:
The desired sliding effect is achieved. The experiment code does not set infinite sliding of the Banner, in order to verify that there is no exception when sliding to the end of the processing.
Now that I’ve solved the problem, let’s go back to the second question, what does ViewPager do to avoid sliding collisions?
The View hierarchy is as follows:
-- ViewPager
-- ConstraintLayout
-- ViewPager2
Copy the code
When sliding horizontally, the sliding event is handled by ViewPager2. The onInterceptTouchEvent in ViewPager has more stringent criteria for intercepting.
// ViewPager
if (xDiff > mTouchSlop && xDiff * 0.5 f > yDiff) {
if (DEBUG) Log.v(TAG, "Starting drag!");
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0
? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
}
// ViewPager2
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
Copy the code
The biggest difference is that ViewPager makes judgment on the ratio of horizontal and vertical moving distance, in order to confirm that the sliding direction is landscape, so as to avoid the error of ViewPager intercepting vertical sliding events.
3. Experimental conclusions
- ViewPager2 is final, and sliding conflicts on ViewPager2 can be handled in the container Layout
- The onInterceptTouchEvent condition of the two ViewPager2 onInterceptTouchEvent conditions are the same. The onInterceptTouchEvent condition of the two ViewPager2 onInterceptTouchEvent conditions are the same. The touch event does not reach the inner ViewPager2
- The way to resolve slippage conflicts is to customize the parent View of the inner ViewPager2, check for slippage in dispatchTouchEvent or onInterceptTouchEvent, Timely call requestDisallowInterceptTouchEvent outer ViewPager2 intercept is prohibited
Four, afterword.
Android’s event (delivery) mechanism is difficult to think about, and even if you understand the whole process, you can get sidetracked and come to the wrong conclusion. When thinking about the View in the event mechanism of the order, and then divided into “bottom-up” event distribution process and “top down” event consumption process, find the right place.
The NestedScroll mechanism is almost distorted in the process of thinking. Although the NestedScroll mechanism is also designed to solve the sliding conflict in the same direction, when we use NestedScroll, our goal is basically to “distribute the sliding event to multiple views”. The ViewPager needs don’t need to be that complicated.
RecyclerView based on the realization of ViewPager2 is a flexible use of RecyclerView, see part of the source code, learning RecyclerView enthusiasm gradually rising up, while the holiday, wash!
Happy Spring Festival, year of the Ox!