02 · 触摸事件全流程(核心篇)

入口只有一个:onTouchEvent——BaseSlider/Slider/IconSlider/IconSeekBar 全都不 override dispatchTouchEvent(全模块 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"]

关键行为要点

  1. 没有几何意义的 hit test:pickActiveThumb(:2349-2389)是”选最近 thumb”不是”命中 判定”——单 thumb Slider 恒选 thumb0(Slider.java:139-147)。Normal 主题点轨道任何 位置都会 snap 到触点并改值;“点按 vs 拖动”仅靠 UP 分支的 slop 判定(:2243-2246) 补发一对 start/stop 回调。
  2. tap 会成对触发 onStartTrackingTouch → onStopTrackingTouch(:2248 → :2255)—— 下游如果把 start 当”开始拖动”语义用,会看到一次”零拖动的拖动”。
  3. scaledTouchSlop 是系统值的一半(:470),比常见 view 更敏感。
  4. 滚动容器内 DOWN 不立即开拖(:2196-2198),MOVE 超 slop 才接管;setScrollDragEnable(true) (:3199)可强制吃掉滚动容器。
  5. 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:2212onStartTrackingTouch
滚动容器内首次有效 MOVE:2222onStartTrackingTouch
每个 DOWN/MOVE 且值变化:2426onValueChange(fromUser=true)
UP tap 补发:2248onStartTrackingTouch(与 stop 配对)
UP/CANCEL 且 activeThumbIdx≠-1:2255/:2269onStopTrackingTouch
程序 setValue 且值变化:2642-2648onValueChange(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 :2230UP :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 :2558ensureLabelsRemoved :2530 / onDetachedFromWindow :1838
ignoreClick(:268)XML attr :595 / setIgnoreClick :670 / IconSlider 每事件强制 :571—
mIsPressAnimEnter(IconSlider/IconSeekBar)startPressAnim 成功startUpPressAnim / setEnabled(false)