打造一個啟動界面

目前,很多 Android 應用都有一個啟動界面 (Launch/Splash Screen),即應用在從桌面或應用抽屜啟動的過程中,顯示的一個有內容的界面,而不是一個空白頁;在應用運行時,啟動界面則不會出現。

根據 Material Design 的定義,啟動界面可分為兩種:

  • 一種是應用在數據加載過程中顯示的不可交互的占位符界面 (Placeholder UI),它適用于應用啟動以及應用內 Activity 切換的場景,好處是讓用戶感知數據正在加載,而不致于誤認為應用無響應。在此暫不討論這種啟動界面。
  • 另一種是專門在應用啟動時顯示的品牌頁 (Branded Launch Screen),它可以在應用啟動的短暫時間內展示應用的品牌信息,這有利于提升應用的品牌辨識度。例如以下三款應用的啟動界面都顯示了自家應用的 logo;另外,也可以顯示應用的品牌口號,不過要避免使用需要用戶仔細閱讀的大段文字。
Facebook(左)Twitter(中)Google+(右)

分析啟動界面的概念,其重點應該在于:啟動界面僅在應用啟動的瞬間顯示。也就是說,啟動界面不能導致應用的啟動速度變慢,更不用說強制顯示一段時間了(下面有更多討論)。畢竟沒人是為了看啟動界面而打開應用的,為用戶提供他們關心的內容才是應用的首要任務。

基于這個原則,Android Development Patterns 項目組的 Ian Lake 在 Google+ 發了一個帖子,詳細敘述了打造啟動界面的“正確”方法。其主體思路是:在應用冷啟動時,也就是用戶點擊應用圖標到應用的 Launcher Activity 的 onCreate() 被調用的這段時間內,設備的窗口管理器 (Window Manager) 會根據應用的主題元素 (Theme) 繪制一個占位符界面,而我們就可以通過設置這個特定的主題元素,使這個占位符界面替換為有品牌內容的啟動界面。下面分步驟詳細描述。

  1. 設置主題元素

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 屬性為匹配啟動界面的顏色,提供更好的視覺效果。

  1. 定義啟動界面

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(不透明),以防主題轉換時發生閃屏的情況。

  1. 切換應用主題

設置好 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 發表的文章,在此特表感謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,807評論 25 708
  • 【Android Activity】 什么是 Activity? 四大組件之一,通常一個用戶交互界面對應一個 ac...
    Rtia閱讀 3,840評論 3 18
  • 1.安裝虛擬機軟件virtualBox 自行百度下載安裝軟件 2.安裝vagrant 下載地址,https://w...
    pangxiaofei閱讀 909評論 0 0
  • 他只有一匹馬 他走在一條別人走過的路上 別人成功過 千萬人走在那條路上 也許有你 也許有我 他騎著他的馬, 轉個方...
    大鴨蛋黃閱讀 149評論 0 0
  • 1)項目管理知識 項目經理作為項目的管理者,應該具備這些系統的項目管理理論知識,包括對項目管理專有的概念術語、管理...
    青峰星宇閱讀 1,902評論 0 51