乐趣区

关于前端:聊聊React-Native屏幕适配那些事儿

写在后面

在我从事 React Native(以下简称 RN)开发的两年工作中,本人与团队成员时常会遇到一些令人纳闷的屏幕适配问题,如:全屏 mask 款式无奈笼罩整个屏幕、1 像素边框有时无奈显示、非凡机型布局错乱等。另外,局部成员对 RN 获取屏幕参数的 API——Dimensions.get('window')Dimensions.get('screen') 最终返回的值代表的意义也存在纳闷。
其实 RN 的适配比较简单,我将在此文中论述适配原理,提出适配计划,并针对局部非凡问题一一解释其起因,原则上能笼罩所有机型的适配。若有脱漏与不当之处,欢送指出,独特交换。

往期精彩 RN 文章举荐:
-【从源码剖析】可能是全网最实用的 React Native 异样解决方案【倡议珍藏】

适宜浏览群体

  • 有肯定 RN 开发教训,理解 RN js 模块如何与原生模块通信;
  • 有 RN 适配教训,懂了,但没齐全懂的那种;
  • 想理解 RN 适配;

为什么须要适配

保障界面在不同的设施屏幕上都能按设计图成果展现,对立用户视觉与操作体验

常见适配名词论述

如果你从网上去搜屏幕适配,你搜到的博文中肯定都会有以下一大堆名词及其解释

  1. 适配:不同屏幕下,元素显示成果统一
  2. 屏幕尺寸:指的是屏幕对角线的长度
  3. px(单位): px 理论是 pixel(像素)的缩写,依据 维基百科的解释,它是图像显示的根本单元,既不是一个确定的物理量,也不是一个点或者小方块,而是一个抽象概念。所以在议论像素时肯定要分明它的上下文!
  4. 分辨率:是指宽度上和高度上最多能显示的物理像素点个数
  5. 设施物理像素:指设施能管制显示的最小物理单位,意指显示器上一个个的点。从屏幕在工厂生产出的那天起,它下面设施像素点就固定不变了,和屏幕尺寸大小无关
  6. 设施独立像素(设施逻辑像素):计算机坐标零碎中得一个点,这个点代表一个能够由程序应用的虚构像素(比方: css 像素),这个点是没有固定大小的,越小越清晰,而后由相干零碎转换为物理像素
  7. CSS 像素:css px 和物理像素的对应关系, 与 viewport 的缩放无关 scale = 1/dpr 时 1px 对应一个 物理像素
  8. DPI:打印设施印刷点密度。每 inch 多少个点
  9. PPI:设施物理像素密度。每 inch 多少个物理像素
  10. DPR:设施像素比 = 设施物理像素 / 设施独立像素(CSS 像素)

看完这些名词后大多数人的感觉:懂了,但没齐全懂 \~
咱们先遗记这些名词概念,只记住以下 4 个概念:

  • 适配:不同屏幕下,元素显示成果统一
  • 设施独立像素 = 设施逻辑像素 =CSS 像素
  • DPR:设施像素比 = 设施物理像素 / 设施独立像素(CSS 像素)
  • 设计图与编码中的尺寸都是 CSS 像素

OK,上面,正菜开始!客官们请跟我这边来。


RN 的尺寸单位

要做 RN 适配得先明确 RN 款式的尺寸单位。
在 RN 的官网有明确标注:

All dimensions in React Native are unitless, and represent density-independent pixels.
React Native 中的尺寸都是无单位的,示意的是与设施像素密度无关的逻辑像素点。

为什么是无单位的逻辑像素点呢?

因为 RN 是个跨平台的框架,在 IOS 上通常以逻辑像素单位 pt 形容尺寸,在 Android 上通常以逻辑像素单位 dp 形容尺寸,RN 选哪个都不好,既然大家意思雷同,罗唆不带单位,在哪个平台渲染就默认用哪个单位。
RN 提供给开发者的就是曾经通过 DPR(设施像素比)转换过的逻辑像素尺寸,开发者无需再关怀因为设施 DPR 不同引起的尺寸数值计算问题
在有些博文中,会提到 RN 曾经做好了适配,其实指的就是这个意思。

适配计划

留神:本文示例与形容中设计图尺寸规范都为 375X667(iPhone6/7/8)

对于 RN 适配,我总结为以下 口诀:
一理念,一像素,一比例;
部分盒子全副按比例;
遇到整页布局垂直方向弹一弹;
安卓须要解决状态栏。

一理念

适配就是不同屏幕下,元素显示成果统一的理念
怎么了解呢?
举个栗子:
假如有一个元素在 375X667 的设计图上标注为 375X44,即宽度占满整个屏幕,高度 44px。如果咱们做好了 RN 的屏幕适配,那么:
在 iPhone 6/7/8(375X667)机型与 iPhone X(375X812)机型上,此元素渲染后果会占满屏幕 宽度
在 iPhone 6/7/8 Plus(414X736)机型上, 此元素渲染后果也应占满屏幕 宽度


打个现实生活中的比如:
联合国依据恩格尔系数的大小,对世界各国的生存程度有一个划分规范,即一个国家均匀家庭恩格尔系数大于 60% 为贫困;50%-60% 为饥寒;40%-50% 为小康;30%-40% 属于绝对富裕;20%-30% 为富足;20% 以下为极其富裕。
假如要实现小康生活,不论你是哪个国家的人民,发达国家也好,发展中国家也好,家庭的恩格尔系数都必须达到 40%-50%。
这里,国家就能够了解为手机屏幕、生存程度就了解为元素渲染成果。
至于上述的一些名词,如:物理像素,像素比等,你能够了解为国家的货币以及货币汇率。毕竟,程序设计源自生存。

那么,正在搬砖的你,小康了吗~?

一像素

RN style 中所有的尺寸,包含但不限于 width、height、margin、padding、top、left、bottom、right、fontSize、lineHeight、transform 等都是 逻辑像素(web 玩家能够了解为 css 像素)

  h3: {
    color: '#4A4A4A',
    fontSize: 13,
    lineHeight: 20,// 逻辑像素
    marginLeft: 25,
    marginRight: 25,
  },

一比例

设施逻辑像素宽度比例
为了更好的视觉与用户操作体验,目前风行的挪动端适配计划,在大小上都是进行宽度适配,在布局上垂直方向自在排列。这样做的益处是:保障在页面上元素大小都是按设计图进行等比例缩放,内容恰好只能铺满屏幕宽度;垂直方向上内容如果超出屏幕,能够通过手指上滑下拉查看页面更多内容。
当然,如果你想走非凡路子,设计成高度适配,程度方向滑动也是能够的。
回到下面“一理念”的例子,在 iPhone 6/7/8 Plus(414X736)机型上,渲染一个设计图 375 尺寸元素的话,很容易计算出,咱们理论要设置的宽度应为:375 * 414/375 = 414。
这里的 414/375 就是 设施逻辑像素宽度比例

公式: WLR = 设施宽度逻辑像素 / 设计图宽度

WLR(width logic rate 缩写),散装英语,哈哈。
在这里,设施的宽度逻辑像素我倡议用 Dimensions.get('window').width获取,具体原因,前面会进行解释。[Q1]

那么,在指标设施上要设置的尺寸计算公式就是:
size = 设置图上元素 size * WLR
小学四则运算,非常简单!
其实所有的适配都是围绕一个 比例 在做,如 web 端缩放、rem 适配、postcss plugin 等,小道万千,必由之路!

部分盒子全副按比例

为了不便了解,这里的“盒子”意思等同于 web 中的“盒模型”。

部分盒子全副按比例。意思就是 RN 页面中的元素大小、地位、内外边距等波及尺寸的中央,全副按上述一比例中的尺寸计算公式进行计算。如下图所示:

这样渲染进去的成果,会最大限度的保留设计图的大小与布局设计成果。

为什么说是最大限度,这里先留做一个问题,后文中解释。[Q2]

到这里,可能有老手同学会问:为什么在垂直方向上不必设施高度逻辑像素比例进行计算?
因为 设施高度逻辑像素 / 设计图高度 不肯定会等于 设施宽度逻辑像素 / 设计图宽度 ,会引起盒子拉伸。
比方,当初依照设计图在 iPhone X(375X812)上渲染一个 100X100px 的正方形盒子,宽度逻辑像素比例是 1,高度逻辑像素比例是 812/667≈1.22,如果宽度与高度别离按后面的 2 个比例计算,那么最终盒模型的 size 会变成:

  view1: {
    width: 100,
    height: 122,
  },


好嘛,好好的一个正方形被拉伸成长方形了!

这显然是要不得的。
讲到这里,RN 适配其实曾经实现 70% 了,对,就是玩乘除法~

遇到整页布局垂直方向弹一弹

何为整页布局?

内容刚好铺满整页,没有溢出屏幕外。

这里的弹一弹,指的是 flex 布局。在 RN 中,默认都是 flex 布局,并且方向是 column,从上往下布局。
为啥要弹一弹呢?
咱们先来看挪动端页面布局常见的整页上中下分区布局设计,以 TCL IOT 单品旧版 UI 设计为例:

依照设计,在 iPhone 6/7/ 8 机型 (375X667) 上恰好铺满整页,在 iPhone 6/7/ 8 机型 plus(414X736)机型上根据上述的适配办法,其实也是近似铺满的,因为 414/375≈736/667。然而,在 iPhone X(375X812)机型上,如果依照设计图从上往下布局,会呈现底下空出一截的状况:

此时有两种解决办法:

  1. 底部 - 操控菜单栏区域应用相对定位 bottom:0 固定在底部,最顶部 - 状态栏 + 标题栏是固定在顶部的,不须要解决,而后计算并用相对定位微调顶部 - 设施信息展示区,中部 - 设施状态区的地位,使它们恰好平分多进去的空白空间,让页面看起来更加协调;
  2. 顶部 - 设施信息展示区,中部 - 设施状态区,底部 - 操控菜单栏区域应用父容器包裹,利用 RN flex 弹性布局的个性,设置 justifyContent:'space-between' 使得这 3 个区域垂直方向高低两端对齐,两头区域高低平分多进去的空白区域。
    第 1 种,每个设施都须要去计算空白区域大小,再去微调元素地位,非常麻烦。
    我举荐第 2 种,编码上更加简略。这就是 “弹一弹”
    有同学会放心第 2 种形式会导致两头区域垂直方向上跨度十分大,页面看起来不协调。然而在理论中,设施屏幕高度逻辑像素很少会有比 667 大十分多的,多出的空白区域比拟小,UI 成果还是能够的,目前咱们上线的 N 款产品中也都是应用的这种形式,请释怀食用。


到此为止,如果依照以往 web 端的适配教训,RN 适配应该曾经实现了,然而,还是有坑的。

安卓须要解决状态栏

RN 尽管是跨平台的,然而在 ios 与 Android 上渲染成果却不一样。最显著的就是状态栏了。如下图所示:

Android 在不设置 StatusBartranslucent属性为 true 时,是从状态栏下方开始绘制的。这与咱们的适配指标不吻合,因为在咱们的设计图中,整页的布局设计是笼罩了状态栏的。所以,倡议将 Android 的状态栏 translucent属性设为true,整个页面交给咱们开发者本人去布局。

<StatusBar translucent={true} />

如果你曾经看到这里,祝贺你,同学,把握 RN 的适配了,能够应答 90% 以上的场景。


然而还有一些奇奇怪怪的场景以及一些 API 你可能不太了解,这蕴含在剩下的 10% 适配场景中或在其中帮忙你了解与调试,没关系,我上面持续论述。有些会波及到源码,如果你有趣味,能够持续跟我看上来。


上面的内容十分十分多,然而对我集体而言,这部分才是我此次分享,想带给大家的最重要的局部。

一些奇奇怪怪又有意思的货色

这部分内容十分多,请酌情浏览

1. DimensionsAPI

Dimensions是 RN 提供的一个获取 设施 尺寸信息的 API。咱们能够用它来获取屏幕的宽高,这是做适配的 外围 API
它提供了两种获取形式:

const {windowWidth,windowHeight} = Dimensions.get('window');
const {screenWidth,screenHeight} = Dimensions.get('screen');

官网文档上并没有阐明这两种获取形式的后果的含意与区别是什么。在理论开发中,这两种形式获取的后果有时雷同,有时又有差别,让局部同学感到困惑:我到底该应用哪一个才是正确的?
我举荐你始终应用 Dimensions.get('window')。只有通过它获取的后果,才是咱们真正能够操控绘制的区域。
首先,明确这两种形式获取的后果的含意:

  • Dimensions.get('window')——获取视口参数 width、height、scale、fontScale
  • Dimensions.get('screen')——获取屏幕参数 width、height、scale、fontScale
    其中,在设施屏幕同状态的默认状况下 screen 的 width、height 永远是≥window 的 width、height,因为,window 获取的参数会排除掉状态栏高度(translucent 为 false 时)以及底部虚构菜单栏高度。 当此安卓机设置了状态栏 translucenttrue并且没有开启虚构菜单栏时,Dimensions.get('window')就会与 Dimensions.get('screen') 获取的 width、height 统一,否则就不同。这就是本段开始时有时雷同,有时又有差别的问题的答案。


    这并非靠猜测或空穴来风,间接源码安顿上:

    因作者设施无限,本文源码仅从 Android 平台剖析,ios 的源码,有 ios 教训的同学能够依照思路自行查阅。
    筹备:依照官网文档新建一个 Demo RN 工程。为了稳定性,咱们应用后面的一个 RN 版本 0.62.0。命令如下:
    npx react-native init Demo --version 0.62.0

step1. 先找到 RN 的该 API 的 js 文件。node_modules\react-native\Libraries\Utilities\Dimensions.js

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

import EventEmitter from '../vendor/emitter/EventEmitter';
import RCTDeviceEventEmitter from '../EventEmitter/RCTDeviceEventEmitter';
import NativeDeviceInfo, {
  type DisplayMetrics,
  type DimensionsPayload,
} from './NativeDeviceInfo';
import invariant from 'invariant';

type DimensionsValue = {
  window?: DisplayMetrics,
  screen?: DisplayMetrics,
  ...
};

const eventEmitter = new EventEmitter();
let dimensionsInitialized = false;
let dimensions: DimensionsValue;

class Dimensions {
  /**
   * NOTE: `useWindowDimensions` is the preffered API for React components.
   *
   * Initial dimensions are set before `runApplication` is called so they should
   * be available before any other require's are run, but may be updated later.
   *
   * Note: Although dimensions are available immediately, they may change (e.g
   * due to device rotation) so any rendering logic or styles that depend on
   * these constants should try to call this function on every render, rather
   * than caching the value (for example, using inline styles rather than
   * setting a value in a `StyleSheet`).
   *
   * Example: `const {height, width} = Dimensions.get('window');`
   *
   * @param {string} dim Name of dimension as defined when calling `set`.
   * @returns {Object?} Value for the dimension.
   */
  static get(dim: string): Object {invariant(dimensions[dim], 'No dimension set for key' + dim);
    return dimensions[dim];
  }

  /**
   * This should only be called from native code by sending the
   * didUpdateDimensions event.
   *
   * @param {object} dims Simple string-keyed object of dimensions to set
   */
  static set(dims: $ReadOnly<{[key: string]: any, ...}>): void {
    // We calculate the window dimensions in JS so that we don't encounter loss of
    // precision in transferring the dimensions (which could be non-integers) over
    // the bridge.
    let {screen, window} = dims;
    const {windowPhysicalPixels} = dims;
    if (windowPhysicalPixels) {
      window = {
        width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
        height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
        scale: windowPhysicalPixels.scale,
        fontScale: windowPhysicalPixels.fontScale,
      };
    }
    const {screenPhysicalPixels} = dims;
    if (screenPhysicalPixels) {
      screen = {
        width: screenPhysicalPixels.width / screenPhysicalPixels.scale,
        height: screenPhysicalPixels.height / screenPhysicalPixels.scale,
        scale: screenPhysicalPixels.scale,
        fontScale: screenPhysicalPixels.fontScale,
      };
    } else if (screen == null) {screen = window;}

    dimensions = {window, screen};
    if (dimensionsInitialized) {
      // Don't fire'change' the first time the dimensions are set.
      eventEmitter.emit('change', dimensions);
    } else {dimensionsInitialized = true;}
  }

  /**
   * Add an event handler. Supported events:
   *
   * - `change`: Fires when a property within the `Dimensions` object changes. The argument
   *   to the event handler is an object with `window` and `screen` properties whose values
   *   are the same as the return values of `Dimensions.get('window')` and
   *   `Dimensions.get('screen')`, respectively.
   */
  static addEventListener(type: 'change', handler: Function) {
    invariant(
      type === 'change',
      'Trying to subscribe to unknown event:"%s"',
      type,
    );
    eventEmitter.addListener(type, handler);
  }

  /**
   * Remove an event handler.
   */
  static removeEventListener(type: 'change', handler: Function) {
    invariant(
      type === 'change',
      'Trying to remove listener for unknown event:"%s"',
      type,
    );
    eventEmitter.removeListener(type, handler);
  }
}

let initialDims: ?$ReadOnly<{[key: string]: any, ...}> =
  global.nativeExtensions &&
  global.nativeExtensions.DeviceInfo &&
  global.nativeExtensions.DeviceInfo.Dimensions;
if (!initialDims) {
  // Subscribe before calling getConstants to make sure we don't miss any updates in between.
  RCTDeviceEventEmitter.addListener(
    'didUpdateDimensions',
    (update: DimensionsPayload) => {Dimensions.set(update);
    },
  );
  // Can't use NativeDeviceInfo in ComponentScript because it does not support NativeModules,
  // but has nativeExtensions instead.
  initialDims = NativeDeviceInfo.getConstants().Dimensions;}

Dimensions.set(initialDims);

module.exports = Dimensions;

这个 Dimensions.js 模块初始化了 Dimensions 参数信息,咱们的 Dimensions.get()办法就是获取的其中的信息。并且,该模块指出了信息的起源:

//...
initialDims = NativeDeviceInfo.getConstants().Dimensions;
//...
Dimensions.set(initialDims);
let {screen, window} = dims
const {windowPhysicalPixels} = dims
const {screenPhysicalPixels} = dims
//...
dimensions = {window, screen};

数据起源是来自原生模块中的 DeviceInfo module。
好嘛,咱们间接去找安卓源码,看看它提供的是啥玩意儿。

step2: 从 node_modules\react-native\android\com\facebook\react\react-native\0.62.0\react-native-0.62.0-sources.jar 中取到安卓源码 jar 包。

下载下来,保留到本地。
step3: 应用工具 java decompiler 反编译 react-native-0.62.0-sources.jar:

能够看到,有很多 package。咱们直奔 com.facebook.react.modules,这个模块是原生为 RN jsc 提供的绝大部分 API 的中央。

step4: 关上 com.facebook.react.modules.deviceinfo.DeviceInfoModule.java:

看图中红色方框标记的中央,就是在上述 js 中模块中

initialDims = NativeDeviceInfo.getConstants().Dimensions; 

设施的初始尺寸信息来源于此。
step5: 关上 DisplayMetricsHolder.java, 找到 getDisplayMetricsMap() 办法:

怎么样,windowPhysicalPixels & screenPhysicalPixels是不是很相熟? 而它们的属性字段 widthheightscalefontScaledensityDpi 等是不是常常用过一部分?没错,你在开始的 Dimensions.js 中见过它们:

严格来说,Dimensions.js还漏了个densityDpi(设施像素密度)没有解构进去~
ok,那咱们看它们最开始的数据起源:

result.put("windowPhysicalPixels", getPhysicalPixelsMap(sWindowDisplayMetrics, fontScale));
result.put("screenPhysicalPixels", getPhysicalPixelsMap(sScreenDisplayMetrics, fontScale));

别离来自:
sWindowDisplayMetricssScreenDisplayMetrics
其中,sWindowDisplayMetrics 通过

DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
DisplayMetricsHolder.setWindowDisplayMetrics(displayMetrics);

设置;
sScreenDisplayMetrics通过

 DisplayMetrics screenDisplayMetrics = new DisplayMetrics();
 screenDisplayMetrics.setTo(displayMetrics);
 WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
 Assertions.assertNotNull(wm, "WindowManager is null!");
 Display display = wm.getDefaultDisplay();
 // Get the real display metrics if we are using API level 17 or higher.
 // The real metrics include system decor elements (e.g. soft menu bar).
 //
 // See:
 // http://developer.android.com/reference/android/view/Display.html#getRealMetrics(android.util.DisplayMetrics)
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {display.getRealMetrics(screenDisplayMetrics);
 } else {
 // For 14 <= API level <= 16, we need to invoke getRawHeight and getRawWidth to get the real
 // dimensions.
 // Since react-native only supports API level 16+ we don't have to worry about other cases.
 //
 // Reflection exceptions are rethrown at runtime.
 //
 // See:
 // http://stackoverflow.com/questions/14341041/how-to-get-real-screen-height-and-width/23861333#23861333
 try {Method mGetRawH = Display.class.getMethod("getRawHeight");
 Method mGetRawW = Display.class.getMethod("getRawWidth");
 screenDisplayMetrics.widthPixels = (Integer) mGetRawW.invoke(display);
 screenDisplayMetrics.heightPixels = (Integer) mGetRawH.invoke(display);
 } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {throw new RuntimeException("Error getting real dimensions for API level < 17", e);
 }
 }
 DisplayMetricsHolder.setScreenDisplayMetrics(screenDisplayMetrics);

设置。
在安卓中 context.getResources().getDisplayMetrics();只会获取可绘制区域尺寸信息,默认会去除顶部状态栏以及底部虚构菜单栏;而设置 screenDisplayMetrics 时,尽管有去辨别版本,但最终都是获取的整个屏幕的物理分辨率。
因而,能够真正有理有据的解释结尾的状况了。并且完完全全从 js 层到原生层讲述了DimensionsAPI,好吧,讲这一个就啰里啰嗦的了,各位看官明确了吗?

全屏 mask 款式无奈笼罩整个屏幕

这个问题呈现在局部老旧安卓机上,大略在 2016~2018 年左右的中低端机型,光荣机型居多。这类手机自带底部虚构菜单栏,并且在应用时能够主动 / 手动暗藏。
问题情境:
当弹出一个带 mask 的自定义 Modal 时,如果设置了 mask 高度是 Dimensions.get(‘window’).height,在暗藏底部虚构菜单栏后,底部会空出一截无奈被 mask 遮罩。
问题起因:
暗藏菜单栏后,页面可绘制区域高度曾经产生了变动,而目前所渲染的视图还是上一次未暗藏菜单栏状态下的。
解决方案:
监听屏幕状态变动,这一点官网其实曾经特地指出了 (https://www.react-native.cn/d…
应用 Dimensions.addEventListener()监听并设置 mask 高度,重点是要扭转 state,通过 state 驱动视图更新。
当然,也要记得移除事件监听
Dimensions.removeEventListener()

1 像素边框有时无奈显示

RN 的 1 像素边框,通常是指:
StyleSheet.hairlineWidth
它是一个常量,渲染成果会合乎以后平台最细的规范。
然而,在列表子项中设置时,常常会有局部列表子项失落这根线,而且诡异的是,同一根线,有些手机显示失常,有些手机不显示,甚至有些机型上线条会比拟“胖”。

老规矩,源码搬一搬:
在 node_modules\react-native\Libraries\StyleSheet\StyleSheet.js 中能够找到:

let hairlineWidth: number = PixelRatio.roundToNearestPixel(0.4);
if (hairlineWidth === 0) {hairlineWidth = 1 / PixelRatio.get();
}

而后在 node_modules\react-native\Libraries\Utilities\PixelRatio.js 中找到:

  /**
   * Rounds a layout size (dp) to the nearest layout size that corresponds to
   * an integer number of pixels. For example, on a device with a PixelRatio
   * of 3, `PixelRatio.roundToNearestPixel(8.4) = 8.33`, which corresponds to
   * exactly (8.33 * 3) = 25 pixels.
   */
  static roundToNearestPixel(layoutSize: number): number {const ratio = PixelRatio.get();
    return Math.round(layoutSize * ratio) / ratio;
  }

这原理就是渲染一条 0.4 逻辑像素左右的线,值不肯定是 0.4,要依据 roundToNearestPixel 换算成最能占据整数个物理像素的一个值,与设施 DPR 无关,也是上述 Dimensions中的 scale 属性值。最差的状况就是在 DPR 小于 1.25 时,等于 1 / PixelRatio.get()
依照下面的规定计算,再怎么样,总归还是应该会显示的。然而,这里咱们要先引入 2 个概念——像素网格对齐 以及JavaScript number 精度:

咱们在设置逻辑像素时能够任意指定精度,然而设施渲染时,理论是按一个一个的物理像素显示的,物理像素永远整个的。为了能保障在任意精度的状况也能正确显示,RN 渲染时会做像素网格对齐;
JavaScript 没有真正意义上的整数。它的数字类型是基于 IEEE 754 规范实现的,采纳的 64 位二进制的“双精度”格局。数值之间会存在一个 “机器精度” 误差,通常是Math.pow(2,-52).

概念说完,咱们来看例子:

假如当初有个 DPR=1.5 的安卓机,在页面高低渲染 2 个 height = StyleSheet.hairlineWidth 的 View, 依照下面计算规定,此时 height = StyleSheet.hairlineWidth≈0.66666667, 现实状况占据 1px 物理像素。但理论状况可能是:

因为 js 数字精度问题,Math.round(0.4 * 1.5) / 1.5 再乘 1.5 不肯定等于 1,有可能是大于 1,有可能是小于 1,当然,也可能等于 1。
感觉困惑吗?
给你看一道常见面试题咯:
0.1+0.2 === 0.3 // false
怎么样?明确了吗?哈哈
而物理像素是整个的,大于 1 时,会占据 2 个物理像素,小于 1 时可能占据 1 个也可能不占据,等于 1 时,失常显示。这就是像素网格对齐,导致设置 StyleSheet.hairlineWidth 显示呈现了 3 种状况:

  • 显示比预期要粗;
  • 显示失常;
  • 不显示;

解决办法:
大部分状况下,StyleSheet.hairlineWidth其实都是体现良好的。如果呈现这个问题,你能够试试选用一个 0.4~1 的一个值去设置尺寸:

wrapper:{
  height:.8,
  backgroundColor:'#333'
}

而后查看渲染成果,选一个最适宜的。

总结

在本文中,我首先介绍了 RN 适配的计划,并总结了一个适配口诀送给大家。如果你了解了这个口诀,就根本把握了 RN 适配;
而后,从源码的角度,带大家寻根究底讲述了适配外围 API——Dimensions的含意以及其值的起源;最初,解释了“全屏 mask 无奈笼罩整个屏幕”以及“1 像素边框有时无奈显示”的景象或问题。
心愿你看完本文有所播种!
如果你感觉不错,欢送点赞与珍藏并举荐给身边的敌人,感谢您的激励与认可!
有任何问题也欢送留言或者私信我
原创不易,转载需获得自己批准。

FQA

  1. 刘海屏、异形屏怎么适配?
    举荐开启“沉迷式”绘制。ios 默认开启,Android 须要设置<StatusBar translucent={true} />。而后依据刘海、异形屏理论状况设置顶部状态栏 + 标题栏的高度。
  2. iPad 等大屏平板电脑怎么适配?
    须要看理论业务。如果需要只需放弃跟手机端统一,那么能够间接用我的这套计划。如果还要求横屏竖屏适配,那么你须要应用 Dimensions.addEventListener()监听并设置此时 RN 视口参数,计算比例时,都以监听到的值为规范,再做适配。
  3. 为什么说适配是最大限度还原设计?(注释中的 [Q2]
    在“1 像素边框有时无奈显示”的章节中,我提到了像素网格对齐以及 js 数字精度问题。在做适配时,咱们最终设置的值都是依据比例进行计算的,这个计算结果会有精度误差,再加上像素网格对齐,在渲染后,存在某些非凡状况,例如在某一块区域内间断渲染大量的小元素节点时,会导致与设计图存在轻微区别。
退出移动版