
1. 项目背景与现实场景1.1 为什么 React Native 适配鸿蒙后第一个要面对的是屏幕尺寸先把结论摆出来React Native 跑鸿蒙这件事社区适配层已经做到了“能跑起来”的程度但你真正把业务页面一搬进去第一个翻车的十有八九是布局错乱而布局错乱的根源八成落在屏幕尺寸获取这个最基础的 API 上。iOS 和 Android 上 Dimensions 用得太顺手了以至于很多团队根本意识不到这是一个需要单独处理的问题直到在鸿蒙设备上打开 App看见界面被裁掉半边、弹窗定位飘到屏幕外才回头来查尺寸来源。我最早接触这块是在做 App 的鸿蒙化改造时。当时团队的计划很简单JS 层尽量复用把原生依赖逐个替换掉。React Native 的鸿蒙适配层也确实做到了 API 对齐Dimensions.get(window)这类调用在语法上完全一致但实际跑下来你会发现同样是拿了宽度做百分比布局鸿蒙上偶尔就是不对而且在分屏、自由窗口、折叠屏这些场景下表现得尤其明显。所以这篇东西的核心就是把Dimensions在鸿蒙环境下的行为差异、底层原因、封装方案和坑位一次讲清楚。适合正在做 React Native 鸿蒙化改造的移动端开发也适合打算评估鸿蒙跨平台成本的前端团队。内容不需要你有原生基础就能看懂大部分但涉及鸿蒙窗口模型的部分我会尽量讲透一点因为那是理解所有“为什么”的关键。1.2 鸿蒙的窗口模型和 iOS/Android 不是一回事很多人以为鸿蒙既然是移动操作系统窗口概念应该和 Android 差不多。但如果你去看鸿蒙的 Stage 模型会发现它的窗口管理比 Android 要“重”得多。在鸿蒙上应用不是直接拿到一个显示区域就开始画而是要通过UIAbility的onWindowStageCreate生命周期拿到WindowStage再通过WindowStage获取主窗口查询窗口属性、监听窗口尺寸变化。整个流程走的是“Ability WindowStage MainWindow”三层结构而 Android 上Activity直接就带着Display信息了。这个模型差异直接影响到了 React Native 的适配层。JS 侧的Dimensions模块最终的数据源在 iOS 上是UIScreen在 Android 上是DisplayMetrics在鸿蒙上则要依赖windowStage.getMainWindowSync().getWindowProperties().windowRect。换句话说鸿蒙适配层要把窗口属性桥接给 JS就必须正确拿到窗口的windowRect而且要在合适的时机去拿。时机不对拿到的就是 0 或者异常值反映到 JS 层就是Dimensions.get(window).width变成 0首屏直接白屏或者整页乱掉。这也是为什么热词里会出现“react native 启动白屏”“windowstage loadcontent”这类搜索。它们表面上不是一个问题但底层全都指向同一个点鸿蒙窗口创建和 React Native 首屏渲染之间的时序关系。你只要弄明白了窗口模型这些现象就都能解释了。1.3 这个项目到底要解决什么问题简单概括我这次要分享的实操内容包括三块第一DimensionsAPI 在鸿蒙上的正确使用姿势以及和 iOS/Android 的差异对照第二一个跨平台的useScreenMetricsHook 封装能做到横竖屏切换、分屏、自由窗口下自动更新尺寸第三鸿蒙原生侧窗口尺寸桥接的核心代码以及一套排查尺寸相关 Bug 的速查方法。按这个思路做完你的 RN App 在鸿蒙上的尺寸适配不说一劳永逸至少能把最磨人的那些问题一次性清掉。2. Dimensions API 核心用法与字段详解2.1 get 方法支持的几种取值你真的分清了吗Dimensions模块是 React Native 自带的不需要额外安装依赖直接import { Dimensions } from react-native就能用。大多数人是这么拿屏幕宽度的const { width, height } Dimensions.get(window);这句话本身没问题问题出在很多人不知道get方法除了window还能传什么也不知道不同取值在不同平台上的行为差异。我把常用的取值整理成了表格取值含义iOS 行为Android 行为鸿蒙适配层常见行为window应用窗口区域不包含状态栏不包含状态栏和导航栏应用窗口区域一般推荐screen整个屏幕区域等同 window包含系统栏部分版本不可用或返回全屏值screenWidth旧版遗留 API物理像素宽物理像素宽尽量不用screenHeight旧版遗留 API物理像素高物理像素高尽量不用这里重点说鸿蒙。鸿蒙适配层早期版本对screen的支持并不完整有些版本直接把screen映射到了窗口有些版本返回的是undefined还有的返回了整个屏幕的物理尺寸导致 UI 计算整体偏差。这就造成一个很隐蔽的问题代码在 Android 上调Dimensions.get(screen)拿到的宽度包含导航栏布局差不了多少到了鸿蒙上如果适配层实现不同可能弹窗居中的算法直接算错了基准值。我的建议非常简单粗暴全平台一律使用window不要用screen。如果业务上确实需要判断系统栏高度通过StatusBar.currentHeightAndroid或安全区组件去处理而不是赌screen在鸿蒙上的行为。2.2 window 对象里到底有哪些字段很多人以为Dimensions.get(window)返回的只有width和height其实还包括scale和fontScale。完整结构如下{ width: 360, height: 780, scale: 3, fontScale: 1 }width和height是逻辑像素也就是 RN 布局时实际使用的数值。比如你在代码里写width: 180就表示半个屏幕宽。scale是设备像素比物理像素 逻辑像素 ×scale。鸿蒙上这个概念对应的是vp虚拟像素底层换算逻辑类似1 vp 约等于中密度屏幕上的 1 物理像素高密度屏幕上会自动放大。fontScale是用户设置的系统字体缩放比例它影响的是文字布局如果用户把系统字体调大了你在固定高度容器里放的文字就可能被截断。实际开发里scale最常见的用途是判断设备是不是高清屏然后决定是否加载 2x 或 3x 的图片资源。fontScale则要谨慎处理——有些团队为了让 UI 在所有设备上长得一模一样会把fontScale强制重置为 1。这个操作在 iOS 上很常见在鸿蒙上就要注意了如果你不做处理鸿蒙系统字体的默认缩放行为和 Android 可能略有差异文字的换行位置就会和设计稿对不上。2.3 动态监听addEventListener 和 useWindowDimensions屏幕尺寸不是一成不变的横竖屏切换、折叠屏展开、分屏拖拽、自由窗口缩放都会让窗口尺寸发生变化。Dimensions模块提供了事件订阅能力import { Dimensions } from react-native; const subscription Dimensions.addEventListener(change, ({ window }) { console.log(window size changed:, window.width, window.height); }); // 不再需要时记得解绑 subscription.remove();在函数组件里更推荐直接使用 React Native 官方提供的useWindowDimensionsHookimport { useWindowDimensions } from react-native; function MyComponent() { const { width, height, scale } useWindowDimensions(); return Text当前窗口尺寸{width} x {height}/Text; }这个 Hook 内部就是订阅了change事件组件卸载时自动解绑不用你手动管理生命周期。在 iOS 和 Android 上这套机制运转得很好但在鸿蒙上有一个前提条件适配层必须正确地把鸿蒙窗口的windowSizeChange事件转发到 JS 的Dimensions模块。如果转发链路没打通就会出现一个非常经典的问题——界面只在 App 启动时取了一次尺寸横竖屏切换后布局完全乱掉日志里却没有任何报错。这个问题怎么排查后面专门讲。现在先记住一条useWindowDimensions在鸿蒙上依赖原生事件桥接转换不是自动的。注意Dimensions.set在开发调试时可以用来模拟屏幕尺寸变化但绝对不要在生产代码里用它去“改写”真实尺寸。它不是用来适配多端的只是开发辅助工具。3. 鸿蒙环境下的实操封装与方案落地3.1 跨平台尺寸 Hook 的封装思路光会用Dimensions.get不够工程化至少要做到把尺寸逻辑统一收口。我推荐封装一个自己的 Hook名字随意关键是内部要处理三个事第一统一用window取值第二监听变化事件第三针对鸿蒙平台做一个判断必要时走原生桥接补充数据。先看基础版本import { useState, useEffect } from react; import { Dimensions, Platform } from react-native; const isHarmony Platform.OS harmony; export function useScreenMetrics() { const [metrics, setMetrics] useState(() { const { width, height, scale } Dimensions.get(window); return { width, height, scale }; }); useEffect(() { const subscription Dimensions.addEventListener(change, ({ window }) { if (window) { setMetrics({ width: window.width, height: window.height, scale: window.scale, }); } }); return () subscription.remove(); }, []); return metrics; }这个版本在 iOS、Android 上够用但鸿蒙上如果事件桥接有问题横竖屏切换后 Hook 不会接收到任何更新。所以真正生产级别的封装必须加入一个“鸿蒙兜底机制”当检测到平台是harmony时主动通过原生桥接模块查询一次窗口尺寸并且优先以原生返回值为准。这里还要提醒一个平台判断的细节鸿蒙适配层里的Platform.OS在 JS 层通常暴露为harmony但不同适配版本可能不一样有些版本可能显示为android适配层直接复用了 Android 的 PlatformConstants。我的经验是在接入初期就把Platform.OS打出来看一眼不要凭空假设。条件允许的话最好用Platform.Version或者原生模块提供的标识来判断思路是“先探测、后使用”。3.2 鸿蒙原生侧WindowStage 窗口尺寸的读取与桥接现在说鸿蒙原生侧怎么把窗口尺寸喂给 JS。这里以 ArkTS 语言为例核心点在UIAbility的onWindowStageCreate阶段拿到WindowStage然后获取主窗口属性和注册窗口尺寸变化监听。理解阶段先看读取部分import { window } from kit.ArkUI; function getWindowRect(windowStage: window.WindowStage): window.Rect { const mainWindow windowStage.getMainWindowSync(); const properties mainWindow.getWindowProperties(); return properties.windowRect; }windowRect里的width、height就是当前窗口的宽高单位是 vp。这里需要注意getMainWindowSync是同步方法如果在窗口尚未创建完成时调用可能会抛异常或者拿到空对象。鸿蒙官方推荐的流程是在onWindowStageCreate回调触发后先loadContent再在loadContent的完成回调里去查窗口属性这样能保证窗口已经进入可用状态。窗口尺寸变化的监听则用事件机制mainWindow.on(windowSizeChange, (size: window.Size) { // 将 size.width 和 size.height 通过桥接模块发给 JS 侧 notifyJSWindowSizeChanged(size.width, size.height); });React Native 鸿蒙适配层本质上就是在这一层做了封装把windowSizeChange事件转换成 JS 侧Dimensions模块的change事件。如果你是自己接原生模块不需要担心如果你怀疑适配层没有正确处理自己写一个极简的 NativeModule 来验证是非常有效的排查手段几分钟就能确认数据链路通不通。3.3 横竖屏切换和分屏自由窗口场景分屏和自由窗口是排查尺寸问题的高危场景尤其是折叠屏。常规直板手机上App 的窗口通常就是全屏横竖屏切换还比较直观。但鸿蒙支持自由窗口用户可以拖拽改变窗口大小这时候窗口宽高会连续变化而且可能不是规则的横屏或竖屏比例。我遇到的典型 Bug 是页面布局在启动时用Dimensions.get(window)算了一次宽度内部使用了绝对定位的弹层组件。用户把应用拖成一个窄长条后弹层还停留在旧宽度的中央位置看起来就像“弹窗飘到了屏幕外”。这个问题的根源就是使用了快照式尺寸而不是响应式尺寸。解决方案是在代码层面养成两个习惯1. 所有需要响应尺寸变化的组件优先使用useWindowDimensions或自定义的useScreenMetrics2. 如果组件内部对尺寸做了缓存要在尺寸变化时主动失效重算。比如状态管理器里记录lastWidth在渲染前判断当前width是否等于缓存值不相等就重新计算布局参数。另一个隐藏的坑是折叠屏展开态。鸿蒙上有一些折叠屏设备展开前后窗口尺寸变化不是简单的等比放大宽高比例会剧烈变化。如果你的 UI 设计只考虑了竖屏窄屏和横屏宽屏两种形态在折叠屏展开时就会出现中间态比例常见的做法是把断点判断从“横屏/竖屏”升级为“按当前宽高比动态判断”。比如宽度大于某个阈值时采用多列布局而不是仅仅判断width height。3.4 设计稿基准宽度与多设备适配换算讲一个工程上经常被忽略的点设计稿尺寸怎么映射到鸿蒙的 vp。常见的做法是设计稿按 750 宽度出图那么代码里所有尺寸都要除以一个基准系数。这个系数在 Android 上通常用屏幕宽度/360 计算因为 360dp 是 Android 的经典逻辑宽度基准之一。鸿蒙上类似但不同设备的 vp 基准不完全一致直接写死 750 或 360 都有风险。我的建议是封装一个scale方法以设计稿宽度为基准做动态换算import { Dimensions } from react-native; const designWidth 375; // 设计稿逻辑宽度 const { width: windowWidth } Dimensions.get(window); export function scaleSize(size) { return (size * windowWidth) / designWidth; }这个方法的核心思想是让 UI 元素随窗口宽度等比缩放。要注意的是scaleSize在窗口尺寸变化后也要重新取windowWidth所以配合上面自定义 Hook 一起用是最稳妥的。如果项目里已经有了基于PixelRatio的适配库也可以保留但思路本质上相同——尺寸都是基于当前窗口逻辑宽度的比例换算而不是简单的物理像素堆叠。4. 常见问题与排查技巧实录4.1 启动白屏尺寸获取的时机到底有多重要热词里“react native 启动白屏”出现频率很高很多人以为白屏只是 JS Bundle 加载慢或者崩溃其实在鸿蒙上还有一类白屏和尺寸获取强相关。现象是应用启动后屏幕一片白过几秒才出现内容或者有些页面直接一直白着。把日志打出来会看到Dimensions.get(window)的宽度和高度都是 0或者首屏渲染发生在窗口尺寸尚未注入 JS 侧之前。原因在于 React Native 的鸿蒙适配层在创建 RN 宿主时需要在原生侧设置初始尺寸。如果这个初始尺寸是从一个未完成的窗口上读取的JS 侧拿到 0首屏的根视图计算出来的布局就是零宽高渲染结果自然是白屏。再加上 JSBundle 执行需要时间问题会被放大。排查和解决办法分三步。第一步在原生侧打日志确认windowRect在初始化时是否有效第二步在 JS 入口文件顶部打印Dimensions.get(window)确认 JS 侧拿到的初始值第三步如果确认是时序问题就调整 RN 宿主初始化的位置把它放到loadContent完成回调之后再执行而不是在onWindowStageCreate里立即执行。注意不要自己用setTimeout延时初始化那只是碰运气。正确做法是依赖生命周期回调保证窗口已经进入可用状态。4.2 get(screen) 在鸿蒙上行为异常前面已经提过screen取值在鸿蒙上可能不靠谱。实际排查中我发现过三种情况适配层返回undefined、返回全屏物理尺寸、返回和window完全相同。这三种行为对业务的影响完全不同但统一的表现是某个组件或弹窗定位不对。最快验证方法是写几个日志点分别打印Dimensions.get(window)和Dimensions.get(screen)在直板手机和折叠屏上各跑一遍。如果结果不一致老老实实全部改回window。如果你的业务真的依赖“整个屏幕的完整尺寸”来做全屏遮罩也不要直接用screen应该用window搭配安全区计算否则遮罩会盖到系统栏或者出现底部偏移。4.3 横竖屏切换后尺寸不更新异常现象App 启动后正常转屏后界面布局没有跟着变。排查思路先分清是“事件没触发”还是“触发了但视图没刷新”。如果是前者需要在鸿蒙原生侧确认windowSizeChange是否注册成功。有些适配层版本只在窗口创建时取了一次尺寸没有注册后续事件监听这就属于适配层能力缺失。解决办法是自己在原生侧写一个极简桥接模块做兜底把窗口尺寸变化通过 DevEmit 事件发到 JS或者干脆在 JS 侧加一个轮询监听系统横竖屏状态变化如果能拿到的话但轮询是下策能修原生还是先修原生。如果是后者也就是事件到了但视图没刷新多半是状态管理或者组件缓存的问题。检查你的页面组件是不是用了React.memo且没有把width作为依赖项或者布局是同步写在构造函数里的。这类问题在 iOS、Android 上也会出现但在鸿蒙上因为事件频率高、自由窗口拖拽时连续触发暴露得更明显。我整理了一个速查表方便你定位问题现象可能原因排查动作白屏或全部尺寸为 0窗口初始化时序不对原生侧日志打印 windowRectget(screen)返回异常值适配层未完整实现 screen改用window转屏布局不变事件桥接未打通原生侧验证 windowSizeChange转屏布局不变JS 组件缓存未失效检查 memo、useMemo 依赖自由窗口拖拽后弹窗偏移尺寸快照未更新改用 Hook 实时取宽高折叠屏展开后比例怪断点逻辑过于简单改用宽高比动态断点4.4 安全区和刘海屏的额外一步尺寸适配之外鸿蒙还有一个需要单独处理的问题安全区。挖孔屏、刘海屏、折叠屏的摄像头区域都会占据一块不可用的屏幕空间如果你的页面底部有操作按钮或者顶部有标题栏就要用SafeAreaView或者手动计算安全区边距。React Native 官方组件库在鸿蒙适配层里不一定完整支持SafeAreaView的行为所以稳妥的方案是用StatusBar.currentHeight加一个自定义的安全区容器或者直接用原生侧传回的窗口可交互区域计算边距。这类问题的共性是只看宽高数据是正常的但 UI 内容被系统栏遮住了所以排查时要把“窗口尺寸”和“内容可视区域”区分开。鸿蒙的窗口 API 里同样有能力查询可交互区域如果在 JS 层发现遮档问题优先考虑从原生侧取安全区数值而不是在 JS 层反复试 padding。5. 一个完整的响应式页面案例5.1 场景设计用一个网格型首页来收尾。需求不复杂一个商品展示页竖屏时每行显示 2 个卡片横屏时每行显示 4 个卡片窗口宽度变化时要实时调整列数。这个案例覆盖了useWindowDimensions、动态计算、响应式布局三个核心点。5.2 核心代码实现新建一个ResponsiveGrid组件import React from react; import { View, Text, StyleSheet, useWindowDimensions } from react-native; const cardGap 12; const cardMargin 16; function ResponsiveGrid({ items }) { const { width } useWindowDimensions(); // 竖屏 2 列横屏 4 列 const isLandscape width height; // 实际需要拿到 height const columns isLandscape ? 4 : 2; const cardWidth (width - cardMargin * 2 - cardGap * (columns - 1)) / columns; return ( View style{styles.container} {items.map((item, index) ( View key{index} style{[styles.card, { width: cardWidth }]} Text{item}/Text /View ))} /View ); }注意这里我故意留了一个问题useWindowDimensions没有解构height直接用width height就会报错。实际代码要写完整const { width, height } useWindowDimensions();通过这种方式计算出的cardWidth会自动跟随窗口宽度刷新因为useWindowDimensions内部监听了change事件。在鸿蒙上前提依然是事件桥接正常否则换成第 3 节里自封的useScreenMetrics就能兜底。5.3 验证步骤与细节补充把组件跑起来后我在鸿蒙设备上做了三轮验证第一轮直板手机切换横竖屏确认列数在 2 和 4 之间切换第二轮开自由窗口把窗口从全屏拖到半屏确认卡片宽度实时变化而不是只在新尺寸稳定后跳变第三轮折叠屏展开确认宽高比变化时列数切换平滑没有出现同时渲染两套布局的闪烁。三轮验证里第一轮最容易过第二、三轮才真正考验适配层的事件转发能力。如果第二、三轮翻车回到第 4 节的排查表逐个对位绝大多数问题都能收敛到事件桥接和状态刷新两个根因上。最后再分享一点我自己的经验踩了这么多坑之后我养成的固定习惯是新项目一定在入口文件最前面打印一行Dimensions.get(window)和Platform.OS的日志任何尺寸异常从日志就能直接定性。这个习惯帮我省了不知道多少时间非常推荐你也试试。还有一个小技巧在给鸿蒙做适配时不要只测试直板全屏这一个形态。折叠屏展开、分屏拖拽、自由窗口、横竖屏切换这四种形态都要在开发阶段就建立可重复的验证路径。尺寸适配的 Bug 往往不是不存在而是你没在用那个形态的时候就藏在那边等到用户碰到了才爆出来。本文后续还可以往PixelRatio适配、安全区封装、动态断点布局这几个方向继续扩展但核心始终是同一个先拿对尺寸再做布局。尺寸这一关过了React Native 在鸿蒙上的跨平台改造就已经走完了最棘手的一段路。