03 · 源码精讲

代码基线:fuben23_micarsettings/MiCarSettings @ rls-xcd-u-mp-26-v7.0 (HEAD 70f7b9afd = 取整修复,与 cc27d08ba 同一 Change-Id I4a34b69c3943fb0574e6fca89b181850db0e766e,内容 diff 为空;dev 双生 3f3f7659a)。 所有 file:line 以 rls 分支为准;在 dev 工作树核对会有 1~6 行漂移 (dev 多了若干 MLog 行,逻辑逐字一致),以符号定位为准。 仓库:/home/zbc/car/fuben23_micarsettings/MiCarSettings。 ⚠️ controller 半数为 Kotlin,检索必须同时覆盖 .java + .kt (纯 java grep 会漏掉 Custom/MainCustom 两个取整 controller,已踩过的坑)。

本篇逐段拆手势判定/取整/下发的全部相关代码。手势状态机与取整判定表的设计讲解在 02-手势语义与取整机制,本篇负责「代码到底长什么样、在哪一行」。

文件地图

文件行数角色
base/settingsBaseUi/src/main/java/com/android/car/settings/common/SliderPreference.java1118手势判定 + 取整 + 下发的总控,全部滑块的父类
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/preferences/MiCarIconSeekBarPreference.java144带图标/数值标签的滑块 Preference,取整 bug 的案发地
base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/IFusionSlider.kt97Settings 仓侧的滑块接口(组件库实现的适配口)
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/common/views/MiCarIconSeekBar.kt102组件库 IconSeekBar 的薄增强壳
settingsPage/micarDrivingSettings/src/main/java/com/android/car/settings/miauto/driving/BrakingStrengthPreferenceController.java250驾驶偏好主页 controller(Java)
settingsPage/micarDrivingSettings/src/main/java/com/android/micar/settings/driving/custom/CustomEnergyRecoveryStrengthController.kt200M1/M2 自定义页 controller(Kotlin)
settingsPage/micarDrivingSettings/src/main/java/com/android/micar/settings/driving/custom/main/MainCustomEnergyRecoveryStrengthController.kt151主驾自定义页 controller(Kotlin)
settingsPage/micarDrivingSettings/src/main/java/com/android/micar/settings/driving/custom/BaseCustomVehicleProgressPrefController.java271Custom 的父类(注意是 .java)
settingsPage/micarDrivingSettings/src/main/java/com/android/micar/settings/driving/custom/main/BaseMainCustomVehicleProgressPrefController.kt97MainCustom 的父类
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleProgressPrefController.java258值映射 + 信号下发/回显的中枢基类

页面拓扑(哪个 XML 挂哪个 controller、车型门控)见 01-功能与页面结构;GT(mach)只挂主页一个滑块。

1. SliderPreference:手势判定 + 取整 + 下发总控

1.1 常量与工作模式(:77-99)

public final static int SLIDER_NORMAL = 0; // 该模式滑动过程中一直下发信号
public final static int SLIDER_ADAPTER_VEHICLE = 1; // 该模式保证拖动进度条手指抬起时一定触发callbackChange
public static final int MAX_PROGRESS_CHANGE_TIMES = 1; // 判断是否静止时进度最大改变次数
public static final float DEFAULT_DESCRIPTION_STEP_VALUE = 1F; // 默认的Slider步长,如果设置为10,云端会进行四舍五入
  • SLIDER_NORMAL(:77)= 拖动跟手逐值下发;SLIDER_ADAPTER_VEHICLE(:78)= 拖动中不下发、 抬手一次。基类默认是后者(见 §4 BaseVehicleProgressPrefController.onCreateInternal:43), 三个能量回收 controller 都显式改回 SLIDER_NORMAL(§3)。
  • MAX_PROGRESS_CHANGE_TIMES = 1(:81)——「点击 vs 拖动」的判定门: 一次手势内整数值变化次数 == 1 视为「点击轨道」,松手时取整吸附;> 1 视为拖动,不吸附。
  • DEFAULT_DESCRIPTION_STEP_VALUE(:83)的注释是语音/云端侧的关键约定: 步长设为 10 时云端会做四舍五入——这就是为什么三个 controller 都设 STEP_FOR_VOICE_ASSIST = 10(语音「调大一点」= ±10,与点击取整粒度一致)。
  • 取整粒度字段:mIsRoundUpWhenClick(:141)默认 false、mRoundUpStep = 10(:143); 开关入口 setValueRoundUp()(:494-496)、setValueRoundUpStep()(:502-509)。

