目前,很多 Android 應用都有一個啟動介面 (Launch/Splash Screen),即應用在從桌面或應用抽屜啟動的過程中,顯示的一個有內容的介面,而不是一個空白頁;在應用執行時,啟動介面則不會出現。
根據 Material Design 的定義,啟動介面可分為兩種:
- 一種是應用在資料載入過程中顯示的不可互動的佔位符介面 (Placeholder UI),它適用於應用啟動以及應用內 Activity 切換的場景,好處是讓使用者感知資料正在載入,而不致於誤認為應用無響應。在此暫不討論這種啟動介面。
- 另一種是專門在應用啟動時顯示的品牌頁 (Branded Launch Screen),它可以在應用啟動的短暫時間內展示應用的品牌資訊,這有利於提升應用的品牌辨識度。例如以下三款應用的啟動介面都顯示了自家應用的 logo;另外,也可以顯示應用的品牌口號,不過要避免使用需要使用者仔細閱讀的大段文字。
分析啟動介面的概念,其重點應該在於:啟動介面僅在應用啟動的瞬間顯示。也就是說,啟動介面不能導致應用的啟動速度變慢,更不用說強制顯示一段時間了(下面有更多討論)。畢竟沒人是為了看啟動介面而開啟應用的,為使用者提供他們關心的內容才是應用的首要任務。
基於這個原則,Android Development Patterns 專案組的 Ian Lake 在 Google+ 發了一個帖子,詳細敘述了打造啟動介面的“正確”方法。其主體思路是:在應用冷啟動時,也就是使用者點選應用圖示到應用的 Launcher Activity 的 onCreate() 被呼叫的這段時間內,裝置的視窗管理器 (Window Manager) 會根據應用的主題元素 (Theme) 繪製一個佔位符介面,而我們就可以通過設定這個特定的主題元素,使這個佔位符介面替換為有品牌內容的啟動介面。下面分步驟詳細描述。
- 設定主題元素
In res/values/styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name=”AppTheme.Launcher”>
<item name="android:windowBackground">@drawable/launch_screen</item>
<!-- Optional, on Android 5.0+ you can modify the colorPrimaryDark color to match the windowBackground color for further branding-->
<!-- <item name="colorPrimaryDark">@android:color/white</item> -->
</style>
</resources>
複製程式碼
由於啟動介面只在應用啟動瞬間顯示,所以不能直接在 AppTheme 中直接修改主題元素,而是需要新建一個繼承 AppTheme 的 Launcher Theme,命名為 AppTheme.Launcher,並且只修改其中的 android:windowBackground
屬性為想要的 drawable 資源即可。對於 Android 5.0 及以上的裝置,還可以修改 colorPrimaryDark
屬性為匹配啟動介面的顏色,提供更好的視覺效果。
- 定義啟動介面
In res/drawable/launch_screen.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list>
複製程式碼
在上面 AppTheme.Launcher 主題中 android:windowBackground
屬性設定的 drawable 資源不能是一個圖片,因為圖片會拉伸至整個螢幕的尺寸;所以這裡需要定義一個 XML 檔案來設定啟動介面。注意 layer-list 的 android:opacity
透明度屬性要設定為 opaque(不透明),以防主題轉換時發生閃屏的情況。
- 切換應用主題
設定好 Launcher Activity 在應用啟動瞬間應用的主題後,即可在 AndroidManifest.xml 中設定好,這樣 Launcher Activity 就預設使用 AppTheme.Launcher 主題了,使啟動介面得以顯示。
In AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ...>
<activity
android:name=".MainActivity"
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
複製程式碼
不過,在呼叫 Launcher Activity 的 onCreate() 時,其主題應該切換回 AppTheme,這裡在 super.onCreate()
之前通過 setTheme(R.style.AppTheme)
設定好。
In MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
// ...
}
}
複製程式碼
至此,一個啟動介面就打造好了。上述這種實現方法的優勢是不會延長應用的啟動速度,是理論上最好的方案;但是有一個問題,即當 Launcher Activity 重啟時,啟動介面會重新顯示一遍。這個問題的解決方法是新建一個專門的 Splash Activity,使其作為應用的 Launcher Activity;也就是說,應用啟動時,啟動介面顯示完即跳到 Splash Activity 中,隨後再跳到其它 Activity。雖然這樣增加了 Activity 之間切換的延時,不過應用可以實現根據不同的啟動狀態跳轉到不同 Activity 的功能。在此不過多討論。
以上描述的啟動介面的實現方法是符合 Material Design 設計規範的,但是它有很多侷限性,例如無法實現動畫,更不用說國內廠商非常熱衷的定時廣告頁面了。要實現這類啟動介面,就要為應用增加一個介面 (Activity & Layout),相當於在使用者與應用的實際內容之間人為地增加一層關係。這必須權衡利弊,因為即使再酷炫的啟動畫面,使用者也很快會審美疲勞。
下面介紹我在 FilmsPeek 應用中實現的啟動介面,它是一個關於應用 logo 的一秒鐘動畫。此專案託管在我的 GitHub 上,專案介紹已詳細寫在 README 上,歡迎大家 star 和 fork。
首先為專用於啟動介面的 Splash Activity 構建佈局,基於 Material Design 只顯示應用 logo 或口號的設計規範,在 FilmPeek App 中只擺放了將應用 logo 拆解為“膠片”和“放大鏡”兩部分的兩個 ImageView,以及由於應用了使用 THE MOVIE DB (TMDb) API 而需要顯示其 logo 的 ImageView。為精簡篇幅,程式碼不在此貼出,具體可在 GitHub FilmsPeek Repository 中檢視。值得一提的是,對於這種扁平化的簡單佈局,Android Support 庫提供的 ConstraintLayout 十分簡單易用,這也是 Google Android 團隊大力推廣的。
然後在 Splash Activity 中僅完成一件事即可,那就是使用 AnimationSet 讓應用 logo 的“放大鏡”部分完成一系列的動畫。例如,“放大鏡”首先需要往左直線移動一段距離,這首先可以通過設定一個 TranslateAnimation 物件,然後將該物件新增到 AnimationSet 來實現。
In SplashActivity.java
// Create an animation that make the lens icon move straight left.
Animation straightLeftAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, -1,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
straightLeftAnimation.setDuration(300);
// Set the LinearInterpolator to the animation to uniform its play velocity.
// The same as below.
straightLeftAnimation.setInterpolator(new LinearInterpolator());
// Add the animation to the set.
animation.addAnimation(straightLeftAnimation);
複製程式碼
由於 Android 未提供做圓周運動的類,所以這裡需要新建一個自定義 Animation 類,在 FilmsPeek App 中實現了順時針的、水平方向的半圓周運動。
In SplashActivity.java
private class SemicircleAnimation extends Animation {
private final float mFromXValue, mToXValue;
private float mRadius;
private SemicircleAnimation(float fromX, float toX) {
mFromXValue = fromX;
mToXValue = toX;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
float fromXDelta = resolveSize(Animation.RELATIVE_TO_SELF, mFromXValue, width, parentWidth);
float toXDelta = resolveSize(Animation.RELATIVE_TO_SELF, mToXValue, width, parentWidth);
// Calculate the radius of the semicircle motion.
// Note: Radius can be negative here.
mRadius = (toXDelta - fromXDelta) / 2;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float dx = 0;
float dy = 0;
if (interpolatedTime == 0) {
t.getMatrix().setTranslate(dx, dy);
return;
}
float angleDeg = (interpolatedTime * 180f) % 360;
float angleRad = (float) Math.toRadians(angleDeg);
dx = (float) (mRadius * (1 - Math.cos(angleRad)));
dy = (float) (-mRadius * Math.sin(angleRad));
t.getMatrix().setTranslate(dx, dy);
}
}
複製程式碼
由於自定義類 SemicircleAnimation 只實現了水平方向的半圓周運動,所以其建構函式的輸入引數只需要水平方向上(X 軸)的起點與終點位置;而且輸入引數都是相對於自身而言的,例如 SemicircleAnimation(0, 2)
表示移動物體從當前位置出發,往右上角做半圓周運動,終點在距離兩倍於自身寬度的水平位置上。
動畫完成後,跳轉至 MainActivity 並呼叫 finish() 使 Splash Activity 不再出現。在 FilmsPeek App 中,通過設定 AnimationSet 的 AnimationListener 來實現。
In SplashActivity.java
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
// When animation set ended, intent to the MainActivity.
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
// It's IMPORTANT to finish the SplashActivity, so user won't reach it afterwards.
finish();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
複製程式碼
從上述程式碼可見,自定義 Animation.AnimationListener 需要 override 三個方法,其中在 onAnimationEnd 方法中實現在動畫結束後的操作。
最後在 AndroidManifest 中設定 SplashActivity,使其作為應用的 Launcher Activity;同時也將 Splash Activity 的主題設定為無應用欄的。注意修改 MainActivity 的 intent-filter 屬性。
In AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.filmspeek">
<application ...>
<activity
android:name=".SplashActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
複製程式碼
本文主要觀點來自 Elvis Chidera 在 Medium 發表的文章,在此特表感謝。