03 · 主题系统与 GT 点击抑制(ignoreClick 全解)

本篇回答两个问题:① Normal/GT/Ultra 主题到底差在哪;② “GT 主题点轨道不跳值”是怎么 实现的。后者是 N801PROB-132401 家族所有”点击抖动/误取整”问题的组件库侧地基。

1. 主题如何生效

flowchart LR
    Attr["theme attr<br/>micarxThemeStyleName"] --> Read["ThemeStyleUtil.getThemeNameFromAttr<br/>BaseSlider.java:591-592"]
    Read --> Create["XxxThemeDelegate.create(themeName)"]
    Create -->|GT_STYLE| GT["Gt 实现"]
    Create -->|ULTRA_STYLE| Ultra["Ultra 实现(与 GT 逐行相同)"]
    Create -->|"null/其他"| Normal["Normal 实现(原样直通)"]

四套 delegate 接口各管一个控件族,互不继承(01 篇 §4):

接口服务对象方法数点击抑制方法
SliderThemeDelegate(MAT,:15-88)BaseSlider/Slider9无
IconSliderThemeDelegate(ABW,:18-154)IconSlider~13shouldIgnoreClick()
SeekBarThemeDelegate(MAT,:19-175)AppCompatSeekBar + IconSeekBar16shouldTransformTouchEvent()
VerticalSliderThemeDelegate(ABW,:16-55)VerticalSlider3无(XML attr/setter)

2. 三主题差异总表

Slider 族(SliderThemeDelegate)

方法NormalGT / Ultra
adjustThumbDrawableBounds按 thumbDiameter 等比缩放保持 drawable 固有尺寸
getEffectiveThumbPaddingthumbPaddingthumbPaddingGT(8dp,dimens.xml:30)
adjustInactiveTrackRight原样非激活轨道从 thumb 右侧之外起画(GtSliderThemeDelegate:31-42)
adjustActiveTrackRight原样激活轨道在 thumb 左侧之外收笔(GtSliderThemeDelegate:45-56)
isPressEnlargeEnabledtruefalse → setPressEnlarge 被 BaseSlider:3205 直接拦截
isNormalThemetruefalse → processAttributes 把 thumb 换成 GT 竖条(6dp×32dp,BaseSlider:626-635)
hasCustomAnimfalse → TrackingAnimation(整体 scaleY 1.2)true → SliderAnimation(轨道增厚+thumb 拉高)

SeekBar 族(SeekBarThemeDelegate)与 IconSlider 族

  • GT/Ultra 的共性改造:thumb 换 GT 竖条资源、thumb icon 置 null、textStart/End 强制改写为 "min"/"max" 数字(GtSeekBarThemeDelegate:21-46、GtIconSliderThemeDelegate:23-46)、 轨道在 thumb 处挖空成两段(clip)、hint 文字悬在轨道上方、轨道中心下移 (height+hintTextHeight)/2。
  • 点击抑制:GT/Ultra 恒 true,Normal 恒 false:
    • IconSlider:shouldIgnoreClick() — GtIconSliderThemeDelegate:152-154 / UltraIconSliderThemeDelegate:153-155 / NormalIconSliderThemeDelegate:109-111
    • IconSeekBar:shouldTransformTouchEvent() — GtSeekBarThemeDelegate:157-159 / UltraSeekBarThemeDelegate:157-159 / NormalSeekBarThemeDelegate:123-125

3. GT 点击抑制的实现:触摸事件重锚定(本篇核心)

3.1 机制总览

不是丢弃事件,而是把事件 X 坐标平移到 thumb 当前位置,使下游一切逻辑看到的等效于 “用户精确按在 thumb 上”。

flowchart TD
    Down["ACTION_DOWN 落在轨道任意点<br/>例:x=800,thumb 实际在 x=300"] --> Compute["computeTouchOffset(down)<br/>BaseSlider.java:2314-2321"]
    Compute --> Formula["progress=(value−from)/(to−from)<br/>thumbX=sidePad+effPad+progress×(trackWidth−2·effPad)<br/>**mOffset = thumbX − down.getX() = −500**"]
    Formula --> Apply["之后所有事件(DOWN/MOVE/UP/CANCEL)<br/>event.offsetLocation(mOffset, 0) :2310<br/>触摸点被永久'偷'到 thumb 上"]
    Apply --> Down2["下游 generateTouchPosition(x−500)<br/>反算 touchPosition ≡ thumb 当前进度"]
    Down2 --> Snap["snapThumbToValue: 新值≡旧值<br/>|Δ| < THRESHOLD(1e-4) :2417"]
    Snap --> NoChange["**值不变、无 onValueChange**"]
    Apply --> Drag["MOVE: x' = x−500 跟手走<br/>等效按住 thumb 拖动 → 值随动"]

3.2 Slider 族链路(ignoreClick)

// BaseSlider.java
:268   private boolean ignoreClick;                          // 标志位
:595   ignoreClick = ...getBoolean(MiSlider_ignoreClick, false);  // XML 来源,默认 false
:670   public void setIgnoreClick(boolean)                   // 代码来源
 
:2177  if (ignoreClick){ transformTouchEvent(event); }       // onTouchEvent 入口拦截
 
