目前,不少 Android 应用都有一个启动界面 (Launch/Splash Screen),即应用在从桌面或应用抽屉启动的过程当中,显示的一个有内容的界面,而不是一个空白页;在应用运行时,启动界面则不会出现。html
根据 Material Design 的定义,启动界面可分为两种:java
分析启动界面的概念,其重点应该在于:启动界面仅在应用启动的瞬间显示。也就是说,启动界面不能致使应用的启动速度变慢,更不用说强制显示一段时间了(下面有更多讨论)。毕竟没人是为了看启动界面而打开应用的,为用户提供他们关心的内容才是应用的首要任务。android
基于这个原则,Android Development Patterns 项目组的 Ian Lake 在 Google+ 发了一个帖子,详细叙述了打造启动界面的“正确”方法。其主体思路是:在应用冷启动时,也就是用户点击应用图标到应用的 Launcher Activity 的 onCreate() 被调用的这段时间内,设备的窗口管理器 (Window Manager) 会根据应用的主题元素 (Theme) 绘制一个占位符界面,而咱们就能够经过设置这个特定的主题元素,使这个占位符界面替换为有品牌内容的启动界面。下面分步骤详细描述。git
In res/values/styles.xmlgithub
<?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
属性为匹配启动界面的颜色,提供更好的视觉效果。bash
In res/drawable/launch_screen.xmlapp
<?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(不透明),以防主题转换时发生闪屏的状况。ide
设置好 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 发表的文章,在此特表感谢。