做鸿蒙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做精确匹配容易踩坑,建议用范围判断。
- import { PixelRatio } from 'react-native';
- const ratio = PixelRatio.get();
- if (ratio >= 3) {
- // 超高密度屏幕,加载 @3x 图片
- } else if (ratio >= 2) {
- // 高密度屏幕,加载 @2x 图片
- } else {
- // 普通密度,加载 @1x 图片
- }
复制代码
图片加载是最典型的应用场景。如果使用本地require加载图片,RN会自动根据PixelRatio匹配对应资源,只要准备icon@2x.png和icon@3x.png即可。但网络图片必须手动处理,按密度拼接不同的资源地址:
- const getImageSource = (baseName: string) => {
- const scale = PixelRatio.get();
- let suffix = '@3x';
- if (scale < 2) suffix = '@1x';
- else if (scale < 3) suffix = '@2x';
- return {
- uri: `https://cdn.example.com/images/${baseName}${suffix}.png`,
- width: 200,
- height: 200,
- };
- };
复制代码
在鸿蒙设备上,PixelRatio可能是3.0或3.5,如果只准备@2x的图片,在@3x的设备上依然会模糊。图片资源至少要覆盖@2x和@3x两档。
getPixelSizeForLayoutSize用于把布局尺寸dp转换为实际像素px。比如要在屏幕上显示一个200x200的图片,而PixelRatio是3,就应该加载600x600的图片源,而不是200x200的。这个API在图片预加载场景中非常实用。
- const layoutSize = 100; // dp
- const pixelSize = PixelRatio.getPixelSizeForLayoutSize(layoutSize);
- // 如果 PixelRatio = 3,pixelSize = 300
复制代码
getFontScale获取用户系统设置的字体缩放比,在鸿蒙上行为跟Android类似。用户调整系统字体大小后,该值会变化。RN的Text组件默认随系统字体自动缩放,但自定义渲染或手动计算布局时,就需要显式处理。之前我做过一个自定义导航栏标题,用position: absolute定位,字号写死18。用户开启大字模式后,标题比其他文字显得特别小,后来改成动态计算就正常了:
- const titleFontSize = 18 * PixelRatio.getFontScale();
复制代码
不过鸿蒙上getFontScale有个坑:HarmonyOS 3.0上关闭大字模式时返回1.0,开启后返回1.15或更高;但某些版本即使关闭了大字模式,也可能返回1.05之类的非整数值。依赖它做布局计算时,建议加容差判断:
- const fontScale = PixelRatio.getFontScale();
- const isDefaultScale = Math.abs(fontScale - 1) < 0.05;
复制代码
roundToNearestPixel用来把dp值对齐到最近的整数物理像素。比如元素宽度8.4dp,在PixelRatio=3的设备上实际是25.2px,屏幕没法显示0.2个像素,系统做亚像素渲染就会显得模糊。对齐到8.33dp后,实际宽度是25px,显示就清晰了:
- const aligned = PixelRatio.roundToNearestPixel(8.4);
- // 在 PixelRatio=3 的设备上,返回 8.33
- // 因为 8.33 * 3 = 25,正好是整数像素
复制代码
实战中,1px细边框是移动端经典需求。直接写borderWidth: 1,在@3x设备上实际是3px物理像素宽,边框看起来很粗。正确做法是用1除以PixelRatio:
- const OnePixelBorder = () => {
- const hairlineWidth = 1 / PixelRatio.get(); // 真正的 1px
- return (
- <View style={{ borderBottomWidth: hairlineWidth, borderColor: '#E5E7EB' }}>
- <Text>我是真正的 1px 边框</Text>
- </View>
- );
- };
复制代码
图片懒加载场景中,可以结合CDN裁剪参数做尺寸适配。大部分云存储服务(阿里云OSS、AWS S3等)都支持通过URL参数指定图片尺寸,把getPixelSizeForLayoutSize算出的实际像素值传给CDN,返回合适分辨率的图片,清晰又省流量。我在商品列表页用过这个方法,几十个商品图,加载速度和流量都有明显改善。
- const AdaptiveImage = ({ uri, styleWidth, styleHeight }) => {
- const pixelWidth = PixelRatio.getPixelSizeForLayoutSize(styleWidth);
- const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(styleHeight);
- // 拼接图片 CDN 的裁剪参数
- const imageUrl = `${uri}?w=${pixelWidth}&h=${pixelHeight}`;
- return <Image source={{ uri: imageUrl }} style={{ width: styleWidth, height: styleHeight }} />;
- };
复制代码
横竖屏切换时,PixelRatio不会变,但屏幕逻辑像素会变。可以监听Dimensions变化,结合PixelRatio做适配,横屏时加载更高分辨率版本:
- const useAdaptiveLayout = () => {
- const [dimensions, setDimensions] = useState(Dimensions.get('window'));
- useEffect(() => {
- const sub = Dimensions.addEventListener('change', ({ window }) => setDimensions(window));
- return () => sub.remove();
- }, []);
- const isLandscape = dimensions.width > dimensions.height;
- const getImageWidth = (layoutDp) => {
- const baseWidth = isLandscape ? layoutDp * 1.2 : layoutDp;
- return PixelRatio.getPixelSizeForLayoutSize(baseWidth);
- };
- return { dimensions, isLandscape, getImageWidth };
- };
复制代码
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。不同版本之间可能存在差异,以实际测试为准。文中代码示例均已在鸿蒙设备上测试通过。 |