Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Used OnTouchListener for nested scrolling in ViewPager2 #112

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,77 +16,62 @@

package androidx.viewpager2.integration.testapp

import android.annotation.SuppressLint
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 android.view.*
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
* Class to scroll 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.
* ViewPager2.
*
* 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 class NestedScrollableHost(context: Context) : View.OnTouchListener {

private var touchSlop = 0
private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
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
private val View.parentViewPager: ViewPager2?
get() = generateSequence(this as? ViewParent, ViewParent::getParent)
.filterIsInstance<ViewPager2>()
.firstOrNull()

init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}

private fun canChildScroll(orientation: Int, delta: Float): Boolean {
private fun View.canScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
ViewPager2.ORIENTATION_HORIZONTAL -> canScrollHorizontally(direction)
ViewPager2.ORIENTATION_VERTICAL -> canScrollVertically(direction)
else -> throw IllegalArgumentException()
}
}

override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View, e: MotionEvent): Boolean {
handleInterceptTouchEvent(view, e)
return false
}

private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
private fun handleInterceptTouchEvent(view: View, e: MotionEvent) {
val orientation = view.parentViewPager?.orientation ?: return

// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
if (!view.canScroll(orientation, -1f) && !view.canScroll(orientation, 1f)) {
return
}

if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
view.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
val isVpHorizontal = orientation == ViewPager2.ORIENTATION_HORIZONTAL

// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
Expand All @@ -95,18 +80,21 @@ class NestedScrollableHost : FrameLayout {
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
view.parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
if (view.canScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
view.parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
view.parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}
}

fun ViewGroup.allowSameDirectionScrollingInViewPager2() =
setOnTouchListener(NestedScrollableHost(context))
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ParallelNestedScrollingActivity : Activity() {
private fun RecyclerView.setUpRecyclerView(orientation: Int) {
layoutManager = LinearLayoutManager(context, orientation, false)
adapter = RvAdapter(orientation)
allowSameDirectionScrollingInViewPager2()
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
Expand Down
27 changes: 9 additions & 18 deletions ViewPager2/app/src/main/res/layout/item_nested_recyclerviews.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,12 @@
android:text="@string/first_rv"
android:textStyle="bold" />

<androidx.viewpager2.integration.testapp.NestedScrollableHost
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/first_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/first_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF" />
</androidx.viewpager2.integration.testapp.NestedScrollableHost>
android:layout_marginTop="8dp"
android:background="#FFFFFF" />

<TextView
android:layout_width="match_parent"
Expand All @@ -60,18 +56,13 @@
android:text="@string/second_rv"
android:textStyle="bold" />

<androidx.viewpager2.integration.testapp.NestedScrollableHost
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/second_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="8dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/second_rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF" />
</androidx.viewpager2.integration.testapp.NestedScrollableHost>
android:layout_marginRight="20dp"
android:background="#FFFFFF" />

</LinearLayout>