带有 RecyclerView 滚动问题的 Vertical ViewPager2
Vertical ViewPager2 with RecyclerView Scrolling Issue
-
vertical-scrolling
-
android-fragments
-
android-recyclerview
-
android-nestedscrollview
-
android-viewpager2
我在垂直方向上使用带有两个片段的 ViewPager2。当用户向下滑动到第二个片段时,有一个 RecyclerView 在相同的垂直方向滚动内容。
问题是当我滚动 RecyclerView 的内容时,有时 ViewPager2 捕获滚动事件,有时 RecyclerView 捕获滚动事件。
我想要这样当用户滚动到 RecyclerView 的顶部时,ViewPager 只会在用户到达顶部时向上滑动回到第一个片段RecyclerView 中的内容。
我试过使用 recyclerView.isNestedScrollingEnabled = false
,但运气不佳。我还尝试将 RecyclerView 放入 NestedScrollView,但不推荐这样做,因为 RecyclerView 然后会创建数据集所需的每个 ViewHolder,这显然效率不高。
所以...我只是阅读了一些 documentation 就能弄清楚。我将 post 答案放在这里,以帮助遇到类似问题的其他人:
由于 ViewPager2 不能很好地支持嵌套滚动视图,与 NestedScrollView 不同,我们需要在布局中使用自定义包装器包装嵌套滚动视图,以便能够处理被嵌套拦截的触摸和滑动事件滚动视图父级。在我们的例子中,子项是 RecyclerView,父项是 ViewPager2。
您可以找到包装器 class here。只需将它添加到您的项目中,然后将可滚动视图包裹在其中,类似于以下内容:
<NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</NestedScrollableHost>
这里有几件事需要注意:documentation 表示此解决方案不适用于 ViewPager 中其他可滚动视图内的可滚动视图。此解决方案仅适用于 ViewPager 的即时滚动视图。
另一个注意事项是包装器 class 使用 requestDisallowInterceptTouchEvent()
来确保如果子 需要滚动,子可滚动视图会告诉父级不要滚动。
我得到的最佳解决方案是在 recyclerView.addOnItemTouchListener(this) 上使用 gestureDetector.SimpleOnGestureListener。
第 1 步:在 OnCreate() 方法中
gestureDetector = new GestureDetector(getActivity(), new GestureListener());
第二步:实现recyclerView addonitemtouchlistenr方法-
recyclerView.addOnItemTouchListener(this);
第 3 步:创建扩展 GestureDetector.SimpleOnGestureListener.
的 class GestureListener
public class GestureListener extends GestureDetector.SimpleOnGestureListener {
private final int Y_BUFFER = 10;
@Override
public boolean onDown(MotionEvent e) {
// Prevent ViewPager from intercepting touch events as soon as a DOWN is detected.
// If we don't do this the next MOVE event may trigger the ViewPager to switch
// tabs before this view can intercept the event.
Log.d("vp", "true1");
recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
return super.onDown(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > Math.abs(distanceY)) {
Log.d("vp2", "true");
// Detected a horizontal scroll, allow the viewpager from switching tabs
recyclerView.getParent().requestDisallowInterceptTouchEvent(false);
} else if (Math.abs(distanceY) > Y_BUFFER) {
// Detected a vertical scroll prevent the viewpager from switching tabs
Log.d("vp3", "false");
recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
}
第 4 步:从 onInterceptTouchEvent() 调用 gestureDetector.onTouchEvent(e)。
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
gestureDetector.onTouchEvent(e);
return false;
}
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 {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
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
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} 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)
}
}
}
}
}
}
vertical-scrolling
android-fragments
android-recyclerview
android-nestedscrollview
android-viewpager2
我在垂直方向上使用带有两个片段的 ViewPager2。当用户向下滑动到第二个片段时,有一个 RecyclerView 在相同的垂直方向滚动内容。
问题是当我滚动 RecyclerView 的内容时,有时 ViewPager2 捕获滚动事件,有时 RecyclerView 捕获滚动事件。
我想要这样当用户滚动到 RecyclerView 的顶部时,ViewPager 只会在用户到达顶部时向上滑动回到第一个片段RecyclerView 中的内容。
我试过使用 recyclerView.isNestedScrollingEnabled = false
,但运气不佳。我还尝试将 RecyclerView 放入 NestedScrollView,但不推荐这样做,因为 RecyclerView 然后会创建数据集所需的每个 ViewHolder,这显然效率不高。
所以...我只是阅读了一些 documentation 就能弄清楚。我将 post 答案放在这里,以帮助遇到类似问题的其他人:
由于 ViewPager2 不能很好地支持嵌套滚动视图,与 NestedScrollView 不同,我们需要在布局中使用自定义包装器包装嵌套滚动视图,以便能够处理被嵌套拦截的触摸和滑动事件滚动视图父级。在我们的例子中,子项是 RecyclerView,父项是 ViewPager2。
您可以找到包装器 class here。只需将它添加到您的项目中,然后将可滚动视图包裹在其中,类似于以下内容:
<NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
</NestedScrollableHost>
这里有几件事需要注意:documentation 表示此解决方案不适用于 ViewPager 中其他可滚动视图内的可滚动视图。此解决方案仅适用于 ViewPager 的即时滚动视图。
另一个注意事项是包装器 class 使用 requestDisallowInterceptTouchEvent()
来确保如果子 需要滚动,子可滚动视图会告诉父级不要滚动。
我得到的最佳解决方案是在 recyclerView.addOnItemTouchListener(this) 上使用 gestureDetector.SimpleOnGestureListener。
第 1 步:在 OnCreate() 方法中
gestureDetector = new GestureDetector(getActivity(), new GestureListener());
第二步:实现recyclerView addonitemtouchlistenr方法-
recyclerView.addOnItemTouchListener(this);
第 3 步:创建扩展 GestureDetector.SimpleOnGestureListener.
的 class GestureListenerpublic class GestureListener extends GestureDetector.SimpleOnGestureListener {
private final int Y_BUFFER = 10;
@Override
public boolean onDown(MotionEvent e) {
// Prevent ViewPager from intercepting touch events as soon as a DOWN is detected.
// If we don't do this the next MOVE event may trigger the ViewPager to switch
// tabs before this view can intercept the event.
Log.d("vp", "true1");
recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
return super.onDown(e);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > Math.abs(distanceY)) {
Log.d("vp2", "true");
// Detected a horizontal scroll, allow the viewpager from switching tabs
recyclerView.getParent().requestDisallowInterceptTouchEvent(false);
} else if (Math.abs(distanceY) > Y_BUFFER) {
// Detected a vertical scroll prevent the viewpager from switching tabs
Log.d("vp3", "false");
recyclerView.getParent().requestDisallowInterceptTouchEvent(true);
}
return super.onScroll(e1, e2, distanceX, distanceY);
}
}
第 4 步:从 onInterceptTouchEvent() 调用 gestureDetector.onTouchEvent(e)。
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
gestureDetector.onTouchEvent(e);
return false;
}
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 {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
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
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} 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)
}
}
}
}
}
}