ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙上RN获取屏幕尺寸不准?物理像素与逻辑像素适配全攻略

鸿蒙上RN获取屏幕尺寸不准?物理像素与逻辑像素适配全攻略 最近在把公司一个核心业务App往鸿蒙上搬我们技术栈选的是React Native。本来以为RN在鸿蒙上跑起来最麻烦的肯定是原生模块适配结果第一批联调bug里最折腾我的反而是屏幕尺寸获取。Dimensions.get(window)在Android、iOS上明明跑得好好的一上鸿蒙拿到的数值就各种不对劲。查了一圈资料发现社区里遇到这个问题的人不少但真正讲清楚原因和解决路径的并不多。这篇文章就把我这几周的踩坑记录、源码分析和最终的稳定方案一次性写明白希望能帮到正在做鸿蒙跨平台适配的同行。先交代一下背景。我们项目的RN版本是0.72鸿蒙侧用的是react-native-harmony这个社区适配方案这是目前把RN跑上鸿蒙最成熟的路线。在这套方案里RN的JavaScript层和渲染层是分开的JavaScript层通过一个桥接层调用鸿蒙原生能力屏幕尺寸这类信息就是在桥接层里拿到的。问题恰恰就出在这个桥接层对“屏幕尺寸”的定义和标准RN实现不一致上。下面我会从原理、复现、修复、参数细节到替代方案一步步拆开讲。1. 为什么鸿蒙上拿到的屏幕尺寸会“不准”先说个结论不是Dimensions这个API本身坏了而是它背后的数据来源和RN标准实现不一样。在Android上RN的Dimensions.get(window)返回的是应用可用窗口的尺寸单位是dp这个dp和我们平时说的逻辑像素是一回事。在iOS上它返回的是points。两边虽然叫法不同但本质都是“逻辑像素”——也就是系统帮我们把物理像素除以了屏幕密度让开发者不用关心设备具体是多少物理分辨率。你的设计稿如果是375x812这类数值那在iPhone 15 Pro上拿到的就是接近这个数值的逻辑尺寸。到了鸿蒙这里情况就变了。鸿蒙自己有完整的尺寸体系vpvirtual pixel虚拟像素和fpfont pixel字体像素。vp是鸿蒙的逻辑像素单位fp是字体专用单位设计上很像Android的dp和sp。一个合理的适配方案应该是把RN的Dimensions映射到鸿蒙的vp上这样JS层拿到的数值就和Android/iOS保持一致了。但我们实测下来react-native-harmony早期版本直接返回了物理像素值。举个例子华为Mate 60 Pro的物理分辨率是2720x1260如果RN的Dimensions直接把这个数值返回给JS那你写Dimensions.get(window).width拿到的就是1260而不是期望中的360或393之类的逻辑值。后果很直接你按照设计稿写的样式在鸿蒙设备上一个按钮可能占了半个屏幕宽。更隐蔽的问题是如果你在代码里用这个值去算布局比例、轮播图分页宽度、或者横竖屏切换时的重新布局那计算出来的结果全部是错的。我们当时第一反应是怀疑鸿蒙适配包版本有问题升级了好几个版本问题依旧。后来直接去翻react-native-harmony的源码才发现它确实有一版在获取window尺寸时没做vp换算是后来社区提了issue才修复的。如果你用的是旧版本或者某些厂商定制ROM对鸿蒙接口的返回做了特殊处理那你遇到的就是这个物理像素问题。1.1 物理像素和逻辑像素的根本区别网上很多文章喜欢把这个问题说成“分辨率不对”但真正理解物理像素和逻辑像素的区别才能根治这类问题。物理像素就是屏幕硬件上真实发光的点。逻辑像素则是软件层定义的一个抽象单位它和物理像素之间通过一个比例因子换算这个比例因子在Android叫densityiOS叫scale鸿蒙叫densityDPI或类似的概念。为什么要有逻辑像素因为不同设备的物理分辨率和屏幕尺寸都不一样如果开发直接使用物理像素同一个UI在低端机和旗舰机上会呈现出完全不同的物理尺寸而且在小屏高分辨率设备上会显得特别小。逻辑像素的出现就是为了让开发者可以用同一套数值在不同设备上获得大致一致的视觉尺寸。RN的Dimensions恰恰是建立在逻辑像素之上的API。它的设计文档里写得很清楚返回的值应该和CSS像素对齐也就是Web里的CSS像素。而鸿蒙的vp在设计上就是为了对标Android的dp所以合理情况下鸿蒙的Dimensions也应该返回vp值而不是物理像素。1.2 Dimensions在鸿蒙适配里的具体差异点我整理了三个实际遇到的差异点大家在排查时可以直接对照。第一个是window和screen的区别。RN的Dimensions可以传window或screen两个参数。在标准实现里window指的是应用可用的绘制区域screen指的是整个屏幕物理区域。但在鸿蒙适配里早期版本对这两个参数并没有严格区分有的版本甚至直接忽略了参数统一返回屏幕物理尺寸。这就导致那些依赖window和screen差异来做沉浸式状态栏适配的代码全部失效。第二个是状态栏和导航栏的处理。Android上window的高度是不包含系统状态栏和导航栏的iOS上则包含了安全区域之外的逻辑处理而鸿蒙早期适配里返回的高度到底是包含了状态栏还是排除了状态栏文档里写得含糊不清。实测中我们发现不同系统版本行为还不一样鸿蒙4.0和4.2的返回值都有肉眼可见的差异。第三个是横竖屏切换时的刷新机制。标准RN在屏幕方向变化时会自动触发Dimensions的监听emit一个change事件。但在鸿蒙适配里旧版本这个事件触发很不稳定有时候旋转了屏幕不通知JS层导致应用里那些根据屏幕宽度计算出来的布局彻底错乱。这个问题比较隐蔽因为没有报错没有异常就是UI表现不对特别难排查。2. 复现问题一个最简单的Dimensions用例做技术排查第一步永远是把问题稳定复现出来。我写了一个最简Demo把每个关键值都打出来跑在不同设备上做对比。这个Demo的核心代码非常简单就是下面这个组件import { Dimensions, Platform, Text, View } from react-native; function ScreenInfo() { const window Dimensions.get(window); const screen Dimensions.get(screen); const { width: windowWidth, height: windowHeight } window; const { width: screenWidth, height: screenHeight } screen; return ( View style{{ flex: 1, paddingHorizontal: 20 }} Text平台: {Platform.OS}/Text Textwindow宽度: {windowWidth}/Text Textwindow高度: {windowHeight}/Text Textscreen宽度: {screenWidth}/Text Textscreen高度: {screenHeight}/Text Text物理分辨率: {screenWidth windowWidth ? 不一致 : 一致}/Text /View ); }这个Demo在Android和iOS上跑出来的结果视觉尺寸都是几百这个量级。但到了鸿蒙Mate 60 Pro上直接打印出了1260和2720这样的数字这基本就能断定是物理像素了。为了进一步确认我又在代码里用PixelRatio.get()打印了当前的像素密度结果是3.5。把1260除以3.5得到360把2720除以3.5得到777.1。这个结果很接近鸿蒙系统设置里的逻辑分辨率。到这里问题定位就非常清晰了鸿蒙侧返回了物理像素而JS侧期望的是逻辑像素。2.1 环境与版本信息如果你也想复现建议环境尽量和我们保持一致否则现象可能不同React Native版本0.72及以上建议0.720.74这个区间新架构下的行为差异后面会讲react-native-harmony版本我们最初用的是0.72.x对应的适配版本问题明显升级到较新版本后逻辑像素换算已部分修复但screen参数行为仍然不准鸿蒙系统版本HarmonyOS 4.0/4.2实测两者在window高度计算上有差异测试机型Mate 60 Pro、P40能明显复现物理像素问题如果你的鸿蒙版本比较新比如5.0或6.0可能表现会不一样因为鸿蒙自己的接口也在持续演进。但核心排查思路不变对比逻辑像素、物理像素和PixelRatio三者的关系。3. 补救方案应用层统一换算既然鸿蒙适配层一时半会儿改不动我们就得在应用层做统一换算保证业务代码不用到处改动。这也是跨平台开发里最常见的一种思路不依赖平台适配包的正确性在JS层做兜底。思路是这样的在App启动早期先拿到鸿蒙系统的dp逻辑像素和物理像素如果发现Dimensions返回的值明显大于常见逻辑像素范围比如大于1000甚至接近2000就判定为“鸿蒙物理像素模式”然后统一除以一个缩放因子。问题来了这个缩放因子从哪来鸿蒙适配层里提供了PixelRatio.get()但它返回的是不是鸿蒙的vp到物理像素的倍率实测下来在多数设备上它返回值等于物理像素除以逻辑分辨率但有些定制ROM会返回错误值。所以我们又叠加了一层安全判断如果计算出来的倍率小于1或者大于4就用兜底值1.0即不缩放。这个方案看起来简单落地时有一个细节需要特别注意时机问题。RN应用启动时Dimensions的值是异步从原生层拿过来的第一次render时可能还没准备好。如果你写在模块顶层做静态常量很容易拿到初始错误的默认值。建议放在useEffect里做一次启动校准校准完成后再给子组件传值。3.1 封装一个跨平台安全获取尺寸的工具函数我把这个逻辑封装成了一个独立工具函数所有业务组件都从它这里读取尺寸不再直接调用Dimensions.get。核心代码如下import { Dimensions, PixelRatio, Platform } from react-native; const getSafeWindowSize () { const window Dimensions.get(window); const rawWidth window.width; const rawHeight window.height; const pixelRatio PixelRatio.get(); // 判断是否可能是鸿蒙物理像素模式 // 逻辑像素在主流设备上不会超过600平板例外 if (Platform.OS harmony (rawWidth 800 || rawHeight 1000)) { const divideRatio pixelRatio 1 pixelRatio 5 ? pixelRatio : 1; return { width: rawWidth / divideRatio, height: rawHeight / divideRatio, rawWidth, rawHeight, }; } return { width: rawWidth, height: rawHeight, rawWidth, height: rawHeight, rawHeight, isHarmony: false, }; };这个工具函数有几个地方值得说明。第一我并没有一刀切把所有平台的数值都除以PixelRatio因为Android和iOS的Dimensions本来就是逻辑值再去除以倍率反而会错。第二判断阈值用的是800和1000这是综合了常见逻辑分辨率和物理分辨率的经验值。如果你要支持平板这个阈值需要再调大一点否则平板竖屏时逻辑分辨率可能超过800导致误判。第三这个工具函数只做了“尺寸换算”没有处理横竖屏监听所以它不负责动态刷新。3.2 为什么不能简单使用useWindowDimensions很多RN开发者会把useWindowDimensions当成最佳实践因为它是动态响应屏幕变化的Hook。但在鸿蒙适配这个问题上它并没有任何魔法——它内部依然是调用Dimensions.get然后监听change事件。也就是说如果底层适配包的Dimensions返回值是物理像素useWindowDimensions同样会返回物理像素如果change事件不触发useWindowDimensions也不会自己动。所以不要指望换一个API能解决所有问题。更好的做法是用useWindowDimensions拿到值之后再过一遍我们上面的缩放逻辑。另外有个经验要分享如果你在自己的代码里已经大量使用了useWindowDimensions可以把前面那个工具函数改造成一个Hook返回值结构尽量兼容useWindowDimensions的返回结构这样你只需要把业务组件里的调用方式统一替换一次就能实现全局修正不用在业务代码里到处打补丁。3.3 测试用例与验收标准修改完之后一定要用一个可量化的用例来验收。我在应用里放了一个调试面板里面的验收标准是这样定义的在Mate 60 Pro上getSafeWindowSize().width应该等于360或者接近接逻辑分辨率在P40上应该等于393如果P40的逻辑分辨率是393。同时对比物理分辨率rawWidth应该等于1260或1080。如果换算出来的值超过这个范围的5%以上说明缩放因子还是不对需要重新检查PixelRatio.get()的返回值。这里有个特别容易犯的错误某些华为手机的默认显示设置里开了“屏幕显示大小”的调节这会影响系统的逻辑分辨率。同一个设备在“默认”和“大”两种显示模式下逻辑分辨率会不同。你在真机上测试时一定要确认系统设置里的显示模式和目标用户一致否则你的验收数据会自己打架。4. 额外避坑适配方案与未来替代路径搞定尺寸换算问题之后我又系统性梳理了鸿蒙适配里其他几个和屏幕相关的坑这次一并写出来。第一个坑是SafeAreaView的失效。RN标准实现里SafeAreaView依赖iOS的原生安全区域数据在鸿蒙适配包里很多版本根本没实现这个模块。即使你用了SafeAreaView它也不会自动避开鸿蒙状态栏和底部导航条。我们在鸿蒙上查界面时发现顶部内容被状态栏遮住排查了很久才发现是SafeAreaView在这里压根没生效。解决方案是把安全区域的逻辑换成鸿蒙原生的avoidArea能力或者在JS层手动使用StatusBar.currentHeight鸿蒙适配里该值也可能不准配合固定的安全边距来兜底。第二个坑是StatusBar.currentHeight的数值含义差异。在Android上拿到的通常是状态栏高度在鸿蒙上不同适配版本拿到的可能是完全不同的值有的返回物理像素有的返回vp。我的建议是不要直接信任这个值而是在真机上用系统设置调整字体大小和显示大小观察UI在不同档位下的表现然后决定你的安全边距是否需要动态适配。第三个坑涉及新架构New Architecture和Dimensions的关系。RN 0.74之后新架构成为默认鸿蒙适配里新旧架构对原生模块的调用方式差别很大。我们内部测试发现旧架构下Dimensions数据错误更严重新架构下的适配包已经部分修复但依然不建议完全依赖默认行为。换句话说新架构替代不了应用层兜底该做归一化处理还是得做。第四个坑是监听事件的可靠性。前面提到横竖屏切换时change事件触发不稳定这里给一个更稳的替代思路——用onLayout来驱动尺寸刷新。具体做法是在根组件上放一个绝对定位且铺满全屏的透明View监听它的onLayout事件来获取实际可用尺寸这个尺寸一定是正确的逻辑像素值至少和鸿蒙系统渲染层一致而且一定会随屏幕方向变化而更新。这种方式比Dimensions监听更可靠也绕开了适配层的坑。关于未来的替换路径再展开说两句。现在社区里已经有人在做基于鸿蒙ArkUI声明式能力的RN适配优化但这些方案都还在早期。短期内如果你想在鸿蒙上继续用RN就必须接受react-native-harmony这个桥接层的各种“方言差异”。这些差异包括尺寸、安全区、字体缩放、键盘弹出高度、状态栏导航栏高度等几乎都和视觉布局相关。我个人的建议是把它们集中收敛到一个统一的UI适配模块里业务层只暴露单一的getSafeLayoutInfo接口后续鸿蒙适配包更新你只需要改这个模块的实现业务代码不用动。这套做法的本质就是把不稳定的平台差异全部隔离在架构的边缘层不让它们渗透进业务代码的肌理。最后再分享一个小技巧调试鸿蒙屏幕尺寸问题不要只看RN日志。在鸿蒙开发工具里可以实时查看ArkUI侧的窗口属性里面会列出窗口的宽高、是否包含状态栏、分辨率等详细信息。把ArkUI侧的窗口宽高和RN侧拿到的LogicalWidth做对比如果ArkUI显示的是360x777而RN返回1260x2720那就100%是适配层的单位换算问题和你的业务代码没关系。这个对比法能让你在团队里快速定位责任方少背很多锅。
返回列表