:2304  protected void transformTouchEvent(MotionEvent event) {
           switch (event.getAction()) {
             case ACTION_DOWN: computeTouchOffset(event);     // 只在 DOWN 算一次
             default:          event.offsetLocation(mOffset, 0);  // 全程平移
           }
       }

谁强制打开:material Slider 层的 XML 默认 false,默认样式 Widget.MiCar.Slider 也未设; 真正强制的是 IconSlider——IconSlider.onTouchEvent:570-572 每个触摸事件先 if (delegate.shouldIgnoreClick()) setIgnoreClick(true)。所以 GT/Ultra 的 IconSlider 必然 ignoreClick=true。

3.3 SeekBar 族链路(shouldTransformTouchEvent,孪生实现)

// IconSeekBar.java:596-598
if (mSeekBarThemeDelegate.shouldTransformTouchEvent()) {   // GT/Ultra true
    transformTouchEvent(event);                            // AppCompatSeekBar:240-248
}
// AppCompatSeekBar.java:250-257 —— 用 getProgress() 与屏幕 rawX 算同一偏移
private void computeTouchOffset(MotionEvent down) {
    getLocationOnScreen(mSeekBarLocation);
    float progress = (getProgress() - getMin()) / (float)(getMax() - getMin());
    float currentX = mSeekBarLocation[0] + getPaddingStart() + progress * visualWidth;
    mOffset = currentX - down.getRawX();
}

注意两族算偏移的坐标系不同:BaseSlider 用 view 内 x,AppCompatSeekBar 用屏幕 rawX。 BaseSlider 里有个同名死字段 mSeekBarLocation(:326,从未使用),grep 时勿混淆。

3.4 为什么用”偏移”而不是”丢弃事件”

方案后果
丢弃轨道 DOWN按压动画、onStartTrackingTouch、按住轨道直接拖全部失效
偏移重锚定(现方案)动画/回调/手势全保留,只是值锚点换成 thumb

这正是产品裁决”GT 主题点击仅按压动画、无值变化”(N801PROB-132401,2026-10-13)的技术 底座。车机默认样式 Widget.MiCar.Slider 的 labelBehavior=gone,所以纯 GT 点击用户只感知到 thumb/轨道动画,值不动。

3.5 三个结构性隐患(排障必读)

  1. mOffset 只在 DOWN 算一次,手势期间不更新。若手势中途程序性 setValue 改了 thumb 位置,偏移不刷新 → thumb 相对手指瞬移(拖动脱钩)。GT 下滑动中下方下发值回写会踩到。
  2. DOWN 瞬间微抖被固化进 mOffset:若下游(Settings 层)还有取整逻辑,微抖跨过 .5 边界即随机击穿——N801PROB-132401 第三轮”GT 点击抖动”(34→30/37→40 方向随机)的成因。 修法是 Settings 层读 isIgnoreClick() 把整个取整关掉(387202/387204 PS4), 组件库不动。
  3. UP tap 补偿分支不受影响:lastEvent 记录的是偏移后坐标(:2282),tap 判定 (:2243-2246)在同一坐标系内自洽。

4. “96px 错位”坐标系线索(GT 按压白点错位调查用)

能量回收 KB 08 篇记录的 GT thumb 视觉错位,组件库侧的坐标链:

  • 触摸换算带补偿:(x − trackSidePadding − effectivePadding) / (trackWidth − 2·effectivePadding) (BaseSlider:2288-2289);
  • thumb 绘制中心不带补偿:centerX = trackSidePadding + normalizeValue(value) × trackWidth (:2130),halo/label/hotspot 同(:1905/:2149/:2596-2598);
  • setTrackRadius() 非 0 时把 trackSidePadding 直接清零(:647-660,清零在 :659), 而 Widget.MiCar.Slider 样式带 trackRadius(appcompat public_styles.xml:590);
  • GT 下 effectivePadding=8dp、thumb 固有宽 6dp,触摸坐标系与绘制坐标系的常量差即错位 来源;具体像素值随 density 变,“96px” 字面常量未在代码出现,数值来源未确认。

5. 按压动画的主题分流(一图)

flowchart TD
    PDOWN["ACTION_DOWN"] --> Q{"控件类型"}
    Q -->|"Slider(BaseSlider 原生)"| QA{"mPressEnlargeEnable &&<br/>!startCustomAnim()? :2188"}
    QA -->|"Normal(delegate hasCustomAnim=false)"| AN1["TrackingAnimation<br/>整体 scaleY 1.0→1.2, 300ms<br/>BaseSlider:3224-3252"]
    QA -->|"GT/Ultra(hasCustomAnim=true)"| AN2["Slider.SliderAnimation<br/>trackOffsetY 0→2dp + thumb高×2, 225ms<br/>Slider.java:173-228"]
    Q -->|IconSlider| QB["startCustomAnim 被 override 恒 true :340-347<br/>**BaseSlider/Slider 动画全被抑制**<br/>走自研双组动画(04 篇)"]
    Q -->|IconSeekBar| QC["startPressAnim 恒 true :626-649<br/>**父类 SeekBarAnimation 被抑制**<br/>走自研双组动画(04 篇)"]