這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408
Android & iOS 上架前都需要對應用進行一些基本設置,比如:應用的package (Bundle ID)、版本、icon…等,這邊簡單分享一下雙系統是如何去設置這些資訊的。
應用版本和包名
Android
versionCode, versionName 都在 android/app/build.gradle
中設置
- 要上傳到 Play Console 不能有重複的 versionCode,每一次上傳新版本記得都要將 versionCode + 1
android {
...
defaultConfig {
applicationId "com.test.pokedex"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 3
versionName "1.0.2"
}
...
}
package 在 android/app/src/main/AndroidManifest.xml
中設置,記得 AndroidManifest.xml 裡面的 package 和 android/app/build.gradle
中的 applicationId 要保持一致,不然打包的時候會失敗。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">
...
</manifest>
iOS
iOS 應用的版本和 bundle id 在 General – Identity 設置
- bundle id 之後講上架 apple store 時會再提到
應用 icon
https://developer.android.com/training/multiscreen/screendensities?hl=zh-tw
上架前有一個非常重要的步驟是替換應用的 icon,如果不替換 icon 的話你的應用下載到設備中看起來就會是這樣的:
Android | iOS |
---|---|
Android
應用的 icon, roundIcon 可以在 AndroidManifest.xml
看到,放在 mipmap 中
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme">
...
</application>
android/app/src/main/res
底下有多個 mipmap 資料夾,每個資料夾中都有 ic_launcher.png, ic_launcher_round.png
- mipmap-hdpi
- mipmap-mdpi
- mipmap-xhdpi
- mipmap-xxhdpi
- mipmap-xxxhdpi
這幾個資料夾是因為每台設備的像素密度不同所以需要製作不同 dpi 的圖片
可以使用工具快速製作不同像素密度的 icon
將下載下來的檔案(android/res
)全部覆蓋到 android/app/src/main/res
:
因為這個工具沒有生成 roundIcon,所以記得將 android/app/src/main/AndroidManifest.xml
中的 android:roundIcon="@mipmap/ic_launcher_round"
刪除。
iOS
在 Info – Information Property List 這邊添加 Icon Name 為 AppIcon
接著需要上傳應用所需要的 Icon 檔案,在左側找到 Images 然後將原本的 AppIcon
先右鍵刪除:
點擊左下角的 +
-> import -> 將剛剛下載的 ios
資料夾整個導入進去:
iOS 如果沒有設置 AppIcon 的話 archive 時會失敗。
Splash screen
除了 App Icon 之外還有一個很重要的是 Splash screen,即應用開啟時的加載畫面。
要修改 Splash Screen 需要安裝 react-native-splash-screen
npm i react-native-splash-screen --save
為了在 App 啟動後關閉 Splash Screen 需要在 App.tsx 中調用 SplashScreen.hide()
import SplashScreen from 'react-native-splash-screen'
const App = () => {
useEffect(() => {
const ac = new AbortController()
setTimeout(() => {
SplashScreen.hide()
}, 3000)
return () => ac.abort()
}, [])
..
}
export default App
Android
將要用作 Splash Screen 的圖片分別丟進對應的 dpi 資料夾中並改名為 launch_screen
在 android/app/src/main/res/drawable
新增 background_splash.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/splashscreen_bg"/>
<item
android:width="300dp"
android:height="300dp"
android:drawable="@mipmap/launch_screen"
android:gravity="center" />
</layer-list>
在 android/app/src/main/res/values
新增 colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splashscreen_bg">#FFFFF8</color>
<color name="app_bg">#f2f2f2</color>
</resources>
在 android/app/src/main/res/values/styles.xml
添加
<resources>
<em><!-- Base application theme. --></em>
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<em><!-- Customize your theme here. --></em>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<em><!-- Add the following line to set the default status bar color for all the app. --></em>
<item name="android:statusBarColor">@color/app_bg</item>
<em><!-- Add the following line to set the default status bar text color for all the app
to be a light color (false) or a dark color (true) --></em>
<item name="android:windowLightStatusBar">false</item>
<em><!-- Add the following line to set the default background color for all the app. --></em>
<item name="android:windowBackground">@color/app_bg</item>
</style>
<em><!-- Adds the splash screen definition --></em>
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@drawable/background_splash</item>
</style>
</resources>
修改 android/app/src/main/AndroidManifest.xml
- 將 .MainApplication 的 android:theme 改為
@style/SplashTheme
- 在 .MainActivity 添加
android:exported="true"
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">
...
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:theme="@style/SplashTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
在 android/app/src/main/java/<PROJECT_NAME>
新增 SplashActivity.java
package com.test.pokedex; <em>// Change this to your package name.</em>
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}
在 MainActivity.java
中新增
...
import android.os.Bundle; <em>// 1.</em>
import org.devio.rn.splashscreen.SplashScreen; <em>// 2.</em>
public class MainActivity extends ReactActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this); <em>// 3.</em>
super.onCreate(savedInstanceState);
}
}
在 app/src/main/res/layout
(如果沒有這個資料夾就新增) 中新增 launch_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="@mipmap/launch_screen" android:scaleType="centerCrop" />
</RelativeLayout>
iOS
使用工具生成 image sets,勾選 4x iOS
Xcode 左側找到 Images – +
Image Set 新增 SlashIcon
,將剛剛下載下來的三張圖片拖移進去
左側找到 LaunchScreen – View Controller Scene – View Controller – View 將原本畫面上的文字刪掉
右上角找到「+」新增 image view
右側 Image 選則剛剛新增的 SplashIcon
如果是手動將元素移到畫面中央的話,在不同解析度的設備上面會跑版,所以需要在右下角找到 Align 圖示新增 Constraints。
水平垂直都設為 0 的話元素會在畫面正中央:
Constraints 都設置好之後可以切換不同設備測試一下是否正常:
General – App Icons and Launch Screen 中將 Launch Screen File 設為 LaunchScreen.storyboard
接著在 ios/<PROJECT_NAME>/AppDelegate.mm
新增
...
#import "RNSplashScreen.h" <em>// Add this</em>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[super application:application didFinishLaunchingWithOptions:launchOptions];
[RNSplashScreen show];
return YES;
}
...
Expo
在 app.json
中設置
{
"expo": {
...
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
...
}
或者可以使用 expo-splash-screen
npx expo install expo-splash-screen
import React, { useCallback, useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import Entypo from '@expo/vector-icons/Entypo';
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
<em>// Keep the splash screen visible while we fetch resources</em>
SplashScreen.preventAutoHideAsync();
export default function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
<em>// Pre-load fonts, make any API calls you need to do here</em>
await Font.loadAsync(Entypo.font);
<em>// Artificially delay for two seconds to simulate a slow loading</em>
<em>// experience. Please remove this if you copy and paste the code!</em>
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
<em>// Tell the application to render</em>
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
<em>// This tells the splash screen to hide immediately! If we call this after</em>
<em>// `setAppIsReady`, then we may see a blank screen while the app is</em>
<em>// loading its initial state and rendering its first pixels. So instead,</em>
<em>// we hide the splash screen once we know the root view has already</em>
<em>// performed layout.</em>
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<View
style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
onLayout={onLayoutRootView}>
<Text>SplashScreen Demo! 👋</Text>
<Entypo name="rocket" size={30} />
</View>
);
}
Android 12 重複的 Splash Screen
如果是按照本篇前面的方式設定就不會發生這種情況。
在上架時收到了 Android 的相容性警告,說是使用 Android 12 以上版本會在啟動程式時出現兩個 Splash Screen:
這是因為從 Android 12 開始應用冷啟動和熱啟動時會顯示預設的 Splash Screen。預設的 Splash Screen 會是 launcher icon 和 theme 的 windowBackground 組合的。
如果應用的 Splash Screen 是自定義的,在 Android 12 或更高版本的設備上啟動應用程式就會出現重複的Splash Screen,首先顯示系統預設 Splash Screen,然後才顯示自定義的。
如果需要顯示自定義的 Splash Screen,那就需要將預設的覆蓋掉,覆蓋方式如下:
在 android/app/src/main/res/values/styles.xml
中添加 SplashTheme
- “android:windowIsTranslucent”
true
- “android:windowBackground”
@drawable/background_splash
- background_splash 是自定義的 splash screen 畫面
<resources>
...
<style name="SplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@drawable/background_splash</item>
</style>
</resources>
打開 android/app/src/main/AndroidManifest.xml
,修改 MainApplication 的 android:theme
為 @style/SplashTheme
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.test.pokedex">
...
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:theme="@style/SplashTheme">
...
</application>
</manifest>
注意:如果不需要自定義 Splash Screen,也可以只修改
windowSplashScreenAnimatedIcon
和windowSplashScreenBackground
,更多請參考 Migrate your splash screen implementation to Android 12 and later