Android

[Android] ViewPager2 내부 RecyclerView 중첩 스크롤

반응형

문제

: Horizontal ViewPager2 내부에 ScrollView(RecyclerView)가 존재할 떄.

예를 들어, ViewPager2의 orientaion: Horizontal 

ScrollView(RecyclerView) orientation : Horizontal 로 같을 때.

 

부모 뷰 (ViewPager2)에 포커싱이 되어 자식뷰의 스크롤이 먹히지 않거나 혼선이 생기는 경우가 발생한다.

자식뷰에 터치 이벤트가 발생했을 때 우선적으로 스크롤을 인식할 수 있도록 도와주는 NestedScrollableHost 라는 애가 있다.

 

구글 Migrate from ViewPager to ViewPager2 가이드에 보면

 

구글 ViewPager 가이드

중첩 스크롤 가능 요소 지원 ViewPager2는 스크롤 보기가 포함된 ViewPager2 개체와 방향이 같은 경우 중첩된 스크롤 보기를 기본적으로 지원하지 않습니다. 예를 들어, 수직 방향 ViewPager2 객체 내부의 수직 스크롤 보기에서는 스크롤이 작동하지 않습니다. 동일한 방향의 ViewPager2 개체 내에서 스크롤 보기를 지원하려면 중첩된 요소를 스크롤해야 하는 경우 ViewPager2 개체에서 requestDisallowInterceptTouchEvent()를 대신 호출해야 합니다. ViewPager2 중첩 스크롤 샘플은 다양한 사용자 정의 래퍼 레이아웃으로 이 문제를 해결하는 한 가지 방법을 보여줍니다.

 

간단히 말해 같은 방향의 스크롤을 지원하지 않는다. 


NestedScrollableHost

ViewPager2 nested scrolling sample

/**
 * 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 {
        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)
                    }
                }
            }
        }
    }
}

 

  1. 사용방법은 위 코드를 그대로 복사해, 클래스를 만들어 붙여 넣는다.
  2. ScrollView(RecyclerView)가 있는 xml로 가서 해당 뷰 위에 <NestedScrollableHost>로 감싸준다. 
  3. ViewPager -> NestedScrollableHost -> ScrollView(RecyclerView) 순으로 자식이어야 한다.

참조

 

반응형