打造Splash引导页的正确姿势

很久没写技术文章了,这次来写一下几乎做每以个 App 都会遇到的 Splash 引导页的诞生过程.当然,解决问题的方法可能有一万种,我只写绝对不会错的这一种(错了再改不迟).

##解决Splash黑屏问题

按照正常的思路,我们肯定是首先要先做一个 Acitivty ,背景就放一张图,铺满整个屏幕,不要标题,也不要顶部的通知栏,这个在 manifest 里配置主题就好了.用以展示我们的App的功能,特性或者只是放一个 Logo, Slogan 什么的,表面看是没问题,但是实际上呢?真实情况并不如我们所愿,会在屏幕上展示出我们想要的图片,我们会发现屏幕会黑一下或者白一下,这到底是什么原因呢?分析下Activity的生命周期,我们不难发现,只有在执行完毕 onCreate 和 onResume ,即处理一些数据之后才会显示界面,这就是问题所在.

通常的做法是,要用 handler post 出去耗时的初始化过程,这样就我们的背景图就会显示出来.当然还有不少其他的奇技淫巧,比如给背景图加一些动画,或者用其他的延迟加载等.但是这样就完了吗?不,即使这样做,我们会发现,黑屏或者白屏还是会出现,这就说明,黑屏或者白屏是Android应用本身自带的一些属性,细究一下,就会发现,问题出在我们在 manifest 里配置的主题上,如果用了黑色的主题,就会黑屏,如果用了白色主题,就会白屏.问题已经明确,这时候就容易解决了,我们只需要把主题给换了就好了嘛,用我们的新主题继承系统的主题就可以完美解决,然后再在清单文件里引用我们自定义的主题即可.

1
2
3
4
<!-- splash的主题 -->
<style name="splash_theme" parent="android:style/Theme.Black.NoTitleBar.Fullscreen">
<item name="android:windowBackground">@drawable/ic_splash</item>
</style>

这种方式给主题设置背景图,程序启动会先显示这张背景图,然后再刷新界面上的控件,给人感觉启动会比较快.

1
2
3
4
<!-- 引导页的主题 -->
<style name="guide_theme" parent="android:style/Theme.Black.NoTitleBar.Fullscreen">
<item name="android:windowIsTranslucent">true</item>
</style>

而这种方式则是透明显示,等到界面初始化完了才整个显示出应用的界面,给人感觉启动会慢一些.

##引导页的实现

至此, Splash 已经可以按我们的意愿正常显示了,但是给人感觉也太平淡了,如果要做出多张引导页切换的效果,就需要再花点功夫.当然也很简单,只需要一个ViewPager就可以了.当然,需要做一些处理,来满足我们的实际需求.

  1. 首先,只有在第一次启动应用的时候才显示这个引导页.我们只需要在启动 Splash 之后做一个 boolean 类型的变量(HAS_LOAD_GUIDE_PAGES)存储在首选项,为 true 则不进入引导页(GuideActivity),false则进入,然后在引导页执行完毕之后,将值置为true.这样就OK了.

  2. 我们需要在引导页的最后一页做一个按钮,点击能够进入登录页面,当然,也可以滑动进入.我们在 onPageSelected 方法里当 position 等于引导图片的 size 时,设置按钮显示,再设置其 onClickListener 事件即可.

##引导页索引的动态变化展示

一般情况下,我们会看到作为引导页,下面会有点,几个引导页就有几个点,而当前被选中的引导页的点颜色跟其他的不同.为了偷懒,有时候会直接将点画上去.但是总觉着是不是有点 low 啊.如果要展示其动态变化的过程呢?

其实也不复杂,首先我们要明确,这个点的动态变化只会在我们滑动引导的时候展现,因为我们要处理就要在 onPageScrolled 方法里进行.布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dip" >


<LinearLayout
android:id="@+id/ll_guide_point_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" >

</LinearLayout>

<View
android:id="@+id/v_guide_selected_point"
android:layout_width="10dip"
android:layout_height="10dip"
android:background="@drawable/guide_point_red_bg" />

</RelativeLayout>

被选中的点

1
2
3
4
5
6
7
<!-- guide_point_red_bg -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >

<corners android:radius="5dip" />
<solid android:color="@color/red"/>
</shape>

然后呢,要保证每个点之间的距离一样,被选中的那一页的点的滑动距离也和点之间的距离一样.下面就是初始化这一组点和被选中的点,以及这组点中每个点距离左边的点的长度(也就是点与点之间的距离).

1
2
3
4
5
6
7
8
9
private LinearLayout llPointGroup;
View view = new View(this);
LayoutParams params = new LayoutParams(10, 10);
if(i != 0) {
params.leftMargin = 10;
}
view.setLayoutParams(params);
view.setBackgroundResource(R.drawable.guide_point_gray_bg);
llPointGroup.addView(view);

没被选中的点

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >

<corners android:radius="5dip" />
<solid android:color="@android:color/darker_gray"/>
</shape>

最后就是设置滑动过程中被选中的点的移动了.

1
2
3
4
5
6
7
8
9
10
11
12
private View mSelectedPoint;
private int basicWidth = -1;
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if(basicWidth == -1) {
basicWidth = llPointGroup.getChildAt(1).getLeft() - llPointGroup.getChildAt(0).getLeft();
}
int leftMargin = (int) ((position + positionOffset) * basicWidth);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(10, 10);
params.leftMargin = leftMargin;
mSelectedPoint.setLayoutParams(params);
}


这样,一个比较完整的 Splash 引导页就完成了.总结一下,相对复杂的一点就是引导页索引的动态变化过程,而这部分主要是牵涉到比较琐碎的计算,思路清晰了,就很容易搞定.而第一部分主题的继承和背景替换则是一个比较容易忽略的细节,而应用的体验的好坏就集中在这些小的细节上.至此,收工.