1.2 手势三回调(mSliderChangeListener :166-236)

onStartTrackingTouch(:191-199)

mIsTrackingTouch = true;
mOriginProgress = fusionSlider.getValue();   // 记录起点,供松手判"无位移"

onValueChange(:202-235)——三层过滤,修复主战场

// ① 顶部同值过滤:非取整模式下,整数值没变直接 return(防重置 mOriginProgress)
if (mSliderValue == getIntSliderValue(value) && !mIsRoundUpWhenClick) { ... return; }  // :205-208
mOriginProgress = Float.MIN_VALUE;                                                     // :209
// ② 置灰保护:滑动中信号置灰后,fromUser 的回调不再受理(:210-220,含 9 行现象注释)
if (!isEnabled() && fromUser) { ... return; }
// ③ 取整分支(:221-231)
if (mIsRoundUpWhenClick) {
    if (mProgressChangeTimes > 0) { // 首次点击不下发,继续滑动/抬手都会下发
        onSliderProgressChanged(slider, getIntSliderValue(value), fromUser);
    }
    // 仅整数值真实变化才计入取整判定次数:
    // 按下 thumb 瞬间控件会回调一次同值的 onValueChange,
    // 若计入,「摸到 thumb 但未拖动」会被误判为点击而触发取整,
    // 拖动设定的值被静默改写到整十倍数(N801PROB-132401)
    if (mSliderValue != getIntSliderValue(value)) {   // :229
        mProgressChangeTimes++;                        // :230
    }
} else {
    onSliderProgressChanged(slider, getIntSliderValue(value), fromUser);
}

修复前(:229 无整数值判断)mProgressChangeTimes++ 无差别计数,按下 thumb 的同值回调 也算 1 次 → 「摸 thumb 未拖」累计成 == 1 → 被当点击取整。修复的全部内容之一 就是把计数条件收窄到「整数值真实变化」(:229-231 及 :225-228 四行注释)。 另一处在 §1.3 的判定门(修复前为 <= 1,0 次也取整)。

②的 9 行注释(:211-219)解释了一个隐蔽时序:preference.setEnabled(false) 与 preference.setValue(x) 都走 notifyChanged(),在重新 onBindViewHolder 之前 slider 未置灰,仍会产生 fromUser 回调;若受理,显示值会变成手停留位置而非信号值。

onStopTrackingTouch(:169-188)——执行顺序是修复的另一半

mIsTrackingTouch = false;
alignAdjustProgress(fusionSlider);        // :173 先取整吸附(内部:先回调刷新标签,后 setValue)
onSliderStopTrackingTouch(fusionSlider);  // :174 再抬手兜底下发
mOriginProgress = Float.MIN_VALUE;
mProgressChangeTimes = 0;                 // :175-176 复位,供下一次手势
// :181-186 埋点(仅 mTrackParamKey 非空时);:187 更新可见即可说 description

1.3 alignAdjustProgress(:331-346)——取整吸附本体

