1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.viewpager2.integration.testapp 18 19 import android.content.Context 20 import android.util.AttributeSet 21 import android.view.MotionEvent 22 import android.view.View 23 import android.view.ViewConfiguration 24 import android.widget.FrameLayout 25 import androidx.viewpager2.widget.ViewPager2 26 import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL 27 import kotlin.math.absoluteValue 28 import kotlin.math.sign 29 30 /** 31 * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem 32 * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as 33 * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. 34 * 35 * This solution has limitations when using multiple levels of nested scrollable elements (e.g. a 36 * horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). 37 */ 38 class NestedScrollableHost : FrameLayout { 39 constructor(context: Context) : super(context) 40 41 constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 42 43 private var touchSlop = 0 44 private var initialX = 0f 45 private var initialY = 0f 46 private val parentViewPager: ViewPager2? 47 get() { 48 var v: View? = parent as? View 49 while (v != null && v !is ViewPager2) { 50 v = v.parent as? View 51 } 52 return v as? ViewPager2 53 } 54 55 private val child: View? 56 get() = if (childCount > 0) getChildAt(0) else null 57 58 init { 59 touchSlop = ViewConfiguration.get(context).scaledTouchSlop 60 } 61 canChildScrollnull62 private fun canChildScroll(orientation: Int, delta: Float): Boolean { 63 val direction = -delta.sign.toInt() 64 return when (orientation) { 65 0 -> child?.canScrollHorizontally(direction) ?: false 66 1 -> child?.canScrollVertically(direction) ?: false 67 else -> throw IllegalArgumentException() 68 } 69 } 70 onInterceptTouchEventnull71 override fun onInterceptTouchEvent(e: MotionEvent): Boolean { 72 handleInterceptTouchEvent(e) 73 return super.onInterceptTouchEvent(e) 74 } 75 handleInterceptTouchEventnull76 private fun handleInterceptTouchEvent(e: MotionEvent) { 77 val orientation = parentViewPager?.orientation ?: return 78 79 // Early return if child can't scroll in same direction as parent 80 if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { 81 return 82 } 83 84 if (e.action == MotionEvent.ACTION_DOWN) { 85 initialX = e.x 86 initialY = e.y 87 parent.requestDisallowInterceptTouchEvent(true) 88 } else if (e.action == MotionEvent.ACTION_MOVE) { 89 val dx = e.x - initialX 90 val dy = e.y - initialY 91 val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL 92 93 // assuming ViewPager2 touch-slop is 2x touch-slop of child 94 val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f 95 val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f 96 97 if (scaledDx > touchSlop || scaledDy > touchSlop) { 98 if (isVpHorizontal == (scaledDy > scaledDx)) { 99 // Gesture is perpendicular, allow all parents to intercept 100 parent.requestDisallowInterceptTouchEvent(false) 101 } else { 102 // Gesture is parallel, query child if movement in that direction is possible 103 if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { 104 // Child can scroll, disallow all parents to intercept 105 parent.requestDisallowInterceptTouchEvent(true) 106 } else { 107 // Child cannot scroll, allow all parents to intercept 108 parent.requestDisallowInterceptTouchEvent(false) 109 } 110 } 111 } 112 } 113 } 114 } 115