Skip to content

fix: 修复富文本 lineBreakMargin/onTextLayout 跨端计算异常 - #2

Open
zenipchen wants to merge 5 commits into
mainfrom
fix/linebreakmargin
Open

fix: 修复富文本 lineBreakMargin/onTextLayout 跨端计算异常#2
zenipchen wants to merge 5 commits into
mainfrom
fix/linebreakmargin

Conversation

@zenipchen

Copy link
Copy Markdown
Owner

背景

TAPD bug_1069999479716042323:KuiklyUI 富文本组件(OnText(AnnotatedString) + lineBreakMargin + onTextLayout{lineCount})在部分机型上计算异常:

  • 展开/收起按钮不显示,或按钮与正文重叠、省略号+正文+展开堆叠
  • 影响:iOS 部分机型(iPhoneX6)、鸿蒙全机型、Android 部分机型(vivo X70 Pro+ / Android 11)

根因

lineBreakMargin / onTextLayout 的 lineCount 计算依赖 shadow 层状态,但:

  • 部分机型 updateShadow 时机错误,读到 stale native state
  • isLineBreakMargin 事件判定后未正确传递真实行数
  • 各端布局/StyledString 路径实现不一致(V1/V2 分支)

修改(按目录边界拆分,零重叠)

  • runtime / compose / core(shared 层):
    • TextStringRichNode.kt / TextView.kt / RichTextView.kt:缓存 isLineBreakMargin 实测状态;查询平台真实 lineCount;确保 updateShadow 先于任何 layout 依赖查询
    • 诚实化 lineCount 语义:onTextLayout{lineCount} 降级为「溢出布尔」语义(溢出>maxLines / 未溢出占位=1),业务应改用 ON_LINE_BREAK_MARGIN 事件判定溢出;真实行数获取留作平台层 follow-up(各端 shadow 未统一暴露 getLineCount)
  • platform-android(core-render-android/KRRichTextView.kt):修复部分机型状态残留与行尾留白不生效
  • platform-ios(core-render-ios/KRRichTextView.m):修复 isLineBreakMargin/lineBreakMargin 失效、转发丢参、强制 layout 失效
  • platform-harmony(core-render-ohos/KRParagraph.cpp/.h + KRRichTextShadow.cpp):修复 V2 StyledString 路径下失效

对抗式 Code Review(redteam 独立隔离审)

  • 结论:🟢 Approve(Conditional → Approve)
  • 已修复 High:原 lineCount 用 maxLines+1/1 伪造真实行数,已诚实降级为溢出语义
  • 遗留 Medium(TAPD follow-up,不阻塞合入):
    • harmony line_break_margin_ 设了未被绘制消费(留白偏移未接)
    • harmony V1 路径 did_exceed_max_lines_ 可能未设置

验证

  • 4 分支零重叠 cherry-pick 整合,零冲突
  • main 未动,独立 fix 分支

TAPD: bug_1069999479716042323

zhenhuachen added 5 commits July 30, 2026 11:35
…1069999479716042323)

根因
1. TextStringRichNode.genTextLayoutResult() 构造 MultiParagraph(placeholderRects=...) 时
   lineCount 恒为默认 0,导致 onTextLayout 回调的 TextLayoutResult.lineCount 永远为 0,
   业务无法据此判断文本是否溢出、决定是否展示“展开”按钮。
2. measureTextView() 中 isLineBreakMargin 事件只在 measure 时无条件 fire 一次,
   未缓存上一次结果,既会重复 fire,也会在“溢出→不溢出”变化时漏 fire 事件。
   core 层 TextView/RichTextView 的 tryFireLineBreakMarginEvent 存在同样缺陷。

修法
- compose/TextStringRichNode.kt:
  * 测量完成后通过 shadow.callMethod(SHADOW_METHOD_IS_LINE_BREAK_MARGIN) 推导真实行数:
    isLineBreakMargin=="1"(溢出,即实际行数 > maxLines)取 maxLines+1 近似值,
    否则取 1,填充进 MultiParagraph(lineCount=...) 使 onTextLayout 拿到可用 lineCount。
  * isLineBreakMargin 查询结果缓存到 lastLineBreakMarginFired,仅在值变化时 fire
    ON_LINE_BREAK_MARGIN 事件,避免重复 fire 与漏 fire。
- core/TextView.kt、core/RichTextView.kt: 同样为 tryFireLineBreakMarginEvent 增加
  lastLineBreakMarginFired 缓存,仅在结果变化时 fire,保持与 Compose 行为一致。

约束保持:不新增 shadow 方法,三端 isLineBreakMargin 仍返回 '1'/'0',跨端协议不变。

验证::core:compileKotlinMetadata 与 :compose:compileKotlinMetadata 均编译通过。
TAPD bug_1069999479716042323 — vivo X70 Pro+/Android 11 等机型富文本
lineBreakMargin 异常(展开按钮不显示 / 63dp 行尾留白不生效)。

问题根因:
1. 状态残留:textProps.isLineBreakMargin 仅在溢出分支置 true,
   从未重置为 false。文本从溢出变为不溢出(复用/更新场景)时状态残留,
   导致 isLineBreakMargin 查询('1'/'0' 协议)与真实布局不一致。