int originProgress = getIntSliderValue(slider);
// 仅发生一次真实位移(点击轨道)时才取整吸附;
// 0 次变化(触摸 thumb 但未产生位移)不取整——避免拖动设定后误触 thumb,
// 当前值被静默改写到整十倍数(N801PROB-132401)
if (mIsRoundUpWhenClick && mProgressChangeTimes == MAX_PROGRESS_CHANGE_TIMES) {   // :336
    originProgress = originProgress / mRoundUpStep * mRoundUpStep +
            (originProgress % mRoundUpStep >= (mRoundUpStep / 2 + mRoundUpStep % 2) ?
                    mRoundUpStep : 0);                                            // :337-339
    originProgress = Math.max(originProgress, mSeekBarData.mMin);                 // :340
    originProgress = Math.min(originProgress, mSeekBarData.mMax);                 // :341
    // Slider.setValue()方法会判断是否与当前一致,如果一致,不会回调onValueChange()方法,
    // 需要手动调用一次onSliderProgressChanged()刷新滑块刻度
    onSliderProgressChanged(slider, getIntSliderValue(originProgress), true);     // :343
}
slider.setValue(originProgress);                                                  // :345

三个要点:

  1. 判定门 :336:mProgressChangeTimes == MAX(1)。修复前为 <= 1, 「摸 thumb 0 次变化」也命中 → 取整。配合 §1.2③ 的计数收窄,双击点/摸 thumb 都不再误吸附。
  2. half-up 公式 :337-339:v/10*10 + (v%10 >= (10/2 + 10%2 = 5) ? 10 : 0)。 余数 ≥5 进位:67→70、45→50;余数 <5 舍去:44→40、33→30。mRoundUpStep%2 的写法 兼容奇数粒度(如 step=5 时阈值=3)。
  3. 先回调、后 setValue :343→:345——数值标签不滞留的关键时序。 FusionSlider「同值 setValue 不回调 onValueChange」(:342 注释;FusionSlider 源码不在本仓, 此行为仅此注释背书,见 00-能量回收-README 纪律 2)。 所以取整后必须手动调一次 onSliderProgressChanged 刷新刻度与标签; 且这次回调发生在 slider.setValue() 之前,下游必须读传入的 progress 而不是 mSlider 当前值——这就是 §2 MiCarIconSeekBarPreference 修复点存在的原因。

1.4 onSliderProgressChanged(:348-358)——跟手下发的入口

if (mShowMinValue > 0 && progress < mShowMinValue) {     // :349-352 下限钳位
    slider.setValue(mShowMinValue); return;
}
if (fromUser && (mIsContinuousUpdate || !mIsTrackingTouch)) {
    // 车控信号slider来回拖动的操作不下发信号, 避免信号上报后UI回弹
    syncValueInternal(slider, false);                     // :354-357
}

mIsContinuousUpdate 由基类默认开启(§4 :36),所以 SLIDER_NORMAL 下每次拖动 onValueChange → 这里 → syncValueInternal(slider, false) 逐值下发。

1.5 onSliderStopTrackingTouch(:368-382)——抬手兜底

if (!isEnabled()) { return; }                       // :369-372 滑动中被置灰,不下发
if (mOriginProgress == slider.getValue()) { return; } // :374-377 只点没滑,不下发
// 抬手时强制下发信号:1、处理抬手下发
// 2、处理跟手下发时,最后一次信号回弹时未松手,导致回弹失败
syncValueInternal(slider, true);                    // :381 force=true

1.6 syncValueInternal(:979-997)——下发的最后一米

if (isStepValidate(mStepValue)) {                    // :981-984 有级调节:对齐到 mStepValue 倍数
    sliderValue = getIntSliderValue(slider) / mStepValue * mStepValue;
} else {
    sliderValue = getIntSliderValue(slider);         // :985 无级:四舍五入取整
}
// 车控使用场景需要保证必须执行一次,可通过传入forceRequestCallChange来实现
if (sliderValue != mSliderValue || forceRequestCallChange) {   // :989
    if (callChangeListener(sliderValue)) {          // :990 → Preference 链 → controller 下发
        setValueInternal(sliderValue, false);       // :991 受理后写内部值
    } else {
        slider.setValue(mSliderValue);              // :993 拒收则 UI 回弹到旧值
    }
}

callChangeListener 一路走到 BaseVehicleProgressPrefController.handlePreferenceChanged (§4),那里才决定是否真的写车控属性。

