轻量级 Splash Screen 实现方案与优化

之前,在博客中写过 Splash Screen 实现方法的学习笔记,现在再回顾总结的时候发现原来的思路不够清晰。刚好,Oreo 版本也提供了官方的 Splash Screen 实现支持,不妨重新写一篇。

TL;DR:使用背景图片而非 View 显示 Logo。

其实,最初 Google 官方是反对使用 Splash Screen 的。

https://www.bignerdranch.com/assets/img/blog/2015/08/adia_no_splash.png

原视频在 Youtube 上:

最主要的原因是大部分的 Splash Screen 比较臃肿,强迫用户等待过长的时间,引发了 Google 的强烈反对。

但是,企业或者致力于打造个人品牌的开发者确实有进行品牌宣传的需要,而 app 本身的图标又不能完全当作企业或个人的宣传画,折衷之后 Google 还是认同了轻量化 Splash Screen 的设计模式。之后在 Material Design 设计风格的建议中又给出了 Splash Screen 的设计建议(具体设计建议可以参看 Launch screens),也可以认为是官方对于实际企业开发需求的妥协吧。 不过,根据官方仍建议 Splash Screen 的设计方案不宜过于复杂,最好只展示企业的 Logo,即我所说的轻量级 Splash Screen。从改善用户使用体验的角度看,这种 Splash Screen 是最容易接受的方案,也能对企业或个人的宣传起到一定的效果。

从用户的角度看,太复杂臃肿的 Splash Screen 确实很讨人厌,尤其是只为了显示很多和应用设计风格不搭的广告的启动页更让人恼火。还有很多 app 的页面虽然设计了跳过广告按钮,但是故意把它放到右上角,而且按钮极小、颜色几乎透明,真是让人抓狂。

  1. 建立 SplashActivity,并且删除对应的布局 xml 文件,也删掉相应的 setContentView() 语句。

  2. res/drawable 目录下新建 Splash Screen 要用到的 logo文件(为了简单,这里使用 ic_launcher 代替)

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

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

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

为了 logo 和应用的标题栏等色调统一,此处使用 LayerDrawable 并且把底色设置为 colorPrimary,实际使用应考虑具体需求进行设计。

  1. res/values/styles.xml 中定义如下的格式:
1
2
3
<style name="SplashScreen" parent="Theme.AppCompat.NoActionBar" >
	<item name="android:windowBackground">@drawable/splash_screen</item>
</style>
  1. 在 Android Manifest 文件中,设置 SplashActivity 的主题为步骤 3 中定义的内容:
1
2
3
4
5
6
7
8
<activity
	android:name=".SplashActivity"
	android:theme="@style/SplashScreen">
	<intent-filter>
		<action android:name="android.intent.action.MAIN" />
		<category android:name="android.intent.category.LAUNCHER" />
	</intent-filter>
</activity>
  1. 跳转到其他的 Activity(本例中为 MainActivity),SplashActivity 中的 Java 代码为:
1
2
3
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();

这样,就实现了基本的 Splash Screen,流程和原理都很简单,不难理解。

Oreo 版本新增加了一个属性专门用来设置 Splash Screen 图片,可以使用如下方法进行设置:

1
2
3
<style  name="SplashScreen" parent="Theme.AppCompat.NoActionBar">
	<item name="android:windowSplashscreenContent">@drawable/splash_screen</item>
</style>

官方给出了 Splash Screen 背景图片的设置方案,当然以前的设置方法在 Oreo 版本也能正常使用。但是,要注意的是目前 Google 仍然没有给出复杂的 Splash Screen 的实现辅助工具,个人认为官方仍然不建议使用过于复杂臃肿的 Splash Screen 设计方案。如果业务有这样的需求,那就自己动手吧。

上面的实现代码已经实现了基本的 Splash Screen 功能,但是仍然不太完善,还需要进行进一步的优化。

