打造一个启动界面

目前,很多 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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • 【Android Activity】 什么是 Activity? 四大组件之一,通常一个用户交互界面对应一个 ac...
    Rtia阅读 3,741评论 3 18
  • 1.安装虚拟机软件virtualBox 自行百度下载安装软件 2.安装vagrant 下载地址,https://w...
    pangxiaofei阅读 846评论 0 0
  • 他只有一匹马 他走在一条别人走过的路上 别人成功过 千万人走在那条路上 也许有你 也许有我 他骑着他的马, 转个方...
    大鸭蛋黄阅读 135评论 0 0
  • 1)项目管理知识 项目经理作为项目的管理者,应该具备这些系统的项目管理理论知识,包括对项目管理专有的概念术语、管理...
    青峰星宇阅读 1,848评论 0 51