一个设置页面,每一行是“图标 + 标题 + 开关”,行高写死 56。默认字体下排版干净,开关和文字都居中。用户把系统字体调大两号,标题变成两行,第二行被裁掉一半,开关被挤出了右边界。你再调大三号,标题直接显示成一行半截,下半截被下一行盖住。你打开代码检查,fontSize('16fp') 写了,maxLines(1) 没加,文字怎么还能被裁?

问题不在字号单位有没有用 fp,也不在 maxLines 少写了一行。行高是 56,单位是 vp,vp 不随系统字体缩放。fp 的字号放大了,容器的高度没放大,多出来的文字没有地方放,它只能溢出或者被裁。字号跟随系统,容器尺寸不跟随系统,两者用的是两套缩放基准。你让文字长大了,却没给容器长大后的空间,裁切是必然的。

fp 跟随字体缩放,vp 只跟随屏幕密度

先把这两个单位的语义钉死。vp 是 virtual pixel,它只随屏幕像素密度换算,不随系统字体缩放变化。fp 是 font pixel,它在 vp 的基础上再乘一次系统字体缩放比例。用户把字体调大 1.3 倍,16fp 渲染出来相当于 16 × 1.3 = 20.8vp 的高度,而 height(56) 仍然是 56vp,它不知道字号变大了。

这意味着“用 fp 写字号”是必要条件,但不是充分条件。字号会变大,容器不会,行高不会,间距不会。你如果只把字号从 vp 改成 fp,默认字体下视觉上没有变化,用户调大字体之后,文字变大、容器不变,裁切和溢出的问题才开始暴露。

typescript

// 行高写死 vp,字号用 fp:字体放大后文字溢出容器
Row() {
  Image($r('app.media.ic_setting')).width(24).height(24)
  Text('通知设置')
    .fontSize('16fp')          // 跟随系统字体缩放
    .maxLines(1)
  Blank()
  Toggle({ type: ToggleType.Switch })
}
.height(56)                    // 固定 vp,不跟随字体缩放

height(56) 这一行就是裁切的根源。16fp 在字体缩放 1.5 倍时约等于 24vp 的行高,加上上下内边距,56vp 的容器装得下一行;缩放 2.0 倍时字号接近 32vp,56vp 的容器已经装不下一行完整的文字加上图标和开关的垂直居中。文字被裁,不是 maxLines 的问题,是容器没有给出足够的空间。

容器高度从固定值改成最小高度,或者交给内容撑开

修正的第一步,是让容器的高度跟着内容走,而不是反过来让内容去适应一个固定高度。constraintSize({ minHeight: 56 }) 给了一个下限,保证默认字体下行高不会太矮;内容变大时,容器的高度由内容撑开,不再是一个死值。

typescript

Row() {
  Image($r('app.media.ic_setting')).width(24).height(24)
  Text('通知设置')
    .fontSize('16fp')
    .layoutWeight(1)           // 文字占据剩余宽度,不挤压开关
  Toggle({ type: ToggleType.Switch })
}
.constraintSize({ minHeight: 56 })   // 最小高度保底,内容可撑开
.padding({ top: 8, bottom: 8 })

constraintSize 的 minHeight 只在内容高度小于 56 时生效,内容更高时容器跟着长高。layoutWeight(1) 让文字占据图标和开关之间的剩余空间,文字变长或者字体变大时,它优先使用可用宽度,而不是把开关挤出去。padding 给了上下各 8 的呼吸空间,字体放大后行高增加,内边距不会跟着变,但容器高度已经由内容决定了。

如果业务确实需要一个“固定高度”的视觉规格,比如列表分割线必须等距,那就不能用 height 硬性裁切,而应该用 minHeight 加 maxLines 加 textOverflow 的组合,让文字在超出时以省略号收尾,而不是被裁掉半截。maxLines(1) 加 textOverflow({ overflow: TextOverflow.Ellipsis }) 会在单行放不下时显示省略号,用户至少知道内容被截断了,而不是看到一行被切掉一半的字。

多语言叠加字体缩放,固定宽度是最先撑不住的地方

字号放大是纵向的压力,多语言是横向的压力。德语和俄语的单词长度普遍比中文和英文长,同一个设置项,中文“通知设置”四个字,德语可能是“Benachrichtigungseinstellungen”,一行放不下。如果这个文字所在的容器宽度是固定的,比如 width(120),字体放大加上长单词,文字要么换行撑高,要么被裁。

横向的修正思路和纵向一致:宽度不要写死,用 layoutWeight 或者 flexShrink 让文字占用它需要的空间,把固定宽度的压力转移到可以伸缩的容器上。图标和开关这类尺寸固定的元素,给它们明确的宽度;文字所在的区域,给它弹性。

typescript

Row() {
  Image($r('app.media.ic_setting')).width(24).height(24)
  Text($r('app.string.notification_settings'))
    .fontSize('16fp')
    .layoutWeight(1)                        // 文字占据弹性宽度
    .maxLines(2)                            // 允许换行,最多两行
    .textOverflow({ overflow: TextOverflow.Ellipsis })
  Toggle({ type: ToggleType.Switch })
}
.constraintSize({ minHeight: 56 })

maxLines(2) 给了长文本换行的空间,Ellipsis 在超过两行时收尾。layoutWeight(1) 保证文字优先使用可用宽度,开关不会被挤出去。这套组合在字体放大和多语言同时发生时,容器会变高,文字会换行或者省略,但不会有内容被裁掉半截。

系统字体缩放比例可以读,但不要用它去反向计算固定高度

configuration.fontSizeScale 能读到当前的系统字体缩放比例,有些开发者会用它去动态计算容器高度,比如 56 * fontSizeScale。这个思路在简单场景下能跑,但它把“容器该多高”这件事交给了“字号缩了多少”,而不是交给“内容实际需要多高”。文字换行数、多语言长度、图标和文字的垂直对齐,这些因素叠加之后,56 * fontSizeScale 算出来的高度可能仍然不够或者过高。

更稳的做法是让布局引擎自己算。constraintSize({ minHeight: 56 }) 加上弹性宽度和 maxLines,容器的高度由内容决定,你不需要知道 fontSizeScale 是多少,也不需要为每个缩放比例写一套适配逻辑。fontSizeScale 可以用来做“缩放超过 1.5 倍时切换成更紧凑的布局”这类粗粒度判断,但它不适合用来做精确的像素计算。

typescript

// 粗粒度适配:字体缩放很大时切换布局模式
@StorageProp('fontSizeScale') fontSizeScale: number = 1.0;

build() {
  if (this.fontSizeScale > 1.5) {
    // 大字体模式:图标在上、文字在下,纵向排列
    Column() { /* ... */ }
  } else {
    Row() { /* ... */ }
  }
}

fontSizeScale 需要从 configuration 更新事件里同步到 AppStorage,它不会自动变成响应式状态。这个判断的粒度是“布局模式切换”,不是“每个容器高度精确计算”。

固定高度的图标和按钮,在字体放大后同样会错位

文字之外的固定尺寸元素,也会在字体放大后出现问题。一个 Button('确定').height(40).fontSize('16fp'),字体放大到 2 倍时,文字高度接近 32vp,40vp 的按钮高度加上上下内边距,文字可能刚好贴边或者被裁。Button 的默认内边距是固定的,它不会跟着字体缩放变化。

同样的修正逻辑:按钮的高度用 constraintSize({ minHeight: 40 }) 而不是 height(40),内边距用 padding 给足,文字用 maxLines(1) 加 Ellipsis 兜底。图标按钮如果只有图标没有文字,width/height 固定是安全的,因为图标不随字体缩放;但如果图标旁边有文字,整个组合的容器高度就要跟着文字走。

排查顺序:先看单位,再看容器,最后看多语言和兜底

下次“字体调大后文字被裁”的时候,按这个顺序走,比逐个组件改字号快。

第一步,确认字号单位是 fp 而不是 vp。用 vp 写字号,系统字体缩放对它完全无效,用户调大字体,你的文字纹丝不动——这本身也是一个适配问题,只是表现是“字没变大”,而不是“字被裁了”。

第二步,检查文字所在容器的 height 是不是固定值。固定 height 不跟随字体缩放,字号变大后内容溢出,裁切和重叠都从这里来。改成 constraintSize({ minHeight }),让内容撑开容器。

第三步,检查横向宽度是不是固定值,文字区域有没有 layoutWeight 或者 flexShrink。多语言的长单词加上字体放大,固定宽度是最先撑不住的地方。

第四步,给文字加 maxLines 和 textOverflow 兜底。允许换行的场景用 maxLines(2),不允许换行的场景用 maxLines(1) 加 Ellipsis,不要让文字在没有兜底的情况下溢出容器。

第五步,图标按钮和文字按钮的高度同样从固定值改成最小高度,内边距给足,避免字体放大后文字贴边。

fp 跟随系统字体缩放,vp 只跟随屏幕密度,两者用的是两套基准。字号长大了,容器没长大,裁切就是必然结果。constraintSize({ minHeight }) 让容器跟着内容走,layoutWeight 让文字优先使用横向空间,maxLines 加 Ellipsis 给溢出兜底,多语言的长文本在这些规则下才有地方放。下一次用户把字体调大两号发现页面裁字的时候,先别怀疑 maxLines 少写了一行——打开代码看一眼,那个 height(56),到底有没有给字体缩放留过余地。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