1.7 值写回 UI 的拖动保护(setValue :903-913 / setValueInternal :915-928)

public void setValue(int sliderProgress, boolean forceUpdate) {
    // 滑动过程中信号上报不允许更新UI位置,否则会出现跳闪
    if (!isTrackingTouch() || forceUpdate) {
        setValueInternal(MathUtils.clamp(sliderProgress, getMin(), getMax()), true);
    } else { /* ignored (tracking touch) */ }
}

信号回显(§4 updateProgress → setValue)在拖动中默认被吞——手还在滑,UI 不跟信号跳; forceUpdate=true 只在「信号不可用需强制落值」时使用(:200 调用处)。 setValueInternal 里 persistInt(sliderProgress) 做 SharedPreferences 持久化。

1.8 置灰遮罩(:529 / :545-574)

mAllowTouchWhenDisable = true(默认)且 isEnabled() == false 时, onBindViewHolder 在 slider 父容器上加一个同尺寸遮罩 View(tag=mask_view, :529 调用、:545-574 实现)。遮罩接管点击并路由到 mClickListenerInDisable—— 三个 controller 都注册它弹「不可用原因」toast(§3)。这就是「置灰仍可点、点了弹提示」 而非静默吞掉的机制。

1.9 语音/可见即可说(mSliderVoiceAssistListener :238-321)

OPERATION_SET_PROGRESS(:247-265):云端给定 progress → getSliderValueFromProgress 映射 + mDescriptionOffset → 合法性检查 (:256-260,同值/越界直接 false)→ setValueInternal(targetValue, true) + callChangeListener(targetValue) 下发。调大/调小(:280-291/:267-278)按 getStepValue() 步进;调到最大/最小(:293-316)走 getValueTo()/getValueFrom()。 语音路径不经过取整分支(mIsRoundUpWhenClick 只在触摸回调链里), 云端侧的四舍五入约定靠 :83 注释里的步长语义保证。

2. MiCarIconSeekBarPreference:144 行的数值标签层(全量拆)

类声明 :42 extends SliderPreference,布局 micar_ui_pref_icon_seek_bar(:69-72)。

2.1 bindSlider(:85-98)

先走父类绑定,再校验 mSlider instanceof IFusionSlider(:91-94,失败打 error 并 return false), 然后 applySliderData(:100-110)把左右端文案与初始 thumbText 刷上去。 校验存在的意义:布局里的 view 必须是 IFusionSlider 一族,否则后面所有接口调用都会 ClassCastException。

2.2 onSliderProgressChanged 覆写(:112-120)——132401 修复点

@Override
protected void onSliderProgressChanged(IFusionSlider slider, int progress, boolean fromUser) {
    super.onSliderProgressChanged(slider, progress, fromUser);
    // 必须使用传入的 progress 更新数值标签:
    // 取整场景(SliderPreference#alignAdjustProgress)在 slider.setValue() 之前回调本方法,
    // 若改读 mSlider 当前值会拿到取整前的旧值,导致数值标签滞留旧值,
    // 与车辆实际属性值不一致(N801PROB-132401)
    updateThumbText(progress);   // :119
}

修复前这里调的是无参 updateThumbText()(:122-124),它读 getIntSliderValue(mSlider) ——而 §1.3 的回调发生在 setValue() 之前,mSlider 内部值还是取整前的旧值, 于是标签画了旧值、系统值已是新值,分裂到下一次触摸(05 篇半面 B)。 修复 = 改用传入的 progress(:119)+ 四行注释(:115-118)。

2.3 两个 updateThumbText 重载(:122-128)与 getThumbText(:134-139)

