Android 应用 Splash Screen 最佳实现

一直以来,程序员对应用启动页面都有比较多的争论:有人认为它必不可少,有人则认为它纯粹浪费编程和用户使用的时间。那么,应用到底要不要加入启动页面,而如果(老板要求)必须要加入,那又该如何实现呢?

Google 对 Splash Screen 的态度并非一成不变的。

在 2013 年的时候,Google 官方非常反对给应用加入 Splash Screen,称它为“反模式”(anti-pattern)。Youtube 上的 Android Developers 频道有相关的视频(需翻墙):

相关的画面截图如下:
https://www.bignerdranch.com/assets/img/blog/2015/08/adia_no_splash.png

而后,2014年 Google 发布 Material Design,旨在帮助 Android 应用设计师设计更美观,风格更协调的 UI。必须要注意到,这时 Google 又同意在应用中加入启动界面了。具体设计建议可以参看 Launch screens。当然,Google 叫它 Launch Screen,但是仍属于启动页面,似乎 Google 又同意使用 Splash Screen 了?

而现在,有人根据 Android 项目的提交消息,知道在 Android Oreo 版本中 Google 加入了 Splash API,以帮助程序员更好的实现 Splash Screen。具体文章参考 Android Oreo Adds a Splash Screen API so Developers can Easily Build App Loading Screens

实际上,反对 Splash Screen 的意见不难收集,甚至随便找个用户扯扯他都能倒出一堆意见,这些意见大体上集中在如下方面:

  1. 时间太长: 有的应用的 Splash Screen 会持续 5 秒甚至以上,完全是强行浪费用户时间。尤其是部分特殊应用的启动页面长的让人抓狂,完全不考虑用户的使用场景,比如:某些地图应用的启动页面又臭又长,根本没想过万一用户在开车时候打开应用的情况。

  2. 广告太烦: 开应用就必须看全屏广告,一不留神就点错,简直强行喂屎的典范。

  3. 加广告就算了,广告还特丑: 不得不说,大部分应用 Splash Screen 加入的广告画面非常丑陋,尤其是与应用风格完全不搭,喂屎就算了,屎里还有毒。

  4. 难点的跳过按钮: 跳过 Splash Screen 的按钮故意放在难以点击的地方(比如最靠右上角),按钮极小,颜色也不突出,一不留神就点到了广告。特别要提出,这样的设计完全不考虑大屏用户的单手使用体验,让人非常反感。

  5. 应用吓得用户“失忆”: 不开玩笑,部分应用突兀的 Splash Screen 会分散用户的注意力,让用户突然忘了打开应用的目的(尤其和长时间广告搭配,风味更佳)。

首先,要给应用加入 Splash Screen 的原因最重要的还是宣传。通过启动画面显示公司的 Logo 或者应用使用场景的剪贴画,以此来宣传公司,加深用户对应用的印象。几乎所有商业应用都有这样的诉求,换言之这是“老板的要求”。

其次,部分中低端 Android 机器,以及部分系统优化不尽如人意的高端 Android 机器,在启动应用时会出现短暂的黑屏或者白屏现象,短时间内频繁开关应用或者启动多个应用时这一现象更为明显。一般情况下黑屏或者白屏不会持续太久,仅仅一闪而过。但是,如果 Android 系统负载较高,资源紧张的时候,黑屏或白屏就会非常明显,有明显的“卡顿感”。这时候,可以把 Splash Screen 当做缓冲画面,给用户营造应用运行流畅的感觉。

个人意见:启动页面加广告是非常 SB 的行为。除非店大欺客,以至于我不能不用这个应用,大部分时候糟糕的启动广告会让我直接卸载应用。

首先,要竭力避免长时间停驻的启动页面,也不要加入广告(如果老板要求必须加广告,就无视下面的内容吧)————个人开发者尤其要注意这一点。 然后,要明白为什么尽力缩短了启动页面的留驻时间,减少了不必要的内容,但是还是无法避免应用启动的黑屏或者白屏:因为 Android 应用绘制界面要占用不少时间。如果应用的第一个 Activity 的页面内容比较复杂的时候,黑屏或白屏会更长一些。

这主要是因为 Android 应用绘制流程比较复杂。Google 的微信公众号谷歌开发者曾经为了推广 ConstraintLayout,在一篇文章谈到过这个问题。

{% blockquote @谷歌开发者 https://mp.weixin.qq.com/s/gGR2itbY7hh9fo61SxaMQQ 解析ConstraintLayout的性能优势 %} 当用户将某个 Android 视图作为焦点时,Android 框架会指示该视图进行自我绘制。这个绘制过程包括 3 个阶段:

  1. 测量: 系统自顶向下遍历视图树,以确定每个 ViewGroup 和 View 元素应当有多大。在测量 ViewGroup 的同时也会测量其子对象。

  2. 布局: 系统执行另一个自顶向下的遍历操作,每个 ViewGroup 都根据测量阶段中所确定的大小来确定其子对象的位置。

  3. 绘制: 系统再次执行一个自顶向下的遍历操作。对于视图树中的每个对象,系统会为其创建一个 Canvas 对象,以便向 GPU 发送一个绘制命令列表。这些命令包含系统在前面 2 个阶段中确定的 ViewGroup 和 View 对象的大小和位置。

绘制过程中的每个阶段都需要对视图树执行一次自顶向下的遍历操作。因此,视图层次结构中嵌入(或嵌套)的视图越多,设备绘制视图所需的时间和计算功耗也就越多。 {% endblockquote %}

所以,正确的实现办法:

  1. 尽量不适用任何布局,直接加载图片
  2. 如果要使用布局,尽量使用扁平化设计,尽量使用 ConstraintLayout

而 ConstraintLayout 的使用方法这里就不细讲了,可以自行 Google,这里着重讲一下不使用布局的办法(假设 Splash Screen 对应的 activity 为 SplashActivity)。

要确保待加载的图片风格和应用一致,主要是使用的颜色大体相近甚至相同,使得载入过程不会太过突兀。要实现这一目标,可以使用“图层列表”方案来解决。 在图用图片文件夹 res/drawable/ 中新建文件 splash_background.xml,填入如下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

  <item android:drawable="@color/colorPrimary"/>

  <!-- app icon -->
  <item>
    <bitmap
      android:gravity="center"
      android:src="@mipmap/ic_launcher"/>
  </item>
</layer-list>

这样图片的底层背景就和应用的主要颜色一致,在默认情况下也就和应用的状态栏等最显眼的地方颜色一致,这样图片消失载入布局就显得非常平滑。

如果图片和应用的风格实在不搭,也无法重新设计图片或应用配色,可以考虑加入颜色改变的动画效果,使原始图片向应用主要颜色渐变。也就是说,你需要一只 UI 设计师,或者 Google 以下 “Adobe Illustrator 从入门到放弃” :-)

要不使用布局文件加载图片,可以通过设置 Activity 的主题来实现,即将属性 android:windowBackground 设置为要上一步设置好的图片。具体操作如下: 首先,找到 res/values/styles.xml 文件,向其中加入如下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<resources>

  <!-- other styles -->

  <!-- SplashActivity theme. -->
  <style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
    <item name="android:windowBackground">@drawable/splash_background</item>
  </style>

</resources>

然后,在 AndroidManifest.xml 文件中设置 SplashActivity 的主题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.cielyang.android.test">

  <!-- other settings -->

    <activity
      android:name=".SplashActivity"
      android:theme="@style/SplashTheme">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

</manifest>

最后,在 SplashActivity 的 Java 代码中做如下修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class SplashActivity extends AppCompatActivity {

  // other codes

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // start main Activity
    startActivity(new Intent(SplashActivity.this, MainActivity.class));

    // close splash screen
    finish();
  }
}

其实,到此为止大部分的功能已经实现了,但是还是有一点小瑕疵:如果在 MainActivity 对应页面按 Back 键退出应用,然后再打开的时候,启动页面又会重新出现,无法像微信那样只显示一次启动页。 其实,改进的方法也很简单,当用户在 MainActivity 按下 Back 键时:

  • 使用操作属性设置为 ACTION_MAINIntent 来重启 MainActivity,即 MainActivity 作为应用入口
  • 把 MainActivity 类别设为 CATEGORY_HOME,即把它的页面设置为应用“主页”
  • 利用 Intentflag 设置,把 MainActivity 设置到一个新的任务栈 实际代码为:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {

  // other codes

  @Override
  public void onBackPressed() {
  	// Do NOT call super.onBackPressed()
    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.addCategory(Intent.CATEGORY_HOME);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    startActivity(intent);
  }
}

这样,就能实现一个用户体验优秀的 Splash Screen 了。

如果是要延迟一定时间再启动 MainActivity 的功能,其实也比较简单,就是对 SplashActivity 做类似如下修改:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class SplashActivity extends AppCompatActivity {

  public static final int DELAY_MILLISECONDS = 2000;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Handler handler = new Handler();
    handler.postDelayed(() -> {
      Intent intent = new Intent(SplashActivity.this, MainActivity.class);
      startActivity(intent);
      finish();
    }, DELAY_MILLISECONDS);
  }
}

注意:

  • 这里的代码仅仅为了演示,所以尽量简单
  • 延迟启动不是个好点子,一定慎用

这样,又会引发新的 bug:如果用户在 Splash Screen 页面按下 Back 键退回桌面,在 Splash Screen 创建 2 秒后仍然会显示 MainActivity 的页面。这是典型的多线程错误,改正也简单,注意在正确的时机的去除启动 MainActivity 的动作即可,最后的代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SplashActivity extends AppCompatActivity {

  public static final int DELAY_MILLISECONDS = 2000;

  private Handler mHandler;
  private final Runnable mRunnable = () -> {
    Intent intent = new Intent(SplashActivity.this, MainActivity.class);
    startActivity(intent);
    finish();
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mHandler = new Handler();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mHandler.postDelayed(mRunnable, DELAY_MILLISECONDS);
  }

  @Override
  public void onBackPressed() {
    super.onBackPressed();
    removeCallbacks();
  }

  @Override
  protected void onStop() {
    super.onStop();
    removeCallbacks();
  }

  private void removeCallbacks() {
    if (mHandler != null) {
      mHandler.removeCallbacks(mRunnable);
    }
  }
}

总的来说,从用户的角度来看应用最好尽量简洁灵敏,不要给自己“加戏”。说实在话,国内 Android 应用为了盈利考虑,总会有各种奇奇怪怪的功能,但是开发人员最好还是明白什么是“好”、什么是“坏”————毕竟,用户可以选择卸载。而如果是自己开发的东西,还是尽量简单吧,毕竟也省事不是吗?

还有,即使再简单的功能实现起来也不要眼高手低,而且尽量深入的思考改进的空间,成功实现或改进之后最好也要明白成功的原因,所谓知其所以然。

Splash Screens the Right Way Right Way to create Splash Screen on Android Android 启动页 (Splash) 的实现 A simple splash screen in Android 解析ConstraintLayout的性能优势 Intent 和 Intent 过滤器