Created
October 12, 2020 07:26
-
-
Save ichenhe/25452c4bece3c18a1c5f5164c7089f1b to your computer and use it in GitHub Desktop.
Fix parallel scroll with a NestedScrollView inside a ViewPager2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 | |
} | |
} |
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
@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
Fix for the use case: ViewPager2(V) -> NestedScrollView(V) -> RecyclerView(H)
Releated AndroidX issues: 123006042
Original code: views-widgets-samples