02 · 触摸事件全流程(核心篇)
入口只有一个:
onTouchEvent——BaseSlider/Slider/IconSlider/IconSeekBar 全都不 overridedispatchTouchEvent(全模块 grep 实证)。拦截一律发生在 onTouchEvent 开头。
1. BaseSlider 手势总流程图
flowchart TD Enter["onTouchEvent(event)<br/>BaseSlider.java:2172"] --> Enabled{"isEnabled?<br/>:2173"} Enabled -->|否| RetFalse["return false"] Enabled -->|是| Ignore{"ignoreClick?<br/>:2177"} Ignore -->|是| Transform["transformTouchEvent(event)<br/>:2304-2312<br/>DOWN 算 mOffset,全程 offsetLocation<br/>(详见 03 篇)"] Ignore -->|否| Dispatch Transform --> Dispatch Dispatch{"event.getActionMasked()<br/>:2183"} Dispatch -->|ACTION_DOWN :2184| D1["generateTouchPosition(x) :2186<br/>touchPosition=(x−sidePadding−effPadding)<br/>/(trackWidth−2·effPadding) clamp[0,1]"] D1 --> D2["按压动画 :2188<br/>Normal: mTrackingAnim.start()<br/>GT/Ultra: startCustomAnim()→SliderAnimation"] D2 --> D3{"垂直滚动容器内?<br/>:2196 isInVerticalScrollingContainer"} D3 -->|是| DBreak["break(等 MOVE 判方向)"] D3 -->|否| D4["requestDisallowInterceptTouchEvent(true) :2200"] D4 --> D5{"pickActiveThumb() :2202<br/>Slider override 恒选 thumb0"} D5 -->|失败| DBreak D5 -->|成功| D6["thumbIsPressed=true :2208<br/>snapTouchPosition() :2209 ← 点轨道即跳值<br/>onStartTrackingTouch() :2212"] Dispatch -->|ACTION_MOVE :2214| M1["generateTouchPosition(x) :2215"] M1 --> M2{"thumbIsPressed? :2216"} M2 -->|否| M3{"|x−touchDownX| < slop?<br/>:2218 (slop=系统值/2 :470)"} M3 -->|是| RetFalse2["return false(还给父容器滚动)"] M3 -->|否| M4["disallowIntercept + onStartTrackingTouch :2221-2222"] M2 -->|是| M5 M4 --> M5["pickActiveThumb :2225<br/>thumbIsPressed=true :2230<br/>snapTouchPosition() :2231"] Dispatch -->|ACTION_UP :2235| U1["动画回退 :2237-2239<br/>thumbIsPressed=false :2241"] U1 --> U2{"tap 判定:上一事件是DOWN<br/>且 |dx|,|dy| ≤ slop?<br/>:2243-2246"} U2 -->|是| U3["补发 onStartTrackingTouch :2248<br/>(与 stop 配对)"] U2 -->|否| U4 U3 --> U4{"activeThumbIdx ≠ -1?<br/>:2252"} U4 -->|是| U5["snapTouchPosition() 终值 :2253<br/>activeThumbIdx=-1 :2254<br/>onStopTrackingTouch() :2255"] U4 -->|否| U6 U5 --> U6["trackOffsetY=0 :2257<br/>invalidate()"] Dispatch -->|ACTION_CANCEL :2260| C1["同 UP,但无 tap 补发<br/>:2260-2274"] D6 --> Tail M5 --> Tail U6 --> Tail C1 --> Tail DBreak --> Tail Tail["setPressed(thumbIsPressed||animPressed) :2280<br/>lastEvent = obtain(event) :2282<br/>return true :2283"]
关键行为要点
- 没有几何意义的 hit test:
pickActiveThumb(:2349-2389)是”选最近 thumb”不是”命中 判定”——单 thumb Slider 恒选 thumb0(Slider.java:139-147)。Normal 主题点轨道任何 位置都会 snap 到触点并改值;“点按 vs 拖动”仅靠 UP 分支的 slop 判定(:2243-2246) 补发一对 start/stop 回调。 - tap 会成对触发 onStartTrackingTouch → onStopTrackingTouch(:2248 → :2255)—— 下游如果把 start 当”开始拖动”语义用,会看到一次”零拖动的拖动”。
- scaledTouchSlop 是系统值的一半(:470),比常见 view 更敏感。
- 滚动容器内 DOWN 不立即开拖(:2196-2198),MOVE 超 slop 才接管;
setScrollDragEnable(true)(:3199)可强制吃掉滚动容器。 - UP/CANCEL 一定把
trackOffsetY清零(:2257/:2271)——GT 按压轨道增厚量的复位点。
2. x → value 换算链(改值唯一路径)
flowchart LR X["event.getX()"] --> GTP["generateTouchPosition :2287-2292<br/>touchPosition=(x−sidePad−effPad)<br/>/(trackWidth−2·effPad)"] GTP --> GVT["getValueOfTouchPosition :2459-2467"] GVT --> SP{"stepSize > 0?<br/>snapPosition :2335-2342"} SP -->|是| Snap["Math.round(pos×stepCount)/stepCount<br/>**组件库唯一吸附点**"] SP -->|否| Raw["原样(连续模式无取整)"] Snap --> Map["pos×(valueTo−valueFrom)+valueFrom"] Raw --> Map Map --> STV["snapThumbToValue(idx, value) :2413-2427"] STV --> Thr{"|new−old| < 1e-4?<br/>THRESHOLD :244"} Thr -->|是| NoOp["return false 无回调"] Thr -->|否| Clamp["getClampedValue :2430-2440<br/>clamp 到 [valueFrom, valueTo]"] Clamp --> Set["values.set(idx, newValue) :2423"] Set --> Fire["dispatchOnChangedFromUser :2426<br/>onValueChange(slider, value, fromUser=true)<br/>:2651-2658"] Fire --> A11y["无障碍节流于 200ms :2945-2954"]
组件库层没有任何”整十取整”:alignAdjustProgress/mProgressChangeTimes 全仓 grep
零命中。整十吸附是 Settings 层 SliderPreference 的机制(能量回收 KB 02 篇),它消费的正是
这里的 fromUser=true 回调序列。
3. 回调时机全表
| 事件 | 触发点 | 回调 |
|---|---|---|
| DOWN 成功 pick thumb | :2212 | onStartTrackingTouch |
| 滚动容器内首次有效 MOVE | :2222 | onStartTrackingTouch |
| 每个 DOWN/MOVE 且值变化 | :2426 | onValueChange(fromUser=true) |
| UP tap 补发 | :2248 | onStartTrackingTouch(与 stop 配对) |
| UP/CANCEL 且 activeThumbIdx≠-1 | :2255/:2269 | onStopTrackingTouch |
| 程序 setValue 且值变化 | :2642-2648 | onValueChange(fromUser=false),每个 value 回调一次 |
| 程序 setValue 值未变 | :915-917 | 短路,无回调 |
4. IconSlider 的触摸流程(在 BaseSlider 之上加的一层)
flowchart TD IEnter["IconSlider.onTouchEvent :566-598"] --> IEnabled{"isEnabled? :568"} IEnabled -->|否| ISuper["super.onTouchEvent(BaseSlider)"] IEnabled -->|是| IIgnore{"mIconSliderThemeDelegate<br/>.shouldIgnoreClick()? :570"} IIgnore -->|GT/Ultra: true| ISetIgnore["setIgnoreClick(true) :571<br/>→ BaseSlider :2177 生效"] IIgnore -->|Normal: false| ISwitch ISetIgnore --> ISwitch ISwitch{"action"} ISwitch -->|DOWN :574| ID1["delegate.applyPressOffsets(GT:边缘判定)<br/>startPressAnim(event) :581"] ISwitch -->|MOVE :584| IM1{"!mIsPressAnimEnter? :585"} IM1 -->|是| IM2["补触发 startPressAnim :586<br/>(DOWN 没命中 thumb,MOVE 进入时)"] ISwitch -->|"UP/CANCEL :589"| IU1["startUpPressAnim() :590<br/>清零 mOffsetXLeft/Right"] ID1 --> ISuper2["super.onTouchEvent → BaseSlider<br/>(走 §1 全流程) :597"] IM2 --> ISuper2 IU1 --> ISuper2 IM1 -->|否| ISuper2
startPressAnim内部分流(IconSlider.java:601-621):GT/Ultra 由shouldStartPressAnim(mPressAnimEnable)直接跑背景扩张动画;Normal 才有真正的 thumb 几何 hit test(x∈[start, start+mThumbWidth] 且 y 在轨道带内,:615-616)才进 按压态。- 值改变本身不做 hit test:Normal 主题点轨道照样跳值(BaseSlider 原生 snap-to-touch), GT/Ultra 靠 ignoreClick 偏移把触点挪到 thumb(03 篇)。
5. IconSeekBar 的触摸流程(int 体系)
flowchart TD SEnter["IconSeekBar.onTouchEvent :592-623"] --> SEn{"isEnabled? :593"} SEn -->|否| SSuper SEn -->|是| STrans{"mSeekBarThemeDelegate<br/>.shouldTransformTouchEvent()? :596"} STrans -->|"GT/Ultra: true"| ST["transformTouchEvent(event) :597<br/>AppCompatSeekBar:240-248<br/>DOWN 算偏移,全程平移到 thumb"] STrans -->|Normal: false| SS ST --> SS{"action"} SS -->|DOWN :600| SD["calculatePressOffsets(delegate)<br/>startPressAnim(event) :608"] SS -->|MOVE :611| SM["!mIsPressAnimEnter 时补 startPressAnim :612-614"] SS -->|"UP/CANCEL :616"| SU["startUpPressAnim(event) :618"] SD --> SSuper["super.onTouchEvent :622<br/>→ micarx AppCompatSeekBar.onTouchEvent :208-237"] SM --> SSuper SU --> SSuper SSuper --> SParent{"父类动画分流 :225"} SParent -->|"startPressAnim 返回 true(IconSeekBar 恒 true :626-649)"| SNoAnim["父类 SeekBarAnimation 被抑制"] SParent -->|否则| SAnim["父类 mSeekBarAnim.start()<br/>track scaleY + thumb 拉高"] SSuper --> SFramework["最终交 super(framework SeekBar)<br/>trackTouchEvent(int progress)<br/>AppCompatSeekBar.java:236"]
⚠️ 双触发怪癖:IconSeekBar.onTouchEvent 先调一次 startPressAnim(:608),super 链
micarx AppCompatSeekBar.onTouchEvent 在 DOWN 分支又调一次(:225)。GT 路径下 expand 动画
会重建两次(功能幂等但冗余)。
6. VerticalSlider(垂直系,自绘拷贝版)
- 坐标系:0 在顶端、1 在底端——
normalizeValue = 1 − (v−from)/(to−from)(:670-672), 触点换算mTouchPosition = 1 − clamp(y/trackHeight)(:674-676)。 - onTouchEvent(:331-395)结构与 BaseSlider 同款:DOWN 记 mTouchDownY、横向滚动容器等 MOVE
判方向(:348-350,水平 slider 判垂直滚动容器,垂直 slider 判横向滚动容器 :781-793)、
pickActiveThumb恒选 thumb0、DOWN 即 snap 跳值(除非 ignoreClick :114/:700-706)。 - 只有 OnChangeListener,没有 start/stop tracking 概念(:805-808)。
- 程序
setValues会对每个 value 都回调一遍 fromUser=false(:723-730)。
7. 状态标志迁移速查
| 标志 | 置位 | 复位 |
|---|---|---|
thumbIsPressed(:306) | DOWN :2208 / MOVE :2230 | UP :2241 / CANCEL :2261 / setEnabled(false) :1785 / setVisibility(GONE) :1795 |
activeThumbIdx(:313) | pickActiveThumb/键盘 | UP :2254 / CANCEL :2268 / DPAD_CENTER :2745 |
trackOffsetY(:256,GT 按压增厚) | SliderAnimation update(Slider.java:190) | UP :2257 / CANCEL :2271 / 动画 cancel(Slider.java:203-205) |
labelsAreAnimatedIn(:285) | ensureLabelsAdded :2558 | ensureLabelsRemoved :2530 / onDetachedFromWindow :1838 |
ignoreClick(:268) | XML attr :595 / setIgnoreClick :670 / IconSlider 每事件强制 :571 | — |
mIsPressAnimEnter(IconSlider/IconSeekBar) | startPressAnim 成功 | startUpPressAnim / setEnabled(false) |