前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

React Native图片宽高与字体的平台适配实战

首页2019-10-02 12:50:12Front-End
RNReact移动端适配

同一份 React Native 代码,装到 iPhone 上标题栏顶进了状态栏,换到一台 720p 的老安卓上,字又小得看不清。这类问题在模拟器上很难一次暴露干净,你手边能开的分辨率就那么几种,真机一到就全冒出来了。适配这事说难不难,麻烦在于它散在四五个地方,平台判断、API 的双端支持、图标倍图、字号和间距的换算,漏掉任意一块都会露馅。

这篇把这几块串成一条线讲清楚,从最基础的 Platform.OS 一直到基于 PixelRatio 的等比换算工具,最后还会把老工具函数里一处一直没被发现的判断错误改掉。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 为什么 RN 写了一遍代码还是要做双端适配,适配到底在补哪个缺口
  • 用 Platform.OS 和 Platform.select 做平台分支,以及它们的取舍
  • 怎么读懂官方 API 文档里那些 android / ios 前缀标记
  • 组件选型时该怎么判断一个组件是不是真的双端可用
  • 图标的 1x / 2x / 3x 倍图规则,以及引用时最容易犯的那个错
  • 用 PixelRatio + Dimensions 写一套字号和宽高的等比换算工具
  • 老版工具函数里那处 PixelRatio === 2 的判断为什么永远不成立
  • 这套 2019 年的写法放到现在,哪些该换、哪些还能用

# 一、RN 的适配到底在补什么缺口

先把概念理顺,后面的 API 才不会记成八股。

React Native 里写的所有尺寸数字都是无单位的。你写 width: 100,iOS 上它是 100pt,Android 上它是 100dp。这两个单位背后是同一个思路,就是「与设备无关的逻辑像素」,系统会拿设备的像素密度把它换算成真正的物理像素。所以你不需要像写 CSS 那样纠结 px 还是 rem,RN 已经帮你抹掉了一层。

那既然抹掉了,为什么还要适配呢?

因为 RN 只抹平了「像素密度」,没抹平另外三件事。一是屏幕的逻辑宽高本身不一样,iPhone SE 是 320pt 宽,iPhone 11 Pro Max 是 414pt 宽,同样写 width: 300 在两台机器上占的视觉比例差了一大截。二是两个系统的原生行为不一样,比如 iOS 的根视图默认是顶到状态栏下面的,Android 不是。三是 API 本身就有单端独占的部分。

所以适配要干的活其实就三样,判断平台、按比例换算、给资源准备多套倍图。下面一个个说。

# 二、Platform.OS 做平台分支

最直接的一招就是运行时判断当前平台。原文里那个状态栏的例子非常典型。

在 iOS 上,如果你没有用 SafeAreaView 之类的容器包一层,根视图默认会占据状态栏的位置,导航栏的内容就会和时间、电量图标糊在一起。Android 上系统会自己留出状态栏的空间,不需要你补。于是就有了这种写法,给 StatusBar 的外层容器按平台设一个高度。

<View style={{height: Platform.OS === 'ios' ? 20:0}}>
    <StatusBar {...this.props.statusBar} />
</View>;
@前端进阶之旅: 代码已经复制到剪贴板

这里的 20 是那个年代 iOS 状态栏的经典高度。这个数字现在已经不能写死了,后面第八节会展开说。

如果分支不止一个属性,用三元表达式会很快糊掉。Platform.select 更适合成块的样式,它接收一个以平台名为键的对象,返回当前平台对应的那份。

import { Platform } from 'react-native'

const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: { paddingTop: 20, shadowOpacity: 0.2 },
      android: { paddingTop: 0, elevation: 4 }
    })
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

这个写法的好处是把两端的差异集中在一处,别人接手时一眼能看出「这里两端不一样」。散在各处的三元判断就没这个效果了。

还有一种更彻底的做法是按文件名分平台,同一个组件写成 Toast.ios.js 和 Toast.android.js,引用时只写 import Toast from './Toast',打包器会自动挑对应平台的那份。差异大到样式分支已经写不动的时候,用这招比在一个文件里塞满 if 干净得多。

# 三、留意 API 文档里的 android 和 ios 标记

并不是所有 React Native 的 API 或组件属性都同时兼容两端。官方文档在这类属性和方法前面会加上平台前缀。

android renderToHardwareTextureAndroid bool
ios shouldRasterizeIOS bool
@前端进阶之旅: 代码已经复制到剪贴板

上面这两行,renderToHardwareTextureAndroid 只在 Android 上生效,shouldRasterizeIOS 只在 iOS 上生效。它俩其实是同一件事的两端实现,都是把视图提升成一层独立的纹理交给 GPU,做动画时能省掉重复的重绘。

这里有个坑要注意。这类单端属性写到另一端上通常不会报错,只是静默失效。你在 Android 上加了 shouldRasterizeIOS 想优化动画,跑起来一点变化都没有,还以为是自己参数没调对,实际上这个属性压根没被消费。查这类问题最快的方式就是回文档看前缀,比在代码里瞎试快得多。

同理,某些方法在两端都存在但行为不同。Alert 的按钮数量、StatusBar 的部分属性、Linking 能识别的 scheme,都属于这一类。写之前扫一眼文档下方的平台说明,能省不少事。

