鸿蒙 HarmonyOS 6 | UI 单位转换迁移详解
很多老项目升到鸿蒙 6 之后,最先看到的一批告警,就是单位转换相关的全局接口不再推荐继续使用。vp2px、px2vp、fp2px、px2fp、lpx2px、px2lpx 这些写法,过去很顺手,现在继续写也不是立刻不能跑,但已经不适合继续当主路径。这件事表面上看是接口替换,实际上牵涉的是另一层问题。以前很多代码默认单位转换和当前界面天然绑定,所以直接全局调用就行。到了 Stage 模型、多窗口、异步
前言
很多老项目升到鸿蒙 6 之后,最先看到的一批告警,就是单位转换相关的全局接口不再推荐继续使用。vp2px、px2vp、fp2px、px2fp、lpx2px、px2lpx 这些写法,过去很顺手,现在继续写也不是立刻不能跑,但已经不适合继续当主路径。
这件事表面上看是接口替换,实际上牵涉的是另一层问题。以前很多代码默认单位转换和当前界面天然绑定,所以直接全局调用就行。
到了 Stage 模型、多窗口、异步回调这些场景里,这个前提已经不稳了。真正要改的,不只是函数名,而是单位转换这件事到底跟谁绑定。

一、这轮迁移的核心是把单位转换绑回当前 UI 实例
全局单位转换接口的问题,不在换算能力本身,而在上下文归属不清。你在一个普通页面里直接调,看上去没什么问题。一旦到了异步逻辑、复杂页面、多个 UI 实例并存的场景,系统未必能准确判断这次换算到底应该使用哪一个界面的参数。
这也是为什么现在更推荐走 UIContext。因为 UIContext 对应的是具体 UI 实例的运行时环境,单位转换放到这里之后,谁来换、按什么参数换,关系就明确了。代码虽然比过去多了一步,但结果更稳,尤其是在多窗口和异步场景里。
所以这轮迁移不要理解成旧接口过时了,换个新接口继续写就行。更准确的理解是,单位转换从一个看起来谁都能调的全局工具,收回到了具体 UI 实例自己的上下文里。这个逻辑想清楚了,后面的改造就不会乱。
二、组件里怎么拿Ability 里怎么拿先分清楚
如果代码就在自定义组件内部,最直接的写法就是 this.getUIContext()。这种情况最简单,也最适合优先改。页面按钮点击、局部布局计算、组件内部状态联动,这些地方本来就和当前 UI 实例绑得很紧,直接就近拿上下文就行。
@Entry
@Component
struct ProfileCard {
build() {
Column() {
Text('个人资料')
.fontSize(20)
Button('计算宽度')
.onClick(() => {
const uiContext = this.getUIContext();
const widthPx = uiContext.vp2px(120);
console.info(`120vp -> ${widthPx}px`);
})
}
.padding(16)
}
}
这种写法的好处不是形式新,而是归属清楚。当前组件拿当前组件所在的 UI 上下文,不需要猜,也不需要兜底。
如果代码在 UIAbility 这一层,或者在窗口初始化流程里,那就不要硬去找组件的 this。这时候更合适的入口是窗口。等 windowStage.loadContent(...) 完成后,再从主窗口拿 UIContext。这样更符合 Stage 模型的实际结构。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private appUIContext?: UIContext;
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err && err.code) {
console.error(`loadContent failed: ${JSON.stringify(err)}`);
return;
}
const mainWindow = windowStage.getMainWindowSync();
this.appUIContext = mainWindow.getUIContext();
const pxValue = this.appUIContext.vp2px(24);
console.info(`24vp -> ${pxValue}px`);
});
}
}
这里最重要的是时机。界面还没真正挂到窗口上时,就不要急着拿。拿早了,后面的问题不一定立刻暴露,但会在一些边界场景里慢慢冒出来。
三、最容易翻车的地方是异步回调和工具函数
页面里的全局 vp2px 其实不难改,搜一下基本都能找到。真正麻烦的,是那些异步回调、Promise 结束后的处理逻辑、还有一些项目里共用多年的工具函数。这些地方最容易留下半新半旧的写法,看上去已经迁移了,实际上还是在偷偷依赖全局思路。
最稳的办法不是继续做一个伪全局适配层,然后内部再想办法兜底。而是把 UIContext 显式传进去。这样虽然多了一个参数,但依赖关系是明的,代码也更容易维护。
export function vp2pxBy(uiContext: UIContext, value: number): number {
return uiContext.vp2px(value);
}
export function px2vpBy(uiContext: UIContext, value: number): number {
return uiContext.px2vp(value);
}
组件里用的时候也别偷懒,真正执行换算的那个点,再拿当前可用的 UIContext。
@Entry
@Component
struct AsyncLayoutDemo {
@State widthPx: number = 0;
async refreshLayout() {
await new Promise<void>((resolve) => setTimeout(() => resolve(), 100));
const uiContext = this.getUIContext();
this.widthPx = vp2pxBy(uiContext, 100);
}
build() {
Column() {
Text(`当前宽度: ${this.widthPx}`)
Button('刷新布局').onClick(() => this.refreshLayout())
}
}
}
这类地方最忌讳的是想当然。不要觉得既然是同一个页面发起的异步任务,后面随便怎么算都还是它。工程里最容易出问题的,恰恰就是这些默认不出问题的地方。
四、迁移要先统一规则,再做缓存
项目一大,这类迁移最怕的不是改不完,而是改着改着出现三四种写法并存。有人在组件里直接 this.getUIContext(),有人从窗口层往下传,有人又自己包了一个适配器,最后表面上都能跑,后面维护的人根本不知道该按哪套来。
更稳的方式是先把规则定清楚。
组件内部统一走 this.getUIContext()。Ability 或窗口级逻辑统一在 loadContent 完成后,从主窗口拿 UIContext。公共工具函数不要再偷偷依赖上下文,统一显式接收 UIContext。
这三条先定下来,再开始逐步替换,项目里的风格才会收得住。
等替换做完了,再考虑性能层面的事。比如一些高频使用的尺寸,确实可以缓存。但缓存也不能回到老路上,做成一个全局静态值到处复用。更合理的方式,是让缓存和当前 UIContext 绑定。哪个页面、哪个窗口在用,就按它自己的上下文缓存。
class UnitCache {
constructor(private uiContext: UIContext) {}
readonly space4 = this.uiContext.vp2px(4);
readonly space8 = this.uiContext.vp2px(8);
readonly icon24 = this.uiContext.vp2px(24);
readonly icon32 = this.uiContext.vp2px(32);
}
@Entry
@Component
struct CachedLayoutDemo {
private cache?: UnitCache;
aboutToAppear() {
this.cache = new UnitCache(this.getUIContext());
}
build() {
Column({ space: this.cache?.space8 ?? 0 }) {
Text('缓存后的常用尺寸')
.margin({ bottom: this.cache?.space4 ?? 0 })
Image($r('app.media.ic_public'))
.width(this.cache?.icon24 ?? 24)
.height(this.cache?.icon24 ?? 24)
}
}
}
这种写法看着比过去麻烦一点,但边界是清楚的。尤其是多窗口、折叠屏这类设备环境里,这种清楚比省几行代码更重要。
总结
这轮迁移最容易被误解成一次普通的接口替换。实际上不是。真正要改的是思路,单位转换以后不能再默认是一个谁都能随手调的全局工具,它必须和具体 UI 实例绑定。
组件里就近拿 this.getUIContext(),窗口级逻辑在 loadContent 完成后从主窗口拿 UIContext,公共工具函数显式接收 UIContext。再进一步,把高频尺寸缓存也跟当前上下文绑在一起,而不是继续做成全局静态值。
更多推荐



所有评论(0)