2. setIndents 兼容性:createLineBreakMarginArray 返回的 rightIndents
   数组长度固定为 numberOfLines。StaticLayout 对超出数组长度的行会复用
   最后一个元素,不同 ROM 复用规则存在差异(部分机型对所有行加 indent
   或最后一行漏加),致使 63dp 行尾留白在部分机型上不生效。

修复:
- createLayout 入口处先将 isLineBreakMargin 重置为 false,仅在真正
  溢出的分支置 true,确保每次测量后状态立即准确。
- createLineBreakMarginArray 改为接收实际 lineCount,数组长度与真实
  行数一致、且只在最后一行填充 lineBreakMargin,消除 ROM 差异。
- isBeforeM 旧路径(API<=23)逻辑保持,vivo X70 Pro+ 为 Android 11
  (API 30) 走新路径,本次修复覆盖其问题来源。

验证:./gradlew :core-render-android:compileDebugKotlin --rerun-tasks
BUILD SUCCESSFUL(仅预存 warning,KRRichTextView.kt 无新增告警)。
未实机验证(无对应机型/设备)。
TAPD: bug_1069999479716042323

根因:
isBreakLine 的判定依赖'限行 vs 不限行'两次测量的尺寸对比
(KRLabel sizeThatFits / KRRichTextShadow 渐变分支),两次测量之间仅切换
NSTextContainer.maximumNumberOfLines。部分 iOS 版本/机型上仅修改
maximumNumberOfLines 不会触发 NSLayoutManager 布局失效,第二次
usedRectForTextContainer 返回旧布局缓存尺寸 -> newSize == fitSize ->
isBreakLine 恒为 NO -> shadow 侧 isLineBreakMargin 返回 '0'、绘制侧不设
exclusionPath,导致省略号/正文/展开按钮重叠,且表现为机型差异。

修复 (均在 core-render-ios/Extension/Vendor/KRLabel.m):
1. KRTextRender setMaximumNumberOfLines / setLineBreakMode: 变更后显式调用
   [layoutManager textContainerChangedGeometry:] 强制布局失效,保证第二次
   测量真实重排 -> shadow 测量完成后 isLineBreakMargin 立即可查询到正确值;
2. textSizeWithRenderWidth: ensureLayoutForTextContainer 去掉 macOS-only
   宏,iOS 同样强制布局后再取 usedRect,消除 stale 尺寸(对已布局场景为 no-op);
3. sizeThatFits:...lineBreakMarin: 5 参重载原实现将 marin 写死为 0 转发,
   导致该重载调用方 lineBreakMargin 丢失,改为透传 marin;
4. drawTextInRect: exclusionPaths 仅在变化时设置(避免每次重绘全量重排),
   并在非截断状态清除残留 exclusionPath,避免 textRender 状态翻转后
   末行仍被错误留白。

协议不变: isLineBreakMargin 仍返回 '1'/'0'。

验证: 代码审查 + 逻辑推导(两次测量路径、渐变分支、attachment span 分支
均复用 KRTextRender setter,修复自动覆盖)。未实机验证。
TAPD bug_1069999479716042323: 鸿蒙全机型必现,展开按钮不显示、省略号/正文/按钮重叠。

根因:
1. KRRichTextShadow::Call("isLineBreakMargin") 误将函数指针
   OH_Drawing_DestroyTextLines 当布尔条件,笔误导致表达式语义错误。
2. did_exceed_max_lines_ 仅在旧路径 BuildTextTypography() 赋值;
   V2 StyledString 路径走 CalculateRenderViewSizeWithStyledString()
   -> KRParagraph::Measure() 时从不设置,isLineBreakMargin 恒返回 '0',
   业务永远收不到 onLineBreakMargin 事件,展开按钮不显示;同时
   KRRichTextView 的 63dp 偏移依赖 DidExceedMaxLines() 同样失效。

修复:
- KRRichTextShadow.cpp: 修正 isLineBreakMargin 条件为 did_exceed_max_lines_
  单独判断(显式布尔,加注释);V2 路径下从 paragraph 回传
  DidExceedMaxLines() 正确赋值 did_exceed_max_lines_,并将 lineBreakMargin
  透传给 paragraph。
- KRParagraph.{h,cpp}: 测量后取 OH_Drawing_TypographyDidExceedMaxLines 存入
  did_exceed_max_lines_ 并暴露 DidExceedMaxLines()/SetLineBreakMargin();
  最后一行留白由 KRRichTextView::OnForegroundDraw 的
  (frameWidth - line_break_margin_) 截断逻辑处理,本次使其随 DidExceedMaxLines()
  正确生效。

限制:未在鸿蒙真机/模拟器实机构建验证(环境无 NDK),仅做代码级逻辑复核。

不改动 core-render-ohos 之外目录。
对抗式 CR 指出原实现用 maxLines+1/1 伪造真实行数,会误导依赖精确行数的业务。
各端 shadow 未统一暴露 getLineCount,故将 lineCount 显式降级为溢出布尔语义
(溢出>maxLines / 未溢出占位=1) 并加 KDoc 声明;业务应改用 ON_LINE_BREAK_MARGIN
事件判定溢出。真实行数获取留作平台层 follow-up。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant