要理解 View 的滑动,我们可以从「底层原理」和「生活类比」两个维度展开。先看透本质,再用故事串起来,保证大家都能秒懂。
一、底层原理:滑动的本质是什么?
View 的滑动,本质是改变 View 在屏幕上的「绘制位置」。而位置的计算,依赖于 Android 的「坐标系」和「View 的位置参数」。
1. 坐标系:屏幕上的 "地图"
Android 规定了一套坐标系:
-
原点(0,0)在屏幕左上角;
-
向右为 x 轴正方向,向下为 y 轴正方向(和数学坐标系的 y 轴相反,记住 "下为正" 即可)。
每个 View 在屏幕上的位置,都用 4 个核心参数描述:
-
left
:View 左边缘到父容器左边缘的 x 距离; -
top
:View 上边缘到父容器上边缘的 y 距离; -
right
:View 右边缘到父容器左边缘的 x 距离(= left + 宽度); -
bottom
:View 下边缘到父容器上边缘的 y 距离(= top + 高度)。
这 4 个参数就像 View 的 "坐标标签",决定了它在父容器中的位置。
2. 滑动的底层逻辑:改参数 = 改位置
要让 View 滑动,本质是修改这些位置参数,或者通过「偏移绘制」让 View "看起来动了"。具体有 3 种核心方式,对应不同的参数修改逻辑:
滑动方式 | 底层修改的参数 | 效果 |
---|---|---|
scrollTo/scrollBy | 父容器的mScrollX /mScrollY |
父容器 "滚动",内部子 View 的显示位置偏移(子 View 自身位置不变) |
LayoutParams | View 的布局参数(如margin 、x 、y ) |
父容器重新布局,View 的left /top 等参数改变(View 自身位置真的动了) |
offsetLeftAndRight/offsetTopAndBottom | View 的left /right /top /bottom |
直接修改 View 的位置参数,强制刷新显示(View 自身位置真的动了) |
3. 触摸事件:滑动的 "动力源"
用户的滑动操作(手指在屏幕上拖动),会被包装成「MotionEvent」对象(包含触摸点的 x/y 坐标)。这个事件会从 Activity 传递到 ViewGroup,再到具体的 View(类似 "快递派送")。
当 View 收到ACTION_MOVE
事件(手指滑动中)时,就可以根据「起始位置」和「当前位置」的差值,计算出需要滑动的距离(dx、dy),然后用上面 3 种方式中的一种执行滑动。
二、生活类比:用 "房间 + 家具" 讲透滑动
把手机屏幕比作一个「房间」,View 是房间里的「家具」(比如桌子、椅子),滑动就是 "改变家具的位置" 或 "改变看家具的视角"。
1. 坐标系:房间的 "定位规则"
房间的左上角墙角是「原点(0,0)」:
-
沿着墙向右(x 轴),每走 1 厘米记为 x=1;
-
沿着墙向下(y 轴),每走 1 厘米记为 y=1。
每个家具(View)的位置用 4 个参数标记:
-
left
:家具左边缘到房间左墙的距离; -
top
:家具上边缘到房间上墙的距离; -
right
:家具右边缘到房间左墙的距离(= left + 家具宽度); -
bottom
:家具下边缘到房间上墙的距离(= top + 家具高度)。
比如一张桌子左边缘离左墙 30cm,上边缘离上墙 40cm,宽 50cm,高 60cm,那它的参数就是:left=30,top=40,right=80(30+50),bottom=100(40+60)。
2. 触摸事件:"推动家具的手"
用户的手指滑动,就像 "用手推家具":
-
手指刚碰到家具(
ACTION_DOWN
):记录初始位置(比如手指在桌子上的点,距离房间左墙 x1=50,上墙 y1=60); -
手指拖动(
ACTION_MOVE
):实时记录当前位置(x2=70,y2=80),计算出滑动距离 dx=20(70-50),dy=20(80-60); -
手指离开(
ACTION_UP
):结束滑动。
这个 "dx、dy" 就是家具需要移动的距离,接下来就是 "怎么移" 的问题 ------ 对应 3 种滑动方式。
3. 三种滑动方式:怎么移动家具?
我们用 "移动桌子" 的场景,对应三种底层实现:
方式 1:scrollTo/scrollBy("人不动,移动视线")
底层逻辑 :父容器(比如房间地面)记录一个 "滚动偏移量"(mScrollX
/mScrollY
),绘制子 View 时,会根据这个偏移量 "偏移绘制位置"。子 View 的left
/top
没变,只是看起来动了。
生活类比 :
你站在房间中间看桌子(初始位置),现在你不移动桌子,也不自己动,而是让 "视线平移"(比如戴了一副可以左右滑动的眼镜)。此时桌子在地面上的实际位置(left/top)没变,但你看到的桌子位置 "偏移" 了。
举例 :
手机里的ListView
滑动时,其实是ListView
(父容器)通过scrollBy(dx, dy)
改变mScrollY
,每个 item(子 View)的left
/top
没变,但绘制时会随父容器的偏移而 "向上 / 向下移动",看起来就是列表在滚。
方式 2:LayoutParams("移动桌子的地脚")
底层逻辑 :修改 View 的布局参数(比如marginLeft
、width
),父容器会触发requestLayout()
(重新测量、布局、绘制),最终改变 View 的left
/top
,实现位置变化。
生活类比 :
桌子的的脚是可以调节的(类似LayoutParams
)。你想把桌子向右移 10cm,就把左侧的脚移动 10cm(增加marginLeft
)。此时房间会重新计算桌子的位置(left = 原来的 left + 10),桌子的实际位置真的变了。
举例 :
拖动一个按钮时,我们可以通过LayoutParams
修改它的leftMargin
和topMargin
,按钮会真的在父容器中 "平移",left
/top
参数也会同步更新。
方式 3:offsetLeftAndRight("直接推桌子")
底层逻辑 :直接修改 View 的left
/right
(水平方向)或top
/bottom
(垂直方向),然后调用invalidate()
刷新绘制。View 的位置参数直接改变,无需父容器重新布局。
生活类比 :
你直接用手推桌子,桌子在地面上的实际位置(left/top)被你硬生生推远了 10cm。此时桌子的left
从 30 变成 40,right
从 80 变成 90,位置参数直接变了,房间不需要重新规划布局,桌子自己动了。
举例 :
自定义一个可拖动的 View 时,在ACTION_MOVE
中调用offsetLeftAndRight(dx)
,可以直接让 View 随手指移动,效率很高(无需重新测量布局)。
三、总结:三种方式的核心区别
方式 | 本质 | 适用场景 | 特点 |
---|---|---|---|
scrollTo/scrollBy | 父容器偏移绘制,子 View 位置不变 | 滚动列表(ListView/RecyclerView)、滑动面板 | 效率高,适合大量子 View 滑动 |
LayoutParams | 改布局参数,父容器重新布局 | 拖动 View(如按钮、悬浮窗) | 位置真的改变,适合单个 View 移动 |
offset 系列 | 直接改位置参数,强制刷新 | 快速拖动单个 View | 效率最高,适合高频滑动(如手势拖动) |
最后:一句话记住
View 滑动的本质是 "改变绘制位置",要么通过父容器的偏移让它 "看起来动了"(scroll),要么通过修改自身参数让它 "真的动了"(LayoutParams/offset)。就像移动家具,要么你戴个偏移眼镜看它(scroll),要么直接推它(offset),要么移动桌子的脚让它换位置(LayoutParams)------ 核心都是 "位置变了"。