private void updateThumbText() { updateThumbText(getIntSliderValue(mSlider)); }  // :122-124 保留给"读当前值"场景
public void updateThumbText(int progress) {
    ((IFusionSlider) mSlider).setThumbText(getThumbText(progress));              // :126-128
}
private String getThumbText(int progress) {
    if (mThumbTextGetter != null) return mThumbTextGetter.getThumbText(progress);
    return String.valueOf(progress);                                             // :138 默认就是数字本身
}
  • 默认标签 = 进度值的十进制字符串(:138);要显示「45%」「低/中/高」等文案, 用 setThumbTextGetter(:130-132)注入 ThumbTextGetter 接口(:141-143)。
  • 能量回收三个 controller 都没注入 getter,标签即数值(与 bugreport 里「显示 67 实际 70」 的读数口径一致,所以标签值可以直接和信号值对账)。

3. 三个取整 controller:同配置三兄弟

三者的取整配置完全相同(setValueRoundUp(true) + SLIDER_NORMAL + 语音步长 10), 差异在挂载页面、propId 和最小值。共同父类见 §3.4。

3.1 BrakingStrengthPreferenceController(主页,Java,250 行)

  • @PropertyRange(lower=0, upper=100, defaultVal=100) :46-50; 常量区 :53-57(MIN=0、MIN_HYBRID=1、MAX=100、STEP_FOR_VOICE_ASSIST=10)。 混动车型通过动态代理把 lower 换成 1(:86-104 createModifiedConfigRange, 判据 CONFIGURATION_FOR_HMI_DISPLAY_HYBRID :93-94)。
  • onCreateInternal :106-156:
    • :111 setStepValueForVoiceAssist(10);:112 setValueRoundUp(true); :113 setWorkMode(SLIDER_NORMAL)(注释原话「滑动过程中下发信号」)
    • :114-135 注册 OnFusionSliderChangeListener 仅用于抬手埋点 (TIP_BRAKING_STRENGTH_SLIDER_TOUCH + 当前值)
    • :137-142 读 BRAKING_MODE 缓存定初始可见性(== Energy.RegenerationMode.ADVANCED); :144-150 再注册 ViewModel observer 持续跟随——双保险
    • :152-153 setAllowTouchWhenDisable(true) + 置灰点击弹 showDisableToast()
    • :154-156 setValue 初值 = ENERGY_REGENERATION_LEVEL 当前属性值
  • onPropertyManagerPrepared :199-206:属性服务就绪后再刷一次可见性 (BRAKING_MODE == ADVANCED,与 :137-142/:144-150 同判据)。
  • getPropertyIdSet :206-208 = ENERGY_REGENERATION_LEVEL(下发目标,唯一); getObservedPropertyIdSet :211-216 = 下发集 + ENERGY_REGENERATION_BAN_REASON (多观测一个禁用原因信号)。
  • onHandlePropertyChange :224-232:值变了先推 LiveData<CarPropertyValue>(喂雷达图, 见 04-信号链路与下游消费 §5.1)再走父类回显; onIgnorePropertyFeedback :235-242:回显被屏蔽时(自身 SET 的反馈)同样推雷达—— 雷达图两条刷新路径,保证自设值与外部值都能上图。
  • isDependencyAvailable :245-249 = EnergyRecoveryModeHelper.isEnergyRecoveryAvailable( BAN_REASON)(BAN_REASON==NO_INHIBIT 且信号状态可用才 true,见 04 §6.2)。
  • showDisableToast :158-191:按 BAN_REASON 枚举分别弹文案—— 战斗/湿滑/ADAS/除锈(RSTREMV)/陡坡缓降(HDC)/防晕车/拖挂,共 7 个 case + default 不弹。

