查看: 320|回复: 0

鸿蒙上RN图片模糊?PixelRatio屏幕密度适配实践

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
做鸿蒙RN开发时,屏幕密度适配是个绕不开的话题。最早接触PixelRatio时我也不太理解,为什么不能直接用Dimensions.get('window')拿屏幕宽高?直到在一个电商项目里,商品图片在华为P40上边缘发虚,才发现问题所在:P40的像素密度是3.0,而图片只按@1x准备的,分辨率不够自然就模糊了。后面在鸿蒙设备上跑时,PixelRatio.get()返回3.5,比很多Android设备的密度都高,这也说明鸿蒙旗舰机的屏幕细腻程度确实不错。

PixelRatio提供设备的像素密度信息,get()返回密度值。鸿蒙设备的PixelRatio范围大约在2.0到3.5之间,旗舰机型普遍在3.0到3.5,平板和中低端设备在2.0左右。需要特别注意:这个值是浮点数,有些设备返回2.625这样的值,用switch-case做精确匹配容易踩坑,建议用范围判断。
  1. import { PixelRatio } from 'react-native';
  2. const ratio = PixelRatio.get();
  3. if (ratio >= 3) {
  4.   // 超高密度屏幕,加载 @3x 图片
  5. } else if (ratio >= 2) {
  6.   // 高密度屏幕,加载 @2x 图片
  7. } else {
  8.   // 普通密度,加载 @1x 图片
  9. }
复制代码

图片加载是最典型的应用场景。如果使用本地require加载图片,RN会自动根据PixelRatio匹配对应资源,只要准备icon@2x.png和icon@3x.png即可。但网络图片必须手动处理,按密度拼接不同的资源地址:
  1. const getImageSource = (baseName: string) => {
  2.   const scale = PixelRatio.get();
  3.   let suffix = '@3x';
  4.   if (scale < 2) suffix = '@1x';
  5.   else if (scale < 3) suffix = '@2x';
  6.   return {
  7.     uri: `https://cdn.example.com/images/${baseName}${suffix}.png`,
  8.     width: 200,
  9.     height: 200,
  10.   };
  11. };
复制代码

在鸿蒙设备上,PixelRatio可能是3.0或3.5,如果只准备@2x的图片,在@3x的设备上依然会模糊。图片资源至少要覆盖@2x和@3x两档。

getPixelSizeForLayoutSize用于把布局尺寸dp转换为实际像素px。比如要在屏幕上显示一个200x200的图片,而PixelRatio是3,就应该加载600x600的图片源,而不是200x200的。这个API在图片预加载场景中非常实用。
  1. const layoutSize = 100; // dp
  2. const pixelSize = PixelRatio.getPixelSizeForLayoutSize(layoutSize);
  3. // 如果 PixelRatio = 3,pixelSize = 300
复制代码

getFontScale获取用户系统设置的字体缩放比,在鸿蒙上行为跟Android类似。用户调整系统字体大小后,该值会变化。RN的Text组件默认随系统字体自动缩放,但自定义渲染或手动计算布局时,就需要显式处理。之前我做过一个自定义导航栏标题,用position: absolute定位,字号写死18。用户开启大字模式后,标题比其他文字显得特别小,后来改成动态计算就正常了:
  1. const titleFontSize = 18 * PixelRatio.getFontScale();
复制代码

不过鸿蒙上getFontScale有个坑:HarmonyOS 3.0上关闭大字模式时返回1.0,开启后返回1.15或更高;但某些版本即使关闭了大字模式,也可能返回1.05之类的非整数值。依赖它做布局计算时,建议加容差判断:
  1. const fontScale = PixelRatio.getFontScale();
  2. const isDefaultScale = Math.abs(fontScale - 1) < 0.05;
复制代码

roundToNearestPixel用来把dp值对齐到最近的整数物理像素。比如元素宽度8.4dp,在PixelRatio=3的设备上实际是25.2px,屏幕没法显示0.2个像素,系统做亚像素渲染就会显得模糊。对齐到8.33dp后,实际宽度是25px,显示就清晰了:
  1. const aligned = PixelRatio.roundToNearestPixel(8.4);
  2. // 在 PixelRatio=3 的设备上,返回 8.33
  3. // 因为 8.33 * 3 = 25,正好是整数像素
复制代码