# 四、组件选型先看双端支持

2019 年那会儿导航组件是个典型例子。当时 RN 里同时存在 NavigatorIOS 和 Navigator 两个选择,从文档能看出 NavigatorIOS 只支持 iOS,Navigator 两端都支持。要做双端应用,Navigator 才是那个能用的。

选组件的判断标准我一般看三条。第一条是文档或 README 里有没有明确写双端支持,只写了一端的直接排除。第二条是看仓库的 example 工程里有没有 android 目录,只有 ios 目录的多半是单端库。第三条是翻 issue 列表搜一下另一端的关键字,很多库名义上支持双端,实际上另一端的 bug 挂了一年没人修。

我自己的感受是,第三条最有用,也最容易被跳过。

顺带说一句时效性。NavigatorIOS 和 Navigator 这两个组件在后续版本里都已经被移除了,现在做导航基本都用 React Navigation 这类社区方案。原文这一节的结论依然成立,只是例子换了主角,「选组件前先确认双端支持」这条判断标准并没有过时。

# 五、图片适配用 1x 2x 3x 三套图

无论 Android 还是 iOS,现在不同分辨率的设备越来越多,图标要在这些设备上都不糊,就得为每个图标准备 1x、2x、3x 三种尺寸。React Native 会根据屏幕的像素密度动态挑选合适的那张。

目录结构长这样。

└── img
    ├── check.png
    ├── check@2x.png
    └── check@3x.png
@前端进阶之旅: 代码已经复制到剪贴板

引用的时候只写标准分辨率那一张。

<Image source={require('./img/check.png')} />
@前端进阶之旅: 代码已经复制到剪贴板

这里就是最容易犯错的地方了。如果你写成 require('./img/check@2x.png'),那么应用在所有设备上都只会加载 check@2x.png,自动挑选的机制直接失效。在 2x 屏上看不出问题,到了 3x 屏上图标就开始发虚,而且这种糊法很轻微,不对比着看根本发现不了。

原因不复杂。RN 的打包器在解析 require 的时候,会把 check.png、check@2x.png、check@3x.png 归成同一个资源的三个变体,运行时按 PixelRatio 挑一个。你直接指名 @2x,等于告诉它「这就是一个独立资源」,变体关系就断了。

所以规则很简单,文件按 @2x @3x 命名放好,代码里永远只引用不带后缀的那个名字。

# 六、字体和宽高的等比换算

前面几节解决的是「有没有」的问题,这一节解决「大小对不对」的问题。

设计稿一般按某个固定宽度出,iOS 常见的是 750,Android 早年常见 720。你拿到的标注是设计稿上的物理像素,要变成代码里的逻辑像素,就得按当前屏幕宽度做一次等比缩放。原文的做法是封装两个工具函数,一个管字号,一个管宽高间距。

先看字号这个。

// utils/FontSize.js
import { PixelRatio, Dimensions } from 'react-native';

let { width, height } = Dimensions.get('window');

export let FontSize = (size) => {
  if (PixelRatio === 2) {
    // iphone 5s and older Androids
    if (width < 360) {
      return size * 0.95;
    }
    // iphone 5
    if (height < 667) {
      return size;
      // iphone 6-6s
    } else if (height >= 667 && height <= 735) {
      return size * 1.15;
    }
    // older phablets
    return size * 1.25;
  }
  if (PixelRatio === 3) {
    // catch Android font scaling on small machines
    // where pixel ratio / font scale ratio => 3:3
    if (width <= 360) {
      return size;
    }
    // Catch other weird android width sizings
    if (height < 667) {
      return size * 1.15;
      // catch in-between size Androids and scale font up
      // a tad but not too much
    }
    if (height >= 667 && height <= 735) {
      return size * 1.2;
    }
    // catch larger devices
    // ie iphone 6s plus / 7 plus / mi note 等等
    return size * 1.27;
  }
  // if older device ie pixelRatio !== 2 || 3 || 3.5
  return size;
};
@前端进阶之旅: 代码已经复制到剪贴板

这段的思路是按像素密度分档,再在每档里按屏幕宽高细分,给出一个手工调过的缩放系数。相比纯线性缩放,它的好处是字号不会在大屏上被放得过大,毕竟大屏用户的阅读距离没变。

但这段代码有个致命问题,我当时也是排查了半天才看出来。

PixelRatio 是从 react-native 里 import 进来的模块对象,拿它去和数字 2 做 === 比较,永远是 false。也就是说上面三个 if 分支一个都进不去,函数不管传什么都直接走到最后 return size,等于什么都没做。要拿到真正的像素密度,得调 PixelRatio.get()。

改法是这样。

fe
  • 一、RN 的适配到底在补什么缺口
  • 二、Platform.OS 做平台分支
  • 三、留意 API 文档里的 android 和 ios 标记
  • 四、组件选型先看双端支持
  • 五、图片适配用 1x 2x 3x 三套图
  • 六、字体和宽高的等比换算
  • 七、这套写法放到现在
  • 总结
  • 参考

← React Native启动流程源码拆解从MainActivity到第一帧React Native相机扫码实战从权限配置到扫描线动画 →