3.2 CustomEnergyRecoveryStrengthController(M1/M2 自定义页,Kotlin,200 行)

  • @PropertyRange :32-36,数值同主页(lower=0/upper=100/defaultVal=100,常量 :46-49, 混动 lower=1 的代理 :160-192 同构)。
  • 取整配置 :57-59(:57 语音步长 10、:58 setValueRoundUp(true)、:59 SLIDER_NORMAL)。
  • propId 不是 ENERGY_REGENERATION_LEVEL,而是按当前自定义模式动态选: getPropertyIdsForM1 :88-92 = Driving.REGENERATION_PARAMETER_MODE1_STATUS, getPropertyIdsForM2 :94-98 = ..._MODE2_STATUS; 父类 getPropertyIdSet :178-185 按 mSteeringModeViewModel.isM1Mode()/isM2Mode() 分发, 非 M1/M2 返回空集(不下发)。
  • 可见性门控与主页相同(缓存 :61-67 + observer :69-77 + prepared :154-158, 全部 BRAKING_MODE == ADVANCED);观测集 :100-105 同样追加 BAN_REASON。
  • getInitProgress :142-144 从 SteeringModeViewModel 读当前模式的回收强度做初值; updateProgressSteeringModeData :146-148 把进度回写模式数据(账号同步用)。
  • 置灰 toast 走公共实现 EnergyRecoveryModeHelper.showDisabledReasonToast(:194-199)。

3.3 MainCustomEnergyRecoveryStrengthController(主驾自定义页,Kotlin,151 行)

  • ⚠️ @PropertyRange lower=1(:26,常量 :41 BRAKING_STRENGTH_MIN_VALUE = 1), 不是 0——主驾自定义页的可调区间是 1~100(upper=100/defaultVal=100 与兄弟一致)。 与 README「关键数字一页纸」的「主自定义页 1~100」口径吻合。
  • 取整配置 :60-61(:60 setValueRoundUp(true)、:61 SLIDER_NORMAL);语音步长 :43/:59。
  • 可见性用 getAvailabilityStatus :48-55(CONDITIONALLY_UNAVAILABLE)+ observer :63-74
    • prepared :139-143,判据同为 BRAKING_MODE == ADVANCED。
  • propId 按驾驶模式四选一:getPropertyIdForEco/Comfort/Sport/SportPlus :85-95 = REGENERATION_PARAMETER_{ECONOMIC,COMFORT,SPORT,SPORT_PLUS}_MODE_STATUS。
  • 观测集 :97-102 追加 BAN_REASON;toast 同走 EnergyRecoveryModeHelper(:145-150)。

3.4 两个自定义父类

BaseCustomVehicleProgressPrefController.java(Custom 的父类,271 行):

  • :29 extends BaseVehicleProgressPrefController<MiCarIconSeekBarPreference>—— Preference 类型在此锁定为 MiCarIconSeekBarPreference,所以修复(:119)天然覆盖 Custom。
  • :46 setWorkMode(SLIDER_NORMAL)(父类统一再设一次,与子类重复但无害)。
  • updateVehicleProperty :134-142:仅 M1/M2 模式才 setVehicleProperty (propId 来自 :178-185 的动态集),无论下不下发都把值缓存进 SteeringModeViewModel(:140)。
  • onIgnorePropertyFeedback :144-148 / onHandlePropertyChange :150-154 → notifyRadarViewRefresh :159-164:M1/M2 propId 变化时推 LiveData 刷雷达(同 §3.1 双路径)。

BaseMainCustomVehicleProgressPrefController.kt(MainCustom 的父类,97 行):

  • :21-26 同样 extends BaseVehicleProgressPrefController<MiCarIconSeekBarPreference>; :34 SLIDER_NORMAL。
  • :45-53 initProgressFromCache:onCreate 时用 VehiclePermanentListener 缓存值先画 UI。
  • :55-61 getPropertyIdSet 走 DriveModePropertyProvider.getDriveModePropertyIdSet() (四模式分发,空集打 error);:63-69 updateVehicleProperty 空集直接 false。
  • :84-91 notifyRadarViewRefresh:四模式 propId 命中才推雷达。

4. BaseVehicleProgressPrefController:值映射与下发/回显中枢(258 行)

4.1 恒等映射(:142-158)

protected int propValToProgress(int propertyValToIndex) {   // :142-151
    ... return (propertyValToIndex + mCfgPropertyRange.offset());
}
protected int progressToPropVal(int progress) {             // :153-158
    ... return progress - mCfgPropertyRange.offset();
}

