我们在桌面启动自己辛苦创建的APP时,总是会看到黑屏或是白屏现象,这让人的体验感觉不是很好,看看大厂的APP为什么不会有这个现象?有问题就要解决,即便不是BUG,用户体验一样很重要。

1. APP启动黑/白屏的原因

首先,我们需要知道一个APP启动时,屏幕上都会有什么。在我们的APP里,显示在屏幕上的自然是各个View了,而我们的View又都是在Activity的onCreate()方法中调用了setContentView()方法,传入了我们的layout文件,也就是我们理论上应该看到的Activity内容。但是Android系统在启动一个新的Activity时,首先进行的并不是绘制Activity的内容,我们来看看一个Activity的UI结构。

Activity UI结构
Activity UI结构

我们可以看到,一个Activity中在ContentView的外围还有PhoneWindow、DecorView、TitleView,当Activity进行绘制时会先绘制这三个View,这时ContentView还没加载进来,所以什么东西都看不到,系统会将屏幕填充主题默认的背景色,亮系主题填充白色,暗系主题填充黑色,就出现了Activity启动之前的黑/白屏现象。

2. 解决黑/白屏的方法

刚才说了,系统会为屏幕填充主题默认的背景色,那么要解决这个问题就应该从屏幕的背景下手了。一想到背景,第一反应就是去layout里设置ContentView的background,但是系统并不会先加载ContentView,那有什么在系统绘制之前就能调整屏幕背景呢?

注意,系统会填充主题默认的背景色,所以主题会在绘制之前加载,我们可以修改主题的背景达到目的。一般一个APP第一个启动的Activity都是Splash,作为一个Splash并不需要标题栏,而且普遍是全屏的。那么我们可以将主题进行修改一下,大概有两种方式:

  1. 将主题背景变成透明的,这样在ContentView加载出来之前,我们会透过启动的Activity看到桌面,就不会有黑/白屏的现象。再把标题栏去掉,把Activity设置成全屏的,效果挺不错,缺点是如果启动的是一个有复杂耗时操作的Activity,那么会有一种延迟的感觉。
1
2
3
4
5
6
7
<style name="AppTheme" parent="android:Theme.Light.NoActionBar">  
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:WindowFullscreen">true</item>
</style>
  1. 将主题背景设置成一张图片,把标题栏去掉,把Activity设置成全屏的,这这样在ContentView加载出来之前,我们就能看到一张默认背景图,但是图片的屏幕适配问题就需要考虑了,主题里的背景图片会自动拉伸,可能会导致失真或者比例失调的问题。
1
2
3
4
5
6
<style name="AppTheme" parent="android:Theme.Light.NoActionBar">  
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@drawable/bg_splash</item>
<item name="android:WindowFullscreen">true</item>
</style>

3. 背景显示优化

这里再将上述解决方法进行优化,减少用户使用时不好的体验。(PS:当然你可以不做此优化,如果你想忽悠老板,把锅甩给Android系统、手机的硬件配置、UI的图给的不匹配屏幕等等)

3.1 方法一优化

方法一中的问题在于延迟感严重,那么我们需要做的就是尽量加快Splash的启动速度,在Splash中不加入任何逻辑操作,并且Application中任何的数据及开源框架的初始化方法都不应调用,当Splash启动完全后,在Splash的OnResume()方法中可以启动子线程进行各初始化操作,宁可让用户在背景图中等待,不要让用户看着手机桌面认为手机死机了。

3.2 方法二优化

方法二中的问题在于图片拉伸可能导致失真或者比例失调,使得界面不够美观。简单的方式就是建立各个drawable文件夹,覆盖所有的屏幕尺寸类型,每个文件夹下塞一张让UI做的合理的背景图。这种方法超级令人无语,UI的工作量较大,而且你也不可能覆盖所有的屏幕尺寸,比如这样:

超长的手机屏幕
超长的手机屏幕

那么怎样可以拥有更好的用户体验呢?这时候我们需要的是drawable。

3.2.1 drawable的类型

在Android中,我们可以使用xml自定义一个drawable,用的最多的场景就是背景图了,Android系统的一些默认图标也都是用xml实现的,当然那涉及到了一些矢量图的知识。

首先我们先了解一下drawable的类型,常见的几种有:BitmapDrawableShapeDrawableStateListDrawableLevelListDrawableLayerDrawableTransitionDrawableScaleDrawableAnimationDrawableInsetDrawableNinePatchDrawableClipDrawableVectorDrawable

这里我采用了LayerDrawable来解决图片拉伸的问题,其他的drawable以后再写一篇文章专门分析各个drawable。

3.2.2 LayerDrawable解决图片拉伸

LayerDrawable为什么能解决图片拉伸问题呢?这要从LayerDrawble的性质说起了:

  1. XML标签为layer-list

  2. 层次化的Drawable合集

  3. 可以包含多个item,每个item表示一个Drawable

  4. item中可以通过android:drawable直接引用资源

  5. item中可以通过android:top等指定相对于父节点的位置

多个Drawable的层次化叠加,并且可以指定每个Drawable的位置,是不是和layout很像?一些简单的布局显示可以用LayerDrawble来完成,不过只能塞Drawable进去,文字什么的就不行了。

那么我们来看一下一个可以很好适配屏幕的背景图改如何完成。首先在drawable文件夹下建立一个layer-list类型的drawable文件bg_splash.xml,随后写入如下代码:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/bg"/>
<item android:top="175dp">
<bitmap android:gravity="top" android:src="@drawable/logo"/>
</item>
</layer-list>

我们在layer-list中放入了两个item:第一个是一整个页面的背景,可以是图片,但是笔者建议用纯色的ShapeDrawable,一定程度上减少内存开销并且无需考虑图片失真之类的问题;第二个是一个Bitmap,<bitmap>这个标签是按照图片大小插入一张图片,这样避免了图片在屏幕上的拉伸,通过android:top来指定这个item顶部的偏移距离,同样还可以指定android:bottomandroid:leftandroid:right来定位item的位置,随后对<bitmap>android:gravity设置为top,让logo可以显示在顶部。这样一个能随着屏幕进行适配并且不会失真的背景就做好了,按照方法二设置为android:windowBackground即可。

3.2.3 style主题优化

按照方法二的设定,整个App将使用我们制作的bg_splash作为背景,这时候如果不给每个Activity设置背景或者在使用虚拟键盘时,进入App之后屏幕上也会看到bg_splash出现在没有控件的位置,造成用户的疑惑或者反感。

我们知道Activity也是可以设置主题的,那么我们可以给Application设置一个默认的主题AppTheme,然后给SplashActivity设置我们的全屏带背景的主题SplashTheme,这样在我们的SplashActivity中就可以迅速显示启动背景图,进入App中,在其他Activity中也不会出现启动背景图,最终的styles和AndroidManifest文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
styles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@color/colorDefaultBg</item>
</style>

<style name="SplashTheme" parent="AppTheme">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/bg_splash</item>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
AndroidManifest.xml
<application android:name=".MyApplication"
android:theme="@style/AppTheme"
android:supportsRtl="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true">

<activity android:name=".SplashActivity"
android:theme="@style/SplashTheme"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|keyboardHidden">

<intent-filter>

<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>

</intent-filter>

</activity>

</application>