在安卓魔法学院的《无限画册》里,每张魔法卡片(页面)的滑动都像被施了咒语 —— 手指轻轻一推,卡片就会顺滑地移动,停在该去的位置。这背后,是ViewPager(小帕) 和 “滑动团队” 的精密配合。今天,我们就跟着一次完整的滑动过程,揭开这个咒语的秘密。
第一章:滑动团队的 “魔法分工”
先认识下负责滑动的核心成员(比之前更聚焦滑动):
- ViewPager(小帕) :滑动的 “总调度师”,决定是否响应滑动、该滑多远、滑到哪一页。
- 触摸精灵(Touch Handler) :住在小帕身体里的感知者,能 “听” 懂手指的动作(按下、滑动、抬起),对应源码里的
onTouchEvent()方法。 - 拦截使者(Intercept Guard) :小帕的 “门卫”,判断滑动是否该由小帕处理(而不是交给子 View),对应
onInterceptTouchEvent()方法。 - 滚动精灵(Scroller) :掌握 “平滑魔法” 的工匠,能让卡片匀速移动(而不是瞬间跳变),对应源码里的
Scroller类。 - 页面坐标本:记录每张卡片的 “家”(位置),比如第 0 页在
x=0,第 1 页在x=屏幕宽度,第 - 1 页在x=-屏幕宽度(默认水平滑动)。
第二章:滑动的完整咒语 —— 从手指碰到卡片开始
假设画册当前展示第 1 页(兔子卡片),访客想滑到第 2 页(猴子卡片),我们来看看每个步骤:
第一步:手指 “敲门”—— 触摸开始(ACTION_DOWN)
访客的手指轻轻按在兔子卡片上(ACTION_DOWN事件),触摸精灵立刻惊醒:
“有手指碰我啦!坐标在(100, 150),是按下动作!”
它马上告诉小帕:“准备开始滑动,先记下初始位置(downX=100),把滚动精灵的魔法重置(scroller.abortAnimation()),别让之前的滑动干扰现在。”
(源码对应:ViewPager.onTouchEvent(MotionEvent ev)处理ACTION_DOWN时,会记录按下的 x 坐标,停止正在进行的滚动动画,为新滑动做准备。)
第二步:手指 “推卡片”—— 滑动过程(ACTION_MOVE)
访客手指向右滑动(推卡片),手指位置从 100 移到 300(moveX=300),触摸精灵大喊:
“滑动啦!x 方向移动了 200 像素(dx=300-100=200)!”
小帕立刻计算:“当前页面是第 1 页,它的初始位置在x=0。向右滑时,第 1 页要向左移,第 2 页(猴子)要从右边进来。”
于是小帕指挥两张卡片同时移动:
-
第 1 页的位置变成
x=-200(向左移 200); -
第 2 页的位置变成
x=屏幕宽度-200(假设屏幕宽 300,就是300-200=100,从右边露出 100 像素)。
(源码对应:ACTION_MOVE时,ViewPager会计算手指移动的距离dx,然后通过scrollBy(-dx, 0)让自身滚动 —— 本质是让子 View 的位置反向移动,营造 “卡片被推动” 的视觉效果。)
这时候,拦截使者会一直 “盯着” 滑动:如果滑动是水平的(符合画册滑动规则),就不让子 View(比如卡片里的按钮)抢走事件(onInterceptTouchEvent()返回 true,拦截事件给小帕处理);如果是垂直滑动(可能想滑动卡片里的列表),就不拦截(返回 false)。
第三步:手指 “松开手”—— 滑动结束(ACTION_UP)
访客松开手指,触摸精灵报告:“滑动结束!总移动距离是 250 像素(从 100 到 350)。”
小帕拿出 “判断尺子”(阈值):“屏幕宽 300 像素,滑动超过一半(150 像素)就算成功切换!250>150,该去第 2 页!”
于是小帕对滚动精灵说:“现在位置在x=-250(当前页向左移了 250),目标是滑到x=-300(第 2 页完全显示),剩下 50 像素,用 300 毫秒滑过去,要平滑哦~”
滚动精灵掏出 “魔法卷轴”(Scroller),记下参数:startX=-250,dx=-50(最终到 - 300),duration=300,然后喊:“平滑魔法启动!”
(源码对应:ACTION_UP时,ViewPager计算总滑动距离,若超过屏幕一半,就调用scroller.startScroll()设置滚动参数,然后触发重绘。)
第四步:平滑滚动到目标页(computeScroll())
滚动精灵的魔法不是一步到位的,而是 “分帧施法”:
每过 10 毫秒(一帧),它就看一眼魔法卷轴:“现在该滑到哪里了?”(scroller.computeScrollOffset()计算当前位置)。
比如第 10 毫秒,位置在x=-255;第 20 毫秒,x=-260... 直到 300 毫秒后,精准停在x=-300。
小帕则在每帧刷新时(computeScroll()方法)问滚动精灵:“现在该移到哪?” 然后根据返回的位置移动自己(scrollTo(currentX, 0)),让卡片跟着动。
最后,当滚动结束(scroller.isFinished()为 true),小帕宣布:“第 2 页就位!” 并通知帕朵(PagerAdapter):“可以把第 0 页(如果存在)暂时收进仓库啦~”
(源码对应:ViewPager.computeScroll()是平滑滚动的核心,会不断检查Scroller的滚动状态,调用scrollTo()更新位置,直到滚动完成。)
万一滑动不够远呢?
如果访客只滑了 100 像素(没超过 150),小帕会说:“没到切换条件,回原位!”
滚动精灵就会启动 “回弹魔法”,从x=-100平滑滚回x=0,让卡片回到第 1 页。
第三章:滑动咒语的 “源码对应表”
| 童话场景 | 源码方法 / 类 | 核心作用 |
|---|---|---|
| 手指按下(记录初始位置) | ViewPager.onTouchEvent(ACTION_DOWN) | 记录按下坐标,停止当前滚动,初始化滑动状态。 |
| 手指滑动(实时移动卡片) | ViewPager.onTouchEvent(ACTION_MOVE) | 计算滑动距离,调用scrollBy()实时移动页面,更新显示。 |
| 判断是否拦截滑动 | ViewPager.onInterceptTouchEvent() | 决定是否拦截触摸事件(比如水平滑动由 ViewPager 处理,垂直滑动交给子 View)。 |
| 手指抬起(决定目标页) | ViewPager.onTouchEvent(ACTION_UP) | 计算总滑动距离,判断是否切换页面,调用Scroller.startScroll()启动平滑滚动。 |
| 平滑滚动到目标位置 | Scroller + ViewPager.computeScroll() | Scroller记录滚动参数,computeScroll()每帧更新位置,实现顺滑移动。 |
总结:滑动的本质
ViewPager 的滑动,其实是一场 “触摸感知→距离计算→决策切换→平滑滚动” 的接力赛:
-
触摸精灵负责 “听” 手指指令;
-
小帕负责 “算” 距离、做决策;
-
滚动精灵负责 “移” 到目标位置,让过程不卡顿。
而这一切的核心,是通过onTouchEvent()处理触摸、Scroller管理平滑滚动、computeScroll()分帧更新位置,最终让卡片 “听话” 地滑动~ 这就是《无限画册》的滑动咒语啦!