1. 程式人生 > >簡析在React Native中如何適配iPhoneX

簡析在React Native中如何適配iPhoneX

一、介紹


iPhone X 釋出也有一段時間了,獨特的 "齊劉海",以及 "小嘴巴" 帶給了蘋果粉們無限的遐想,同時也帶來眾多的吐槽。

前幾天,招商銀行公眾號在微信推送了一條訊息,11月招商銀行App要釋出最新版本,完美適配iPhoneX,是國內第一家銀行App適配iPhoneX。感興趣的朋友可以去下載體驗一下。作為App開發者,此時你的心情是欣喜若狂,還是一萬個XXX奔騰而過。欣喜也許是因為又可以在自己開發App中"大展拳腳",而一萬個XXX奔騰而過,也許完美表達了你的真心,又該乖乖的去做適配了。

扯了這麼多,終於上道了。本篇部落格內容就是要和大家分享在React Native開發的App中,我們該如何去做適配。首先在做適配之前,我們先了解下iPhoneX在UI上的一些變化。iPhoneX版本引入了一個新名詞: 【安全區域】


以上圖豎屏為例,安全區域即從頂部感測器之下,底部Home區域之上的可視互動區域。那麼和之前的iPhone系列有什麼不同呢?


iOS11前螢幕的解析度為 375 * 667,而iPhoneX螢幕的高度則變為812,頂部高出145。所以適配的問題基本圍繞UI來解決,並且適配的核心思路就是:【避開安全區域,使佈局自適應】,我們來看幾個對比圖:

(1)狀態列部分



(2)底部導航部分


(3)橫屏狀態


二、適配


iOS11前導航欄的高度是64,其中狀態列(StatusBar)的高度為20。iPhoneX的狀態列(StatusBar)高度變為了44(感測器區域高度),如果是自定義的TopBar,這部分需要做相應的適配。
iPhoneX的底部增加了虛擬Home區,由於安全區域的原因預設TabBar的高度由49變為83,增高了34(Home區高度

),所以自定義的底部TabBar也需要需改其適配方案。


解決這個問題,最簡單的方式就是給每個介面的頂部佈局和底部有導航的佈局曾加高度,修改PaddingTop或者PaddingBottom。同時為了iOS11之前同樣適用,我們需要根據版本來讓系統選擇不同的Style即可。所以第一步我們需要判定當前的手機裝置是否為iPhoneX。如何判斷呢?很簡單,可以根據一個很明顯的改變:螢幕高度。

[javascript] view plain copy
  1. import {  
  2.     Platform,  
  3.     Dimensions  
  4. } from 'react-native';  
  5. // iPhoneX
  6. const X_WIDTH = 375;  
  7. const X_HEIGHT = 812;  
  8. // screen
  9. const SCREEN_WIDTH = Dimensions.get('window').width;  
  10. const SCREEN_HEIGHT = Dimensions.get('window').height;  
  11. exportfunction isIphoneX() {  
  12.     return (  
  13.         Platform.OS === 'ios' &&   
  14.         ((SCREEN_HEIGHT === X_HEIGHT && SCREEN_WIDTH === X_WIDTH) ||   
  15.         (SCREEN_HEIGHT === X_WIDTH && SCREEN_WIDTH === X_HEIGHT))  
  16.     )  
  17. }  

有了上述條件,我們可以根據裝置版本來選擇不同的Style樣式即可。 [javascript] view plain copy
  1. exportfunction ifIphoneX (iphoneXStyle, regularStyle) {  
  2.     if (isIphoneX()) {  
  3.         return iphoneXStyle;  
  4.     } else {  
  5.         return regularStyle  
  6.     }  
  7. }  

然後在你的樣式檔案中新增樣式 [javascript] view plain copy
  1. const styles = StyleSheet.create({  
  2.     topBar: {  
  3.         backgroundColor: '#ffffff',  
  4.         ...ifIphoneX({  
  5.             paddingTop: 44  
  6.         }, {  
  7.             paddingTop: 20  
  8.         })  
  9.     },  
  10. })  


三、擴充套件

想必大家都知道,React Native 在前兩天釋出了0.50.1版本。幸運的是,在該版本中,添加了一個SafeAreaView的Component,來完美支援iPhoneX的適配。並且React-Navigation導航控制元件庫也在^1.0.0-beta.16版本新增對iPhoneX的支援。小夥伴們終於可以輕鬆的燥起來了。此時也會有一個新的問題,不能升級RN版本的童靴怎麼辦呢?也不用急,React社群react-community開源了一個JsOnly版本的SafeAreaView,使得在低版本上同樣可以解決iPhoneX的適配問題,使用方式也很簡單:

  1. <SafeAreaView>
  2.   <View>
  3.     <Text>Look, I'm safe!</Text>
  4.   </View>
  5. </SafeAreaView>
只要將SafeAreaView作為最外層控制元件即可。

四、SafeAreaView 核心原始碼簡析

SafeAreaView的index.js檔案中的核心程式碼,分析實現大致分為如下:

(1)測量,設定觸控安全區域

  1.   componentDidMount() {  
  2.     InteractionManager.runAfterInteractions(() => {  
  3.       this._onLayout();  
  4.     });  
  5.   }  
  6. .....  
  7. _onLayout = () => {  
  8.     if (!this.view) return;  
  9.     const { isLandscape } = this.props;  
  10.     const { orientation } = this.state;  
  11.     const newOrientation = isLandscape ? 'landscape' : 'portrait';  
  12.     if (orientation && orientation === newOrientation) {  
  13.       return;  
  14.     }  
  15.     const WIDTH = isLandscape ? X_HEIGHT : X_WIDTH;  
  16.     const HEIGHT = isLandscape ? X_WIDTH : X_HEIGHT;  
  17.     this.view.measureInWindow((winX, winY, winWidth, winHeight) => {  
  18.       let realY = winY;  
  19.       let realX = winX;  
  20.       if (realY >= HEIGHT) {  
  21.         realY = realY % HEIGHT;  
  22.       } else if (realY <0) {  
  23.         realY = realY % HEIGHT + HEIGHT;  
  24.       }  
  25.       if (realX >= WIDTH) {  
  26.         realX = realX % WIDTH;  
  27.       } else if (realX <0) {  
  28.         realX = realX % WIDTH + WIDTH;  
  29.       }  
  30.       const touchesTop = realY === 0;  
  31.       const touchesBottom = realY + winHeight >= HEIGHT;  
  32.       const touchesLeft = realX === 0;  
  33.       const touchesRight = realX + winWidth >= WIDTH;  
  34.       this.setState({  
  35.         touchesTop,  
  36.         touchesBottom,  
  37.         touchesLeft,  
  38.         touchesRight,  
  39.         orientation: newOrientation,  
  40.       });  
  41.     });  
  42.   };  

(2)獲取裝置環境

  1. const isIPhoneX = (() => {  
  2.   if (minor >= 50) {  
  3.     return isIPhoneX_deprecated;  
  4.   }  
  5.   return (  
  6.     Platform.OS === 'ios' &&  
  7.     ((D_HEIGHT === X_HEIGHT && D_WIDTH === X_WIDTH) ||  
  8.       (D_HEIGHT === X_WIDTH && D_WIDTH === X_HEIGHT))  
  9.   );  
  10. })();  
  11. const isIPad = (() => {  
  12.   if (Platform.OS !== 'ios' || isIPhoneX) return false;  
  13.   // if portrait and width is smaller than iPad width  
  14.   if (D_HEIGHT > D_WIDTH && D_WIDTH <PAD_WIDTH) {  
  15.     return false;  
  16.   }  
  17.   // if landscape and height is smaller that iPad height  
  18.   if (D_WIDTH > D_HEIGHT && D_HEIGHT <PAD_WIDTH) {  
  19.     return false;  
  20.   }  
  21.   return true;  
  22. })();  
  23. const statusBarHeight = isLandscape => {  
  24.   if (isIPhoneX) {  
  25.     return isLandscape ? 0 : 44;  
  26.   }  
  27.   if (isIPad) {  
  28.     return 20;  
  29.   }  
  30.   return isLandscape ? 0 : 20;  
  31. };  

(3)根據裝置環境版本,觸控區域,獲取對應的Padding樣式,並賦值給safeAreaStyle

  1. _getSafeAreaStyle = () => {  
  2.     const { touchesTop, touchesBottom, touchesLeft, touchesRight } = this.state;  
  3.     const { forceInset, isLandscape } = this.props;  
  4.     const style = {  
  5.       paddingTop: touchesTop ? this._getInset('top') : 0,  
  6.       paddingBottom: touchesBottom ? this._getInset('bottom') : 0,  
  7.       paddingLeft: touchesLeft ? this._getInset('left') : 0,  
  8.       paddingRight: touchesRight ? this._getInset('right') : 0,  
  9.     };  
  10.     if (forceInset) {  
  11.       Object.keys(forceInset).forEach(key => {  
  12.         let inset = forceInset[key];  
  13.         if (inset === 'always') {  
  14.           inset = this._getInset(key);  
  15.         }  
  16.         if (inset === 'never') {  
  17.           inset = 0;  
  18.         }  
  19.         switch (key) {  
  20.           case 'horizontal': {  
  21.             style.paddingLeft = inset;  
  22.             style.paddingRight = inset;  
  23.             break;  
  24.           }  
  25.           case 'vertical': {  
  26.             style.paddingTop = inset;  
  27.             style.paddingBottom = inset;  
  28.             break;  
  29.           }  
  30.           case 'left':  
  31.           case 'right':  
  32.           case 'top':  
  33.           case 'bottom': {  
  34.             const padding = `padding${key[0].toUpperCase()}${key.slice(1)}`;  
  35.             style[padding] = inset;  
  36.             break;  
  37.           }  
  38.         }  
  39.       });  
  40.     }  
  41.     return style;  
  42.   };  
  43.   _getInset = key => {  
  44.     const { isLandscape } = this.props;  
  45.     switch (key) {  
  46.       case 'horizontal':  
  47.       case 'right':  
  48.       case 'left': {  
  49.         return isLandscape ? (isIPhoneX ? 44 : 0) : 0;  
  50.       }  
  51.       case 'vertical':  
  52.       case 'top': {  
  53.         return statusBarHeight(isLandscape);  
  54.       }  
  55.       case 'bottom': {  
  56.         return isIPhoneX ? (isLandscape ? 24 : 34) : 0;  
  57.       }  
  58.     }  
  59.   };  

(4)將樣式傳遞給頂層佈局View,使得佈局自使用

  1. class SafeView extends Component {  
  2.   componentWillReceiveProps() {  
  3.     this._onLayout();  
  4.   }  
  5.   render() {  
  6.     const { forceInset = false, isLandscape, children, style } = this.props;  
  7.     if (Platform.OS !== 'ios') {  
  8.       return <Viewstyle={style}>{this.props.children}</View>;  
  9.     }  
  10.     if (!forceInset && minor >= 50) {  
  11.       return <SafeAreaViewstyle={style}>{this.props.children}</SafeAreaView>;  
  12.     }  
  13.     const safeAreaStyle = this._getSafeAreaStyle();  
  14.     return (  
  15.       <View
  16.         ref={c => (this.view = c)}  
  17.         onLayout={this._onLayout}  
  18.         style={[style, safeAreaStyle]}  
  19.       >
  20.         {this.props.children}  
  21.       </View>
  22.     );  
  23.   }  
  24. }  

基本的思路都是根據裝置環境,以及螢幕狀態,設定對應的Padding樣式,從而自適應佈局即可。