offset() 注解默认 0(base/settingsVehicleLib/.../annotation/PropertyRange.java:36), 三个 controller 都没覆写 offset ⇒ 属性值 ≡ 滑块进度,0~100 恒等。 排查「100-x 反转」类猜想时这就是证伪依据(05 篇 §3.4 用它驳了 JarviX 的反转假设)。 滑块区间在 updateSliderRange :67-100 由 Range(lower+offset, upper+offset)(:79)设定, init && defaultVal 合法 时 :93-95 用默认值先画。

4.2 默认工作模式的反转(:33-44)

getPreference().setContinuousUpdate(true);                        // :36
...
// 在slider拖动过程中不会直接下发车控信号
getPreference().setWorkMode(SliderPreference.SLIDER_ADAPTER_VEHICLE); // :43

基类默认 = 抬手才下发(:43),三个能量回收 controller 各自在 onCreateInternal 里 改回 SLIDER_NORMAL(§3)。改任何 slider 需求时先确认这个默认值,别把兄弟控件的行为带歪。

4.3 下发闸门(handlePreferenceChanged :217-231 / updateVehicleProperty :233-238)

// E3 slider车控逻辑调整为拖拽过程中不下发信号,手指抬起时发送一次, 避免拖动过程中信号上报UI回弹
if (getPreference().getWorkMode() == SliderPreference.SLIDER_NORMAL
        || !getPreference().isTrackingTouch()) {          // :223-224
    if (updateVehicleProperty((Integer) progress)) { return true; }
}

SLIDER_NORMAL ⇒ 任何时刻都进 updateVehicleProperty; SLIDER_ADAPTER_VEHICLE ⇒ 只有非拖动中(抬手那一次)才进。 updateVehicleProperty :233-238:progressToPropVal 换算后 setVehicleProperty(propId, propVal, areaId, true)(:237,同步下发, 保证顺序;同步下发的端到端时延实证见 04 §3)。取 getPropertyIdSet().iterator().next() 第一个 propId——三个 controller 的集合都是单元素,安全。

4.4 回显路径(onHandlePropertyChange :160-168 / updateProgress :179-201)

外部(非自身 SET)的属性变化:

  • :183 setEnabled(isPropAvailable)——isPropertyAvailable && isDependencyAvailable && isInnerLogicAvailable 三者与;BAN_REASON 置灰就从这里生效(BAN_REASON 在观测集里, 变化会触发本路径,见 04 §6.2)。
  • :187-191 非法值打 warning 丢弃;:192 propValToProgress 换算;
  • :196-199 有级调节(getStepSize()>0)时对齐到步长节点;
  • :200 setValue(seekProgress, !isPropAvailable) 写 UI——拖动中被 §1.7 的保护吞掉。

5. 组件库分层:为什么「升级组件库修不好」

本功能的代码边界:

层位置内容
接口层(Settings 仓)IFusionSlider.kt:10getValue/setValue/setThumbText/... 接口与 OnFusionSliderChangeListener(:72);setThumbText 默认空实现(:52)
薄壳(Settings 仓)MiCarIconSeekBar.kt:14class MiCarIconSeekBar : IconSeekBar, IFusionSlider,仅增强 touch 事件(:8 import micarx.androidbasewidget.IconSeekBar)
总控(Settings 仓)SliderPreference.java(本篇 §1)手势判定、取整、计数、下发、置灰遮罩、语音
渲染实现(micarx 组件库)com.mi.car.material.slider.Slider(SliderPreference.java:59 import)、micarx.androidbasewidget.IconSeekBar绘制、触摸事件产生、thumb 文本渲染

依赖声明(rls 行号):base/settingsBaseUi/build.gradle:90 def micarx_lib_version = "3.2.6-RC11-SNAPSHOT",:106 api "micarx.support:androidbasewidget:$micarx_lib_version"。

结论:取整判定、标签刷新、信号下发全部在 Settings 仓 settingsBaseUi; 组件库只产出触摸事件流(onStartTracking/onValueChange/onStopTracking)和渲染。 132401 这类「取整误触发/标签滞留」bug,改组件库版本天然修不到——逻辑不在那一层 (「3.2.4 称已修 → 3.2.6 仍在」的完整复盘见 05-排障实战-N801PROB-132401 §5)。 当前基线组件库已是 3.2.6-RC11-SNAPSHOT,bug 修复靠的是本仓 70f7b9afd(即 cc27d08ba 内容)。

6. 同族控件:谁受影响、谁不受影响

控件继承onSliderProgressChanged开取整?结论
能量回收三 controller 用的 MiCarIconSeekBarPreferenceSliderPreference(:42):112-120 已修(读传入 progress)是(:112/:58/:60)✅ 修复覆盖
RoofSunShadeSliderPreference.kt(天窗遮阳帘,micarLockSettings/…/preferences/)直接 extends SliderPreference(:25):152-155 仍读 getSliderValue() 当前值(同款旧写法)否(micarLockSettings 内无 setValueRoundUp 调用点,grep 实证)⚠️ 潜在债:写法未修,但因未开取整,alignAdjustProgress 的提前回调路径不会触发,暂时安全;谁给它开 setValueRoundUp(true) 就会复现 132401 半面 B
MiCarHeightLimitSeekBarPreference.kt(限高,settingsAutoPilot/…/preference/)extends MiCarIconSeekBarPreference(:34)继承修复版—✅ 继承即修复

排查「还有没有同类隐患」的标准动作: grep -rn "onSliderProgressChanged" --include="*.kt" --include="*.java" | grep getSliderValue\|getIntSliderValue(mSlider) 找出所有覆写后仍读控件当前值的 Preference,再核对其 controller 是否开了 setValueRoundUp。

7. 改代码 checklist

需求改哪里
改取整粒度(如 10→5)controller 加 setValueRoundUpStep(5)(:502-509);注意 :337-339 公式已兼容奇数粒度
关/开取整controller 的 setValueRoundUp(true/false)(三处:Braking :112、Custom :58、MainCustom :60)
改语音步长controller 的 STEP_FOR_VOICE_ASSIST 常量(Braking :57、Custom :49、MainCustom :43)
改可见性门控BRAKING_MODE == ADVANCED 三处判据(缓存读 + observer + onPropertyManagerPrepared),三个 controller 共 9 处,必须同步改
改置灰文案Braking showDisableToast :158-191 自己的文案表;Custom/MainCustom 在 EnergyRecoveryModeHelper.showDisabledReasonToast
加新 ban 原因Driving$EnergyRegenerationBanReason 枚举(jar 侧)+ 两个 toast 实现 + isEnergyRecoveryAvailable 语义
改数值标签文案setThumbTextGetter(MiCarIconSeekBarPreference :130-132,接口 :141-143)
改下发 propId主页改 getPropertyIdSet :206-208;Custom 改 :88-98;MainCustom 改 :85-95(注意 M1/M2/四模式分发父类)
看日志adb shell setprop log.tag.SliderPreference D(rls 上关键路径多为 MLog.d;>>> Done set propVal 在 VehicleControlManager.java:121,I 级默认可见)

⚠️ rls 分支 SliderPreference.java / MiCarIconSeekBarPreference.java / RoofSunShadeSliderPreference.kt 的 import 是 com.android.car.setings.carui.R (setings 少一个 t)——这不是笔误:rls 分支 base/settingsBaseUi/src/main/AndroidManifest.xml:3 声明的模块包名就是 package="com.android.car.setings.carui",import 与之自洽,不要「顺手修正」。 dev 分支则相反:manifest 无 package 属性,base/settingsBaseUi/build.gradle:8 namespace 'com.android.car.settings.carui'(拼写正确),两分支各自自洽, cherry-pick 跨分支改动时注意别把两边的 R 引用混了。

下一篇

04-信号链路与下游消费:值离开 App 之后去哪了—— propId 三件套、CARSIGNAL/pps 逐段实证、回显屏蔽、雷达图与 MIoT 上报、三视角对账排查法。