Skip to content

Instantly share code, notes, and snippets.

@ichenhe
Created October 12, 2020 07:26
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ichenhe/25452c4bece3c18a1c5f5164c7089f1b to your computer and use it in GitHub Desktop.
Save ichenhe/25452c4bece3c18a1c5f5164c7089f1b to your computer and use it in GitHub Desktop.
Fix parallel scroll with a NestedScrollView inside a ViewPager2
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.viewpager2.integration.testapp
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
import kotlin.math.sign
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
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
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
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
return handleInterceptTouchEvent(e) || super.onInterceptTouchEvent(e)
}
/**
* @return The child does not need to scroll.
*/
private fun handleInterceptTouchEvent(e: MotionEvent): Boolean {
val orientation = parentViewPager?.orientation ?: return false
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return false
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
if (dx.absoluteValue > touchSlop || dy.absoluteValue > touchSlop) {
if (isVpHorizontal == (dy.absoluteValue > dx.absoluteValue)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
// If you are the following case:
// ViewPager2(V) -> NestedScrollView(V) -> RecyclerView(H)
// return false instead so that the innermost RV(H) can scroll normally.
// It is not tested for other use case.
return true
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
return true
}
}
}
}
return false
}
}
@ichenhe
Copy link
Author

ichenhe commented Oct 12, 2020

Fix for the use case: ViewPager2(V) -> NestedScrollView(V) -> RecyclerView(H)
Releated AndroidX issues: 123006042
Original code: views-widgets-samples

@eme22
Copy link

eme22 commented Oct 19, 2020

Hello i dont know if this is the place to say it but i have used this converted to java version of your code as pointed in : stackoverflow, everything works okay but when i add a swiperefreshlayout as parent of the nestedscrollview the horizontal swiping in the recyclerview stops working, i needed to pointed that, thanks

@ichenhe
Copy link
Author

ichenhe commented Oct 23, 2020

@eme22 Yes, this code is just for my specific use case which has been mentioned in the comment. The whole scrolling issue is a bit difficult since ViewPager2 team has not solve it. So you may need to refer to my thoughts and change the code to fit your case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment