View 滚动与滑动冲突解决
嵌套滚动场景的完整解决方案
目录
- 滑动冲突场景
- View 的滑动方式
- 外部拦截法
- 内部拦截法
- NestedScrolling 机制
- 实战:CoordinatorLayout 原理
1. 滑动冲突场景
典型冲突场景
flowchart TB
subgraph Scene1["场景 1: ViewPager + RecyclerView"]
VP["ViewPager<br/>左右滑动"] --> RV1["RecyclerView<br/>上下滑动"]
RV1 --> Conflict1["冲突:左右滑动和上下滑动如何区分"]
end
subgraph Scene2["场景 2: ScrollView + RecyclerView"]
SV["ScrollView<br/>上下滚动"] --> RV2["RecyclerView<br/>列表项滚动"]
RV2 --> Conflict2["冲突:到底该谁滚动?"]
end
2. View 的滑动方式
方式对比
| 方式 |
原理 |
优缺点 |
| scrollTo/scrollBy |
移动 View 内容 |
不改变 View 位置,性能好 |
| 动画 (Translation) |
改变 View 属性 |
GPU 加速,性能好 |
| LayoutParams |
改变 LayoutParams |
性能差,需要 requestLayout |
| requestDisallowIntercept |
父容器不拦截 |
解决滑动冲突 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| view.scrollBy(100, 0)
view.scrollTo(100, 0)
class ScrollView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ViewGroup(context, attrs) { private var lastY = 0f private var scrollY = 0 override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { lastY = event.y } MotionEvent.ACTION_MOVE -> { val dy = (lastY - event.y).toInt() scrollBy(0, dy) lastY = event.y } } return true } override fun scrollBy(x: Int, y: Int) { scrollTo(scrollX + x, scrollY + y) } override fun scrollTo(x: Int, y: Int) { val maxY = computeVerticalScrollRange() - height val newY = y.coerceIn(0, maxY) if (newY != scrollY) { scrollY = newY scrollTo(scrollX, scrollY) } } }
|
动画方式滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| val scroller = Scroller(context)
fun smoothScrollTo(destX: Int, destY: Int) { val deltaX = destX - scrollX val deltaY = destY - scrollY scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 500) invalidate() }
override fun computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.currX, scroller.currY) postInvalidate() } }
ObjectAnimator.ofInt(scrollableView, "scrollY", targetY).apply { duration = 300 start() }
|
3. 外部拦截法
原理
flowchart LR
Down["DOWN"] --> Record["记录初始位置"]
Record --> NoIntercept["不拦截"]
Move["MOVE"] --> Judge["判断滑动方向"]
Judge --> Decide["拦截 / 不拦截"]
Up["UP"] --> End["不拦截"]
Key["关键"] --> Intercept["onInterceptTouchEvent"]
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class HorizontalViewPager : ViewGroup { private var lastX = 0f private var lastY = 0f private var isIntercept = false override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { lastX = ev.x lastY = ev.y isIntercept = false } MotionEvent.ACTION_MOVE -> { val dx = abs(ev.x - lastX) val dy = abs(ev.y - lastY) if (dx > dy && dx > ViewConfiguration.get(context).scaledTouchSlop) { isIntercept = true lastX = ev.x } } } return isIntercept } override fun onTouchEvent(event: MotionEvent): Boolean { return true } }
|
4. 内部拦截法
原理
flowchart LR
Parent["父容器"] --> Child["子 View 处理事件"]
Child --> Dispatch["dispatchTouchEvent"]
Down2["DOWN"] --> DReq["requestDisallowIntercept(false)"]
Move2["MOVE"] --> Boundary["滑动到边界"]
Boundary --> MReq["requestDisallowIntercept(true)"]
Up2["UP"] --> UReq["requestDisallowIntercept(false)"]
父容器实现
1 2 3 4 5 6 7
| class ParentViewGroup : ViewGroup { override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { return ev.action != MotionEvent.ACTION_DOWN } }
|
子 View 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class ChildRecyclerView : RecyclerView { override fun dispatchTouchEvent(ev: MotionEvent): Boolean { when (ev.action) { MotionEvent.ACTION_DOWN -> { parent.requestDisallowInterceptTouchEvent(true) } MotionEvent.ACTION_MOVE -> { if (!canScrollVertically(1) || !canScrollVertically(-1)) { parent.requestDisallowInterceptTouchEvent(false) } } MotionEvent.ACTION_UP -> { parent.requestDisallowInterceptTouchEvent(false) } } return super.dispatchTouchEvent(ev) } }
|
核心接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| class NestedParentView : ViewGroup, NestedScrollingParent { override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean { return true } override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) { } override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int) { } override fun onStopNestedScroll(target: View) { } }
class NestedChildView : View, NestedScrollingChild { private val helper = NestedScrollingChildHelper(this) override fun onTouchEvent(e: MotionEvent): Boolean { when (e.action) { MotionEvent.ACTION_DOWN -> { helper.startNestedScroll(View.SCROLL_AXIS_VERTICAL) } MotionEvent.ACTION_MOVE -> { helper.dispatchNestedScroll(0, dyConsumed, 0, dyUnconsumed, null) } MotionEvent.ACTION_UP -> { helper.stopNestedScroll() } } return super.onTouchEvent(e) } }
|
RecyclerView 嵌套滚动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
<ScrollView android:layout_width="match_parent" android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView android:layout_width="match_parent" android:layout_height="wrap_content" />
</ScrollView>
recyclerView.layoutManager = LinearLayoutManager() { }
|
6. 实战:CoordinatorLayout 原理
Behavior 机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class CustomBehavior : CoordinatorLayout.Behavior<View> { override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { return dependency.id == R.id.dependency } override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { child.y = dependency.y + dependency.height return true } override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean { parent.onLayoutChild(child, layoutDirection) return true } }
<androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent">
<View android:id="@+id/dependency" android:layout_width="match_parent" android:layout_height="100dp" />
<View android:layout_width="match_parent" android:layout_height="100dp" app:layout_behavior=".CustomBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
滑动冲突解决总结
flowchart TB
Strategy["滑动冲突解决策略"] --> Outer["外部拦截法(推荐)<br/>父容器在 onInterceptTouchEvent 判断方向"]
Strategy --> Inner["内部拦截法<br/>子 View 控制父容器是否拦截"]
Strategy --> Nested["NestedScrolling(推荐)<br/>官方嵌套滚动方案,自动处理冲突"]
面试常问
| 问题 |
答案 |
| scrollBy 和 scrollTo 区别? |
scrollBy 相对滚动,scrollTo 绝对滚动 |
| 滑动冲突如何解决? |
外部拦截法/内部拦截法/NestedScrolling |
| CoordinatorLayout 原理? |
Behavior 机制,监听依赖 View 变化 |
相关文章: