開發 React Native APP —— 從改造官方Demo開始(2)

elainema發表於2018-12-07

還是摘自L小庸的文章,加入了一點個人的實踐和理解

經過第一部分開發 React Native APP —— 從改造官方 Demo 開始(1)介紹,App 框架基本構建完成,這部分主要關注 UI/互動、App 釋出前的準備工作及如何釋出,具體內容包括:

  • 在使用 react navigation 的前提下,iOS 實現單個頁面從下往上(modal)的進入動畫
  • 尺寸自適應
  • 設定啟動頁,更換桌面圖示、app 展示名稱、appID
  • 打包釋出

一 擴充套件 react navigation

這部分在android下無效…… ,個人是win,所以後面沒有展示效果圖,有mac的可以嘗試下效果

 官方標註:modal - Make the screens slide in from the bottom which is a common iOS pattern. Only works on iOS, has no effect on Android.

這裡的擴充套件指的是實現可單獨配置頁面的進入方式react navigation 預設只支援全域性配置,要麼 card,要麼 modal,配置後所有頁面進入動畫相同)。

     實現上述效果需要做兩方面修改:createStackNavigator API(在 route.js 中使用)和進入某個頁面是的呼叫方式。

1.1 修改 StackNavigator API

     修改後如果使頁面預設狀態為 card,只需要輸入對應頁面即可,比如 ..navigate('ScreenSome1');如果要使某個頁面進入方式為 modal 只需要在路徑上加上 Modal 比如:..navigate('ScreenSome2Modal')
需要注意的是如果頁面進入方式為 modal,需要自定義 header,因為預設 header 樣式失效,都疊在一塊了。(還沒有實際遇到,先mark著)
關於card和modal,個人還並不是很理解,後續理解了再補上

1.2 頁面中呼叫

    首先我們新建頁面 ScreenSome2,接下來就讓它以 modal 的形式進入(從螢幕下面進入),作為對比 ScreenSome1card 的形式進入(預設進入方式,從螢幕右側進入)。

    因為以 modal 形式進入的頁面需要自定義 header,一般只是一個關閉按鈕,以 ScreenSome2 為例:

/**
 * ScreenSome2/view.js
 * 自定義 header(關閉按鈕)
 */
import React from 'react';
import { TouchableHighlight,Text,View } from 'react-native'
import pxToDp from '../../config/pxToDp';

<View>
  <TouchableHighlight
    onPress={() => self.navigation.goBack()}
    underlayColor="transparent"
    style={{
      display: "flex",
      justifyContent: "center",
      marginTop: pxToDp(30),
      width: pxToDp(150),
      height: pxToDp(90),
      backgroundColor: "yellow"
    }}
  >
    <Text style={{ marginLeft: pxToDp(24) }}>關閉</Text>
  </TouchableHighlight>

  <Text style={{ fontSize: pxToDp(36) }}>some2,以 modal 的形式進入</Text>
</View>複製程式碼

然後就是更改進入 ScreenSome2 程式碼:

/**
 * ScreenSome2/view.js
 * 自定義 header(關閉按鈕)
 */
import React from 'react';
import { TouchableHighlight,Text,View } from 'react-native'
import pxToDp from '../../config/pxToDp';

export default self => (
  <View>
    <TouchableHighlight
      onPress={() => self.navigation.goBack()}
      underlayColor="transparent"
      style={{
        display: "flex",
        justifyContent: "center",
        marginTop: pxToDp(30),
        width: pxToDp(150),
        height: pxToDp(90),
        backgroundColor: "yellow"
      }}
    >
      <Text style={{ marginLeft: pxToDp(24) }}>關閉</Text>
    </TouchableHighlight>

    <Text style={{ fontSize: pxToDp(36) }}>some2,以 modal 的形式進入</Text>
  </View>

);複製程式碼

為了看起來稍微好看點,加了一點樣式,不加也可以。style.js

import { StyleSheet } from 'react-native';

export default StyleSheet.create({
  buttonContainer: {
    margin: 20
  },
});複製程式碼

別忘了在route裡配置新的路由,完整程式碼如下:

// 引入依賴
import React from 'react';
import { createStackNavigator, createAppContainer } from 'react-navigation'

// 引入頁面元件
import ScreenBottomTab from '../screens/ScreenBottomTab';
import ScreenHome from '../screens/ScreenHome';
import ScreenSome1 from '../screens/ScreenSome1';
import ScreenSome2 from '../screens/ScreenSome2';
import ScreenTab1 from '../screens/ScreenTab1';
import ScreenTab2 from '../screens/ScreenTab2';
import ScreenTab3 from '../screens/ScreenTab3';

/**
 * 自定義 StackNavigator,可以選擇 screen 進入方式
 * 預設狀態為 card,只需要輸入對應頁面,比如 ..navigate('ScreenSome1')
 * 如果要使某個頁面進入方式為 modal 只需要在路徑上加上 Modal
 * 比如:..navigate('ScreenSome2Modal')
 */
const StackModalNavigator = (routeConfigs, navigatorConfig) => {
  const CardStackNavigator = createStackNavigator(routeConfigs, navigatorConfig);
  const modalRouteConfig = {};
  const routeNames = Object.keys(routeConfigs);

  for (let i = 0; i < routeNames.length; i++) {
    modalRouteConfig[`${routeNames[i]}Modal`] = routeConfigs[routeNames[i]];
  }

  const ModalStackNavigator = createStackNavigator(
    {
      CardStackNavigator: { screen: CardStackNavigator },
      ...modalRouteConfig,
    },
    {
      // 如果頁面進入方式為 modal,需要自定義 header(預設 header 樣式失效,都疊在一塊了)
      mode: 'modal',
      headerMode: 'none',
    },
  );

  return ModalStackNavigator;
};


// 配置路由
const AppNavigator = StackModalNavigator({
  ScreenBottomTab: ScreenBottomTab,
  //下面幾個配置的是測試Navigator不同使用場景用,只需要tab的話,只要ScreenBottomTab: ScreenBottomTab即可
  ScreenHome: ScreenHome, 
  ScreenSome1: ScreenSome1,
  ScreenSome2: ScreenSome2,
  ScreenTab1: ScreenTab1,
  ScreenTab2: ScreenTab2,
  ScreenTab3: ScreenTab3,
});
const App = createAppContainer(AppNavigator)
export default App
複製程式碼

this.props.navigation有很多方法,demo中(ScreenHome,ScreenSome1)只用到.navigate,.push,.goBack,具體可參考https://reactnavigation.org/docs/zh-Hans/navigation-prop.html

二 自適應

自適應主要包括兩方面:尺寸根據螢幕大小自適應,包括 fontSizewidth 等;圖片解析度根據螢幕解析度自適應,也就常說的二倍圖、三倍圖等。

2.1 尺寸自適應

copy自@L小庸的程式碼片段

尺寸自適應的原理是通過獲取手機螢幕的寬度,尺寸做相應比例的調整,為此封裝了一個工具函式,放在了 config/pxToDp.js 中。

  • config/pxToDp.js 尺寸轉換的工具函式

1)編寫自適應尺寸工具函式

    因為所有涉及尺寸的資料都要轉換(fontSizewidth等),所以對轉換後的資料要做處理,保證:1.大於等於 1 的數字向上取整;2.小於 1 的數字,如果是 ios 平臺統一設為 0.5;如果是安卓平臺統一設為 1(因為安卓平臺解析度千差萬別萬別,低解析度的螢幕顯示 0.5 的尺寸會有鋸齒狀)。工具函式完整程式碼如下:

/**
 * 自適應佈局
 * @param uiElementPx: ui給的原始尺寸
 */
import { Dimensions, Platform } from 'react-native';

// app 只有豎屏模式,所以可以只獲取一次 width
const deviceWidthDp = Dimensions.get('window').width;

// UI 預設給圖是 750
const uiWidthPx = 750;

function pxToDp(uiElementPx) {
  const transferNumb = uiElementPx * deviceWidthDp / uiWidthPx;

  if (transferNumb >= 1) {
    // 避免出現迴圈小數
    return Math.ceil(transferNumb);
  } else if (Platform.OS === 'android') {
    // 如果是安卓,最小為1,避免邊框出現鋸齒
    return 1;
  }
  return 0.5;
}

export default pxToDp;
複製程式碼

實際上,通過 Dimensions.get('window').width 獲取的螢幕寬度和自己想象的可能有出入,比如,iphone7 螢幕 4.7'',獲取到的寬度是 375,華為 P9 是 5.2',但獲取到的寬度卻是是 360!有點坑,這個工具函式還有待優化。個人還沒有遇到這麼多場景,也還沒有更好的方案,如果踩坑,後續更新。

2.3 Flex佈局

自己折騰了半天flex佈局,相比web的flex佈局,還是有很多區別,需要的可以參考練習練習~也可以自己找難度更高的佈局進行練習,參考程式碼https://github.com/elainema/ELAINE/blob/master/RN/AwesomeProject/src/screens/demos/FlexTest.js

開發 React Native APP —— 從改造官方Demo開始(2)

2.3 圖片解析度自適應

手機解析度越來越多,尤其安卓,React Native 可以根據不同解析度載入不同尺寸的圖片,只需在圖片命名上面加以區分。

  • 提供不同解析度的圖片

比如我們有張圖片叫 test.png,尺寸為 40 x 40(單位畫素),為了做到自適應螢幕解析度,我們還需要提供它的 2 倍圖,3 倍圖,這樣,一張圖片就對應 3 個尺寸,如下:

# 一張圖片提供 3個尺寸

test.png # 尺寸 40 x 40
test@2x.png # 尺寸 80 x 80
test@3x.png # 尺寸 120 x 120複製程式碼
name@nx是 n (n > 1) 倍圖命名規範,React Native 也是根據命名判斷圖片尺寸的。
  • 使用

在引用圖片的時候直接使用 不加倍率字尾的圖片名,比如,直接使用 test.png,如下:

/**
 * ScreenTab3/view.js
 */

<Image
  source={require("../../assets/images/test.png")}
  style={{ height: pxToDp(80), width: pxToDp(80) }}
/>複製程式碼

三 修改桌面圖示、App 展示名稱,設定啟動頁

修改桌面圖示、App 展示名稱相對簡單,設定啟動頁稍微麻煩。

IOS的設定並沒有mac去嘗試,可參考@L小庸的https://juejin.im/post/5a9602c45188257a7262e3fb嘗試,
踩坑:開發要連真機除錯,尤其是涉及原生功能的部分,切記。

3.1 設定桌面圖示

因為 App 圖示對應多個尺寸,手動改寫太麻煩,這個網站可以自動生成 MakeAppIcon

並不是所有尺寸的圖片都需要,見下文。

  • 安卓

安卓的 app 圖示相對簡單,只需要設定桌面圖示。設定位置在 yourApp/android/app/src/main/res/ 目錄下,

開發 React Native APP —— 從改造官方Demo開始(2)

這個目錄預設有5個資料夾,裡面各對應放置了一種尺寸的桌面圖示圖片,圖片尺寸不同,但名稱相同,統一為 ic_launcher.png,具體如下所示:

資料夾名稱 含義 資料夾內部圖片尺寸 資料夾內部圖片名稱
mipmap-ldpi Low Density Screen 36x36 ic_launcher.png
mipmap-mdpi Medium Density Screen 48x48 ic_launcher.png
mipmap-hdpi High Density Screen 72x72 ic_launcher.png
mipmap-xhdpi Extra-high density screen 96x96 ic_launcher.png
mipmap-xxhdpi xx-high density screen 144x144 ic_launcher.png
mipmap-xxxhdpi xxx-high density screen 192x192 ic_launcher.png