原始的 SplashActivity 结构简单,所以实际运行速度很快,常常会出现 Logo 一闪而逝的尴尬情况,如果 Logo 色调明亮的话会有类似闪光的效果,所以实际上还是需要显示 Logo 一段时间后再跳转到 MainActivity。 延迟跳转 MainActivity 的功能确实不难实现,使用 Handler 实例的 postDelayed() 即可实现,但是请注意这里有小小的坑需要避免。

首先,通常的延迟跳转的代码是如下形式:

1
2
3
4
5
6
mHandler.postDelayed(() -> {
	() -> {
    Intent intent = new Intent(this, MainActivity.class);
	startActivity(intent);
	finish(); };
}, 2000);

这段代码使得 Splash Screen 持续显示 2 秒后再跳转到 MainActivity。但是,如果用户在 Splash Screen 持续显示的 2 秒内决定暂时不使用 app,按下 BACK 或 HOME 键,应用仍然会在后台倒数到 2 秒结束然后突然显示 MainActivity 的页面内容。

注意:此处 2 秒的时间是为了演示而故意设置的,实际时间间隔应该更短,一般 0.5 秒就可以了,不宜过长。

修复的方法很简单,只要注意在合适的时候使用 Handler 的方法 removeCallbacks() 来取消跳转动作即可。因为 HOME 和 BACK 键都会触发 onStop() 回调,所以移除操作应该在 onStop() 中。 另外,因为按下 HOME 键后再启动 app 不会触发 onCreate() 回调,所以 postDelayed() 应该放在 onResume() 中。 最后,SplashActivity 的代码如下:

 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
public class SplashActivity extends AppCompatActivity {

	private Handler mHandler = new Handler();
  	private Runnable mStartMainActivity =
    		() -> {
            	MainActivity.actionStart(this);
  				finish();
  	};
  
	@Override
  	protected void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
  	}

    @Override
  	protected void onResume() {
	    super.onResume();    

	 	mHandler.postDelayed(mStartMainActivity, 2000);
  	}

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

		mHandler.removeCallbacks(mStartMainActivity);
  	}
}

注:跳转到 MainActivity 的代码,为了方便使用和管理封装为 MainActivity 的静态方法。

原来的实现方式下,如果在MainActivity 页面按下 BACK 键后再启动应用又会显示 Splash Screen 页面,怎样才能做到像微信那样只显示一次 Splash Screen 呢? 实际上,可以修改 MainActivity 中的 onBackPressed() 方法:

1
2
3
4
5
@Override public void onBackPressed() {
	Intent intent = new Intent(Intent.ACTION_MAIN);
  	intent.addCategory(Intent.CATEGORY_HOME);
  	startActivity(intent); 
}

注意:不要习惯性地调用 super.onBackPressed()

一般来说,如果数据数据应该和应用生命周期等长的情况下,可以考虑在 Splash Screen 中完成。但是实际操作时有一些注意要点:

  • 数据读取不要放在主线程,尤其当 SplashActivity 中设计了延迟跳转操作的时候,不止是为了防止 ANR,更因为跳转时会调用 SplashActivityfinish() 方法
  • 数据加载操作的代码不要持有 SplashActivity 的引用,尤其要注意不要乱用内部类或 lamda 表达式
  • 如果需要传入 Context 实例,要用 Application 或它的子类

这里特别提一句,SharedPreferences 数据的修改操作最好使用 commit() 方法在子线程提交,也方便进行错误处理,同时尽量保证 SplashActivity 中主线程代码的简洁。具体原因可参考这篇文章的简要分析。

另外,不建议使用回调方法使得数据加载完成后再触发 SplashActivity 中的跳转操作,主要是因为数据加载的时间是不可控的。如果用户手机运行速度较慢、系统资源紧张或者加载远程数据时网络情况很差,那么应用会长时间卡在 Logo 画面,虽然不是 ANR 但是用户仍会认为应用“卡死”了,极有可能执行强退、重启应用等操作,要处理这种情况必须加入大量错误处理代码,和我们“轻量级”的目标不符,而且极易出错。而如果仅仅进行少量的数据操作,那么 Logo 图标很可能一闪而过,不仅没有实际的宣传作用,而且如果 Logo 背景颜色较浅还会出现手机屏幕“闪光灯”的效果。