实战中,1px细边框是移动端经典需求。直接写borderWidth: 1,在@3x设备上实际是3px物理像素宽,边框看起来很粗。正确做法是用1除以PixelRatio:
  1. const OnePixelBorder = () => {
  2.   const hairlineWidth = 1 / PixelRatio.get(); // 真正的 1px
  3.   return (
  4.     <View style={{ borderBottomWidth: hairlineWidth, borderColor: '#E5E7EB' }}>
  5.       <Text>我是真正的 1px 边框</Text>
  6.     </View>
  7.   );
  8. };
复制代码

图片懒加载场景中,可以结合CDN裁剪参数做尺寸适配。大部分云存储服务(阿里云OSS、AWS S3等)都支持通过URL参数指定图片尺寸,把getPixelSizeForLayoutSize算出的实际像素值传给CDN,返回合适分辨率的图片,清晰又省流量。我在商品列表页用过这个方法,几十个商品图,加载速度和流量都有明显改善。
  1. const AdaptiveImage = ({ uri, styleWidth, styleHeight }) => {
  2.   const pixelWidth = PixelRatio.getPixelSizeForLayoutSize(styleWidth);
  3.   const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(styleHeight);
  4.   // 拼接图片 CDN 的裁剪参数
  5.   const imageUrl = `${uri}?w=${pixelWidth}&h=${pixelHeight}`;
  6.   return <Image source={{ uri: imageUrl }} style={{ width: styleWidth, height: styleHeight }} />;
  7. };
复制代码

横竖屏切换时,PixelRatio不会变,但屏幕逻辑像素会变。可以监听Dimensions变化,结合PixelRatio做适配,横屏时加载更高分辨率版本:
  1. const useAdaptiveLayout = () => {
  2.   const [dimensions, setDimensions] = useState(Dimensions.get('window'));
  3.   useEffect(() => {
  4.     const sub = Dimensions.addEventListener('change', ({ window }) => setDimensions(window));
  5.     return () => sub.remove();
  6.   }, []);
  7.   const isLandscape = dimensions.width > dimensions.height;
  8.   const getImageWidth = (layoutDp) => {
  9.     const baseWidth = isLandscape ? layoutDp * 1.2 : layoutDp;
  10.     return PixelRatio.getPixelSizeForLayoutSize(baseWidth);
  11.   };
  12.   return { dimensions, isLandscape, getImageWidth };
  13. };
复制代码

Canvas绘制同样要乘上PixelRatio。做K线图时我深有体会:坐标不对齐物理像素,线条看起来是虚的,对齐之后清晰多了。自定义绘制时,建议先根据PixelRatio调整Canvas尺寸,再缩放上下文,按逻辑像素绘制,这样实际渲染就是物理像素级精度。

鸿蒙上还有几个容易踩的坑:

一是物理像素计算偏差。用Dimensions.get('window').width * PixelRatio.get()算出来的值,可能跟屏幕真实物理分辨率不一致,因为Dimensions返回的是RN布局尺寸,可能扣除了状态栏、导航栏等区域。需要精确物理分辨率时,建议通过原生模块获取。

二是图片格式兼容性。鸿蒙设备对图片格式的支持跟Android不完全一致,如果用WebP等格式,建议在鸿蒙真机上测试兼容性。

三是模拟器与真机差异。鸿蒙模拟器上PixelRatio可能返回2.0,真机返回3.0。在模拟器上测图片适配没问题,上真机后可能发现图片不够清晰。图片适配必须真机验证。

四是系统“显示大小”调节。HarmonyOS 4.0开始支持“显示大小”调节,会影响DP到PX的换算关系,但PixelRatio.get()返回的值不变。

最后说一个容易被忽略的细节:PixelRatio.get()在设备生命周期内是固定的,用const声明即可,不需要监听变化。有的代码review里见过有人用let声明,还写监听器想去监听密度变化,其实完全没有必要。

总结一下实用的适配建议:图片资源准备@2x和@3x两套,@1x基本用不上;1px细边框用1/PixelRatio而不是写死0.5;dp转px用getPixelSizeForLayoutSize,不要手动乘;需要精确定位的元素用roundToNearestPixel对齐;Canvas绘制必须乘以PixelRatio;图片适配最终要在真机上验证。PixelRatio看着简单,但适配做得好不好,用户一眼就能看出差别,尤其是图片清晰度和细边框这两个点。

本文基于React Native 0.84 + RNOH 0.84.1编写,鸿蒙设备为HarmonyOS 6.0。不同版本之间可能存在差异,以实际测试为准。文中代码示例均已在鸿蒙设备上测试通过。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-1 10:01 , Processed in 0.019981 second(s), 17 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部