如果你使用了 MakeAppIcon 的服務,直接將對應資料夾全部放入 res/ 目錄下就好,不然就手動替換圖示。

可以根據實際需求刪除不必要的檔案,比如,120 DPI 的螢幕很少了,那麼這個資料夾就可以不要


3.2 修改 App 展示名稱

IOS的參考@L小庸的https://juejin.im/post/5a9602c45188257a7262e3fb嘗試
  • 安卓

安卓修改 App 展示名稱在這個檔案中 yourApp/android/app/src/main/res/values/strings.xml

strings.xml 這個檔案很簡單,全部內容如下:

<resources>
    <string name="app_name">你的app名稱</string>
</resources>
複製程式碼複製程式碼

替換 你的app名稱 為你想要的名字就好。

安卓的話,還要修改預設包名(applicationId),如果不修改,如果系統監測到當前應用的 applicationId 和已安裝的某個應用相同而簽名不同,會報錯:“簽名不一致 該應用可能已被惡意篡改”。

在這個檔案中修改包名: yourApp/android/app/build.gradle

// ...

defaultConfig {
    applicationId "com.yourAppId"
    // ...
}

// ...複製程式碼

3.3 設定啟動頁

這裡使用了第三方外掛react-native-splash-screen,官網教程已經很詳細,這裡做簡要介紹。

  • 專案中安裝依賴

1)下載依賴

yarn add react-native-splash-screen複製程式碼

2)新增到專案中

react-native link react-native-splash-screen複製程式碼

3)在 React Native 配置

這裡指的是設定啟動頁什麼時候消失,下面的程式碼是首頁載入完 5s 後啟動頁消失。

export default class ScreenHome extends Component {
  // ...other code
  componentDidMount() {
    // 隱藏啟動頁,如果不設定消失時間,在元件載入完啟動頁自動隱藏
    setTimeout(() => {
      SplashScreen.hide();
    }, 5000);
  }
  // ...other code
}複製程式碼

  • iOS 設定參考@L小庸的https://juejin.im/post/5a9602c45188257a7262e3fb嘗試
  • 安卓配置

      1)更新 MainActivity.java(yourApp/android/app/src/main/java/com/yourApp/MainActivity.java):            

import android.os.Bundle; // here
import com.facebook.react.ReactActivity;
// react-native-splash-screen >= 0.3.1
import org.devio.rn.splashscreen.SplashScreen; // here
// react-native-splash-screen < 0.3.1
import com.cboy.rn.splashscreen.SplashScreen; // here

public class MainActivity extends ReactActivity {
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        SplashScreen.show(this);  // here
        super.onCreate(savedInstanceState);
    }
    // ...other code
}複製程式碼

  2)新建 launch_screen.xml                                          

app/src/main/res/layout 中建立 launch_screen.xml(如果沒有 layout 目錄,新建),內容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/launch_screen">
</LinearLayout>複製程式碼

3)準備不同尺寸的啟動頁圖片並放到專案中

安卓是通過資料夾路徑尋找啟動頁面的,所以,多張尺寸的啟動頁名稱相同,都為 launch_screen.png,但要放在不同資料夾中,資料夾放置目錄為 yourApp/android/app/src/main/res/,名稱及對應放置的圖片尺寸如下:

資料夾名稱 含義 資料夾內部圖片尺寸 資料夾內部圖片名稱
drawable-ldpi Low Density Screen 240x320 launch_screen.png
drawable-mdpi Medium Density Screen 320x480 launch_screen.png
drawable-hdpi High Density Screen 480x800 launch_screen.png
drawable-xhdpi Extra-high density screen 720x1280 launch_screen.png
drawable-xxhdpi xx-high density screen 960x1600 launch_screen.png
drawable-xxxhdpi xxx-high density screen 1280x1920 launch_screen.png

建議直接從 480x800 起步放置 4 張圖片就好。
檔案必須是 png 格式的圖片,開始隨便找了一張圖片,結果打包各種報錯,命名需對應尺寸

