Skip to content

Instantly share code, notes, and snippets.

@hoang06kx1
Forked from ichenhe/NestedScrollableHost.kt
Created October 4, 2023 15:20
Show Gist options
  • Select an option

  • Save hoang06kx1/fdc32908cb96aa9fb1c7da4d991fcaa9 to your computer and use it in GitHub Desktop.

Select an option

Save hoang06kx1/fdc32908cb96aa9fb1c7da4d991fcaa9 to your computer and use it in GitHub Desktop.

Revisions

  1. Chenhe created this gist Oct 12, 2020.
    118 changes: 118 additions & 0 deletions NestedScrollableHost.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,118 @@
    /*
    * 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
    }
    }