<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊React Native屏幕適配那些事兒

          共 24022字,需瀏覽 49分鐘

           ·

          2021-04-27 01:34

          點擊上方 前端瓶子君,關(guān)注公眾號

          回復(fù)算法,加入前端編程面試算法每日一題群

          作者:張子君

          原文:https://segmentfault.com/a/1190000039805723

          寫在前面

          在我從事React Native(以下簡稱RN)開發(fā)的兩年工作中,自己與團隊成員時常會遇到一些令人疑惑的屏幕適配問題,如:全屏mask樣式無法覆蓋整個屏幕、1像素邊框有時無法顯示、特殊機型布局錯亂等。另外,部分成員對RN獲取屏幕參數(shù)的API——Dimensions.get('window')Dimensions.get('screen')最終返回的值代表的意義也存在疑惑。其實RN的適配比較簡單,我將在此文中闡述適配原理,提出適配方案,并針對部分特殊問題一一解釋其原因,原則上能覆蓋所有機型的適配。若有遺漏與不當之處,歡迎指出,共同交流。

          往期精彩RN文章推薦:-【從源碼分析】可能是全網(wǎng)最實用的React Native異常解決方案【建議收藏】

          適合閱讀群體

          • 有一定RN開發(fā)經(jīng)驗,了解RN js 模塊如何與原生模塊通信;
          • 有RN適配經(jīng)驗,懂了,但沒完全懂的那種;
          • 想了解RN適配;

          為什么需要適配

          保證界面在不同的設(shè)備屏幕上都能按設(shè)計圖效果展示,統(tǒng)一用戶視覺與操作體驗

          常見適配名詞闡述

          如果你從網(wǎng)上去搜屏幕適配,你搜到的博文中一定都會有以下一大堆名詞及其解釋

          1. 適配:不同屏幕下,元素顯示效果一致
          2. 屏幕尺寸:指的是屏幕對角線的長度
          3. px(單位): px實際是pixel(像素)的縮寫,根據(jù) 維基百科的解釋,它是圖像顯示的基本單元,既不是一個確定的物理量,也不是一個點或者小方塊,而是一個抽象概念。所以在談?wù)撓袼貢r一定要清楚它的上下文!
          4. 分辨率 :是指寬度上和高度上最多能顯示的物理像素點個數(shù)
          5. 設(shè)備物理像素:指設(shè)備能控制顯示的最小物理單位,意指顯示器上一個個的點。從屏幕在工廠生產(chǎn)出的那天起,它上面設(shè)備像素點就固定不變了,和屏幕尺寸大小有關(guān)
          6. 設(shè)備獨立像素(設(shè)備邏輯像素):計算機坐標系統(tǒng)中得一個點,這個點代表一個可以由程序使用的虛擬像素(比如: css像素),這個點是沒有固定大小的,越小越清晰,然后由相關(guān)系統(tǒng)轉(zhuǎn)換為物理像素
          7. CSS 像素 :css px 和物理像素的對應(yīng)關(guān)系, 與 viewport 的縮放有關(guān) scale = 1/dpr 時 1px 對應(yīng)一個 物理像素
          8. DPI:打印設(shè)備印刷點密度。每inch 多少個點
          9. PPI:設(shè)備物理像素密度。每inch 多少個物理像素
          10. DPR:設(shè)備像素比 = 設(shè)備物理像素 / 設(shè)備獨立像素(CSS像素)

          看完這些名詞后大多數(shù)人的感覺:懂了,但沒完全懂~我們先忘記這些名詞概念,只記住以下4個概念:

          • 適配:不同屏幕下,元素顯示效果一致
          • 設(shè)備獨立像素=設(shè)備邏輯像素=CSS像素
          • DPR:設(shè)備像素比 = 設(shè)備物理像素 / 設(shè)備獨立像素(CSS像素)
          • 設(shè)計圖與編碼中的尺寸都是CSS像素

          OK,下面,正菜開始!客官們請跟我這邊來。


          RN的尺寸單位

          要做RN適配得先明白RN樣式的尺寸單位。在RN的官網(wǎng)有明確標注:

          All dimensions in React Native are unitless, and represent density-independent pixels. React Native 中的尺寸都是無單位的,表示的是與設(shè)備像素密度無關(guān)的邏輯像素點。

          為什么是無單位的邏輯像素點呢?

          因為RN是個跨平臺的框架,在IOS上通常以邏輯像素單位pt描述尺寸,在Android上通常以邏輯像素單位dp描述尺寸,RN選哪個都不好,既然大家意思相同,干脆不帶單位,在哪個平臺渲染就默認用哪個單位。RN提供給開發(fā)者的就是已經(jīng)通過DPR(設(shè)備像素比)轉(zhuǎn)換過的邏輯像素尺寸,開發(fā)者無需再關(guān)心因為設(shè)備DPR不同引起的尺寸數(shù)值計算問題在有些博文中,會提到RN已經(jīng)做好了適配,其實指的就是這個意思。

          適配方案

          注意:本文示例與描述中設(shè)計圖尺寸標準都為 375X667 (iPhone6/7/8)

          對于RN適配,我總結(jié)為以下口訣:一理念,一像素,一比例;局部盒子全部按比例;遇到整頁布局垂直方向彈一彈;安卓需要處理狀態(tài)欄。

          一理念

          適配就是不同屏幕下,元素顯示效果一致的理念怎么理解呢?舉個栗子:假設(shè)有一個元素在375X667的設(shè)計圖上標注為375X44,即寬度占滿整個屏幕,高度44px。如果我們做好了RN的屏幕適配,那么:在iPhone 6/7/8(375X667)機型與iPhone X(375X812)機型上,此元素渲染結(jié)果會占滿屏幕寬度;在iPhone 6/7/8 Plus(414X736)機型上,此元素渲染結(jié)果也應(yīng)占滿屏幕寬度;


          打個現(xiàn)實生活中的比方:聯(lián)合國根據(jù)恩格爾系數(shù)的大小,對世界各國的生活水平有一個劃分標準,即一個國家平均家庭恩格爾系數(shù)大于60%為貧窮;50%-60%為溫飽;40%-50%為小康;30%-40%屬于相對富裕;20%-30%為富足;20%以下為極其富裕。假設(shè)要實現(xiàn)小康生活,不管你是哪個國家的人民,發(fā)達國家也好,發(fā)展中國家也好,家庭的恩格爾系數(shù)都必須達到40%-50%。這里,國家就可以理解為手機屏幕、生活水平就理解為元素渲染效果。至于上述的一些名詞,如:物理像素,像素比等,你可以理解為國家的貨幣以及貨幣匯率。畢竟,程序設(shè)計源自生活。

          那么,正在搬磚的你,小康了嗎~?

          一像素

          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,
            },

          一比例

          設(shè)備邏輯像素寬度比例為了更好的視覺與用戶操作體驗,目前流行的移動端適配方案,在大小上都是進行寬度適配,在布局上垂直方向自由排列。這樣做的好處是:保證在頁面上元素大小都是按設(shè)計圖進行等比例縮放,內(nèi)容恰好只能鋪滿屏幕寬度;垂直方向上內(nèi)容如果超出屏幕,可以通過手指上滑下拉查看頁面更多內(nèi)容。當然,如果你想走特殊路子,設(shè)計成高度適配,水平方向滑動也是可以的。回到上面“一理念”的例子,在iPhone 6/7/8 Plus(414X736)機型上,渲染一個設(shè)計圖375尺寸元素的話,很容易計算出,我們實際要設(shè)置的寬度應(yīng)為:375 * 414/375 = 414。這里的414/375就是設(shè)備邏輯像素寬度比例

          公式:WLR = 設(shè)備寬度邏輯像素/設(shè)計圖寬度

          WLR(width logic rate 縮寫),散裝英語,哈哈。在這里,設(shè)備的寬度邏輯像素我建議用 Dimensions.get('window').width獲取,具體緣由,后面會進行解釋。[Q1]

          那么,在目標設(shè)備上要設(shè)置的尺寸計算公式就是:size = 設(shè)置圖上元素size * WLR小學(xué)四則運算,非常簡單!其實所有的適配都是圍繞一個比例在做,如web端縮放、rem適配、postcss plugin 等,大道萬千,殊途同歸!

          局部盒子全部按比例

          為了方便理解,這里的“盒子”意思等同于web中的“盒模型”。

          局部盒子全部按比例。意思就是RN頁面中的元素大小、位置、內(nèi)外邊距等涉及尺寸的地方,全部按上述一比例中的尺寸計算公式進行計算。如下圖所示:

          這樣渲染出來的效果,會最大限度的保留設(shè)計圖的大小與布局設(shè)計效果。

          為什么說是最大限度,這里先留做一個問題,后文中解釋。[Q2]

          到這里,可能有新手同學(xué)會問:為什么在垂直方向上不用設(shè)備高度邏輯像素比例進行計算?因為 設(shè)備高度邏輯像素/設(shè)計圖高度 不一定會等于 設(shè)備寬度邏輯像素/設(shè)計圖寬度,會引起盒子拉伸。比如,現(xiàn)在按照設(shè)計圖在iPhone X(375X812)上渲染一個 100X100px的正方形盒子,寬度邏輯像素比例是1,高度邏輯像素比例是812/667≈1.22,如果寬度與高度分別按前面的2個比例計算,那么最終盒模型的size會變成:

            view1: {
              width100,
              height: 122,
            },

          好嘛,好好的一個正方形被拉伸成長方形了!

          這顯然是要不得的。講到這里,RN適配其實已經(jīng)完成70%了,對,就是玩乘除法~

          遇到整頁布局垂直方向彈一彈

          何為整頁布局?

          內(nèi)容剛好鋪滿整頁,沒有溢出屏幕外。

          這里的彈一彈,指的是flex布局。在RN中,默認都是flex布局,并且方向是column,從上往下布局。為啥要彈一彈呢?我們先來看移動端頁面布局常見的整頁上中下分區(qū)布局設(shè)計,以 TCL IOT單品舊版UI設(shè)計為例:

          按照設(shè)計,在 iPhone 6/7/8機型(375X667)上恰好鋪滿整頁,在 iPhone 6/7/8機型 plus(414X736)機型上根據(jù)上述的適配方法,其實也是近似鋪滿的,因為 414/375≈736/667。但是,在iPhone X(375X812)機型上,如果按照設(shè)計圖從上往下布局,會出現(xiàn)底下空出一截的情況:

          此時有兩種處理方法:

          1. 底部-操控菜單欄區(qū)域使用絕對定位bottom:0固定在底部,最頂部-狀態(tài)欄+標題欄是固定在頂部的,不需要處理,然后計算并用絕對定位微調(diào)頂部-設(shè)備信息展示區(qū),中部-設(shè)備狀態(tài)區(qū)的位置,使它們恰好平分多出來的空白空間,讓頁面看起來更加協(xié)調(diào);

          2. 頂部-設(shè)備信息展示區(qū),中部-設(shè)備狀態(tài)區(qū),底部-操控菜單欄區(qū)域使用父容器包裹,利用RN flex彈性布局的特性,設(shè)置justifyContent:'space-between'使得這3個區(qū)域垂直方向上下兩端對齊,中間區(qū)域上下平分多出來的空白區(qū)域。第1種,每個設(shè)備都需要去計算空白區(qū)域大小,再去微調(diào)元素位置,十分麻煩。我推薦第2種,編碼上更加簡單。這就是**“彈一彈”** 有同學(xué)會擔心第2種方式會導(dǎo)致中間區(qū)域垂直方向上跨度非常大,頁面看起來不協(xié)調(diào)。但是在實際中,設(shè)備屏幕高度邏輯像素很少會有比667大非常多的,多出的空白區(qū)域比較小,UI效果還是可以的,目前我們上線的N款產(chǎn)品中也都是使用的這種方式,請放心食用。


          到此為止,如果按照以往web端的適配經(jīng)驗,RN適配應(yīng)該已經(jīng)完成了,但是,還是有坑的。

          安卓需要處理狀態(tài)欄

          RN雖然是跨平臺的,但是在ios與Android 上渲染效果卻不一樣。最明顯的就是狀態(tài)欄了。如下圖所示:

          Android 在不設(shè)置 StatusBartranslucent屬性為true時,是從狀態(tài)欄下方開始繪制的。這與我們的適配目標不吻合,因為在我們的設(shè)計圖中,整頁的布局設(shè)計是覆蓋了狀態(tài)欄的。所以,建議將Android 的狀態(tài)欄 translucent屬性設(shè)為true,整個頁面交給我們開發(fā)者自己去布局。

          <StatusBar translucent={true} />

          如果你已經(jīng)看到這里,恭喜你,同學(xué),掌握RN的適配了,可以應(yīng)對90%以上的場景。


          但是還有一些奇奇怪怪的場景以及一些API你可能不太理解,這包含在剩下的10%適配場景中或在其中幫助你理解與調(diào)試,沒關(guān)系,我下面繼續(xù)闡述。有些會涉及到源碼,如果你有興趣,可以繼續(xù)跟我看下去。


          下面的內(nèi)容非常非常多,但是對我個人而言,這部分才是我此次分享,想帶給大家的最重要的部分。

          一些奇奇怪怪又有意思的東西

          這部分內(nèi)容非常多,請酌情閱讀

          1. DimensionsAPI

          Dimensions是RN提供的一個獲取設(shè)備尺寸信息的API。我們可以用它來獲取屏幕的寬高,這是做適配的核心API。它提供了兩種獲取方式:

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

          官方文檔上并沒有說明這兩種獲取方式的結(jié)果的含義與區(qū)別是什么。在實際開發(fā)中,這兩種方式獲取的結(jié)果有時相同,有時又有差異,讓部分同學(xué)感到困惑:我到底該使用哪一個才是正確的?我推薦你一直使用Dimensions.get('window')。只有通過它獲取的結(jié)果,才是我們真正可以操控繪制的區(qū)域。首先,明確這兩種方式獲取的結(jié)果的含義:

          • Dimensions.get('window')——獲取視口參數(shù)width、height、scale、fontScale

          • Dimensions.get('screen')——獲取屏幕參數(shù)width、height、scale、fontScale其中,在設(shè)備屏幕同狀態(tài)的默認情況下screen的width、height永遠是≥window的width、height,因為,window獲取的參數(shù)會排除掉狀態(tài)欄高度(translucent為false時)以及底部虛擬菜單欄高度。 當此安卓機設(shè)置了狀態(tài)欄translucenttrue并且沒有開啟虛擬菜單欄時,Dimensions.get('window')就會與Dimensions.get('screen')獲取的width、height一致,否則就不同。這就是本段開始時有時相同,有時又有差異的問題的答案。


            這并非靠猜想或空穴來風,直接源碼安排上:

            因作者設(shè)備有限,本文源碼僅從Android平臺分析,ios的源碼,有ios經(jīng)驗的同學(xué)可以按照思路自行查閱。準備:按照官方文檔新建一個Demo RN 工程。為了穩(wěn)定性,我們使用前面的一個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'handlerFunction) {
              invariant(
                type === 'change',
                'Trying to subscribe to unknown event: "%s"',
                type,
              );
              eventEmitter.addListener(type, handler);
            }

            /**
             * Remove an event handler.
             */

            static removeEventListener(type: 'change'handlerFunction) {
              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參數(shù)信息,我們的Dimensions.get()方法就是獲取的其中的信息。并且,該模塊指出了信息的來源:

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

          數(shù)據(jù)來源是來自原生模塊中的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;

          設(shè)備的初始尺寸信息來源于此。step5: 打開 DisplayMetricsHolder.java,找到getDisplayMetricsMap()方法:

          怎么樣,windowPhysicalPixels & screenPhysicalPixels是不是很熟悉?而它們的屬性字段widthheight、scalefontScale、densityDpi等是不是經(jīng)常用過一部分?沒錯,你在開始的Dimensions.js中見過它們:

          嚴格來說,Dimensions.js還漏了個densityDpi(設(shè)備像素密度)沒有解構(gòu)出來~ ok,那我們看它們最開始的數(shù)據(jù)來源:

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

          分別來自:sWindowDisplayMetricssScreenDisplayMetrics。其中,sWindowDisplayMetrics 通過

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

          設(shè)置;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);

          設(shè)置。在安卓中 context.getResources().getDisplayMetrics();只會獲取可繪制區(qū)域尺寸信息,默認會去除頂部狀態(tài)欄以及底部虛擬菜單欄;而設(shè)置screenDisplayMetrics時,雖然有去區(qū)分版本,但最終都是獲取的整個屏幕的物理分辨率。因此,可以真正有理有據(jù)的解釋開頭的情況了。并且完完全全從js層到原生層講述了DimensionsAPI,好吧,講這一個就啰里啰嗦的了,各位看官明白了嗎?

          全屏mask樣式無法覆蓋整個屏幕

          這個問題出現(xiàn)在部分老舊安卓機上,大概在2016~2018年左右的中低端機型,榮耀機型居多。這類手機自帶底部虛擬菜單欄,并且在使用時可以自動/手動隱藏。問題情境:當彈出一個帶mask的自定義Modal時,如果設(shè)置了mask 高度是 Dimensions.get('window').height,在隱藏底部虛擬菜單欄后,底部會空出一截無法被mask遮罩。問題原因:隱藏菜單欄后,頁面可繪制區(qū)域高度已經(jīng)發(fā)生了變化,而目前所渲染的視圖還是上一次未隱藏菜單欄狀態(tài)下的。解決方案:監(jiān)聽屏幕狀態(tài)變化,這一點官網(wǎng)其實已經(jīng)特別指出了(https://www.react-native.cn/d...%E4%BD%86%E6%98%AF%E5%BE%88%E5%A4%9A%E5%90%8C%E5%AD%A6%E8%87%AA%E5%8A%A8%E5%BF%BD%E7%95%A5%E4%BA%86%E3%80%82) 使用 Dimensions.addEventListener()監(jiān)聽并設(shè)置mask高度,重點是要改變state,通過state驅(qū)動視圖更新。當然,也要記得移除事件監(jiān)聽Dimensions.removeEventListener()

          1像素邊框有時無法顯示

          RN的1像素邊框,通常是指:StyleSheet.hairlineWidth它是一個常量,渲染效果會符合當前平臺最細的標準。但是,在列表子項中設(shè)置時,經(jīng)常會有部分列表子項丟失這根線,而且詭異的是,同一根線,有些手機顯示正常,有些手機不顯示,甚至有些機型上線條會比較“胖”。

          老規(guī)矩,源碼搬一搬:在 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,要根據(jù)roundToNearestPixel換算成最能占據(jù)整數(shù)個物理像素的一個值,與設(shè)備DPR有關(guān),也是上述 Dimensions中的scale屬性值。最差的情況就是在DPR小于1.25時,等于1 / PixelRatio.get()。按照上面的規(guī)則計算,再怎么樣,總歸還是應(yīng)該會顯示的。但是,這里我們要先引入2個概念——像素網(wǎng)格對齊以及JavaScript number精度:

          我們在設(shè)置邏輯像素時可以任意指定精度,但是設(shè)備渲染時,實際是按一個一個的物理像素顯示的,物理像素永遠整個的。為了能保證在任意精度的情況也能正確顯示,RN 渲染時會做像素網(wǎng)格對齊; JavaScript 沒有真正意義上的整數(shù)。它的數(shù)字類型是基于IEEE 754標準實現(xiàn)的,采用的64位二進制的“雙精度”格式。數(shù)值之間會存在一個**“機器精度”**誤差,通常是Math.pow(2,-52).

          概念說完,我們來看例子:

          假設(shè)現(xiàn)在有個DPR=1.5的安卓機,在頁面上下渲染2個height = StyleSheet.hairlineWidth的View,按照上面計算規(guī)則,此時height = StyleSheet.hairlineWidth≈0.66666667,理想情況占據(jù)1px物理像素。但實際情況可能是:

          因為js數(shù)字精度問題, Math.round(0.4 * 1.5) / 1.5 再乘 1.5 不一定等于1,有可能是大于1,有可能是小于1,當然,也可能等于1。覺得困惑嗎?給你看一道常見面試題咯:0.1+0.2 === 0.3 // false怎么樣?明白了嗎?哈哈 而物理像素是整個的,大于1時,會占據(jù)2個物理像素,小于1時可能占據(jù)1個也可能不占據(jù),等于1時,正常顯示。這就是像素網(wǎng)格對齊,導(dǎo)致設(shè)置StyleSheet.hairlineWidth顯示出現(xiàn)了3種情況:

          • 顯示比預(yù)期要粗;
          • 顯示正常;
          • 不顯示;

          解決辦法:大部分情況下,StyleSheet.hairlineWidth其實都是表現(xiàn)良好的。如果出現(xiàn)這個問題,你可以試試選用一個0.4~1的一個值去設(shè)置尺寸:

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

          然后查看渲染效果,選一個最適合的。

          總結(jié)

          在本文中,我首先介紹了RN適配的方案,并總結(jié)了一個適配口訣送給大家。如果你理解了這個口訣,就基本掌握了RN適配;然后,從源碼的角度,帶大家追本溯源講述了適配核心API——Dimensions的含義以及其值的來源;最后,解釋了“全屏mask無法覆蓋整個屏幕”以及“1像素邊框有時無法顯示”的現(xiàn)象或問題。希望你看完本文有所收獲!如果你覺得不錯,歡迎點贊與收藏并推薦給身邊的朋友,感謝您的鼓勵與認可!有任何問題也歡迎留言或者私信我原創(chuàng)不易,轉(zhuǎn)載需取得本人同意。

          FQA

          1. 劉海屏、異形屏怎么適配?推薦開啟“沉浸式”繪制。ios默認開啟,Android 需要設(shè)置<StatusBar translucent={true} />。然后根據(jù)劉海、異形屏實際情況設(shè)置頂部狀態(tài)欄+標題欄的高度。
          2. iPad等大屏平板電腦怎么適配?需要看實際業(yè)務(wù)。如果需求只需保持跟手機端一致,那么可以直接用我的這套方案。如果還要求橫屏豎屏適配,那么你需要使用 Dimensions.addEventListener()監(jiān)聽并設(shè)置此時RN視口參數(shù),計算比例時,都以監(jiān)聽到的值為標準,再做適配。
          3. 為什么說適配是最大限度還原設(shè)計?(正文中的**[Q2]**) 在“1像素邊框有時無法顯示”的章節(jié)中,我提到了像素網(wǎng)格對齊以及js數(shù)字精度問題。在做適配時,我們最終設(shè)置的值都是根據(jù)比例進行計算的,這個計算結(jié)果會有精度誤差,再加上像素網(wǎng)格對齊,在渲染后,存在某些特殊情況,例如在某一塊區(qū)域內(nèi)連續(xù)渲染大量的小元素節(jié)點時,會導(dǎo)致與設(shè)計圖存在細微區(qū)別。

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  毛片儿小视频 | 超碰人人在线 | 韩日一级片 | 色中文字幕第一页 | 午夜在线无码 |