4)優化啟動頁出現前的短暫白屏(copy,踩坑填坑)

到這裡,啟動頁功能已經 ok,但如果仔細看,可以看到啟動頁出現前會有短暫白屏,此時可通過更改android/app/src/main/res/values/styles.xml 解決:

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <!--設定透明背景-->
        <item name="android:windowIsTranslucent">true</item>
    </style>
</resources>複製程式碼
這種方案實際沒有根本解決問題:會發現這樣設定以後點選圖片不能立即彈出應用,而有短暫的等待時間,待填坑。

5)解決安卓 6.0,7.0 安裝配置完成後出現閃退,參考下面設定:(copy,踩坑填坑)

android/app/src/main/res/values 下面新建 colors.xml 檔案,內容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- this is referenced by react-native-splash-screen and will throw an error if not defined.  its value does nothing, just here to avoid a runtime error. -->
    <color name="primary_dark">#000000</color>
</resources>複製程式碼
因為 react-native-splash-screen 需要一個名為 primary_dark 的顏色值作為狀態列的顏色。

3.4 最終效果圖

設定完桌面圖示、修改 APP 展示名稱及設定啟動頁之後的效果圖如下:

開發 React Native APP —— 從改造官方Demo開始(2)

demo裡後續新增了自己練習的部分,所以效果圖和程式碼稍有差別,需要的只用care文章裡的步驟就好

四 打包釋出

4.1 安卓打包釋出

  • 生成簽名
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 
  • 設定 gradle 變數

1)首先將簽名檔案 my-release-key.keystore 放在目錄 yourApp/android/app/

2)修改檔案 yourApp/android/gradle.properties 新增下面程式碼 (替換 ***** 為正確的 keystore 密碼、別名、和 key 密碼):

注意這裡的MYAPP_RELEASE_STORE_PASSWORD和MYAPP_RELEASE_KEY_PASSWORD一定要和生成簽名時的密碼保持一致
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****複製程式碼

3)新增簽名資訊到 app 的 gradle 配置中

編輯檔案 yourApp/android/app/build.gradle 加入簽名資訊

android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
                storeFile file(MYAPP_RELEASE_STORE_FILE)
                storePassword MYAPP_RELEASE_STORE_PASSWORD
                keyAlias MYAPP_RELEASE_KEY_ALIAS
                keyPassword MYAPP_RELEASE_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release
        }
    }
}複製程式碼

  • 打包

在終端輸入下面命令

cd android && ./gradlew assembleRelease複製程式碼

等待構建完成,便可以在 yourApp/android/app/build/outputs/apk/release/app-release.apk 中找到編譯後的釋出版本。

NOTE:如果遇到這個錯誤:Execution failed for task ':app:processReleaseResources',做下述修改:,個人沒有遇到,mark先~

yourApp/android/gradle.properties 檔案最後新增下面程式碼:

classpath 'com.android.tools.build:gradle:3.0.0'
distributionUrl=https://services.gradle.org/distributions/gradle-4.1-all.zip
android.enableAapt2=false複製程式碼
NOTE:如果遇到Execution failed for task ':app:bundleReleaseJsAndAssets'.不幸的遇到了這個錯…………

刪除 android/app/build and android/build folders and run react-native run-android again

五 小結

到目前為止,從改造官方 demo 開始,一個比較完整的 React Native App 完成了,在此基礎上可以不斷擴充套件完善。

後面如果有有空會在各部分加入更多詳細的demo和使用場景,以及踩坑分享。

當然,從生產角度來說,這個 demo 的完成度不高,比如,很多樣式還是最原始的狀態、比如 WebView(App 中嵌入 H5)、下拉重新整理等也沒有涉及。其中 WebView、下拉重新整理等常用功能會逐步整合到這個 demo 中,但樣式並不打算做過多優化,因為從使用角度來講,樣式的完成度越高意味著可定製性越差,並且,那樣也會導致程式碼的可讀性變差。希望這個 demo 可以成為完整、普適但不臃腫的腳手架。


相關文章