App 引导界面
App 引导界面
1、前言
最近在学习实现App的引导界面,本篇文章对设计流程及需要注意的地方做一个浅显的总结。
附上项目链接,供和我水平类似的初学者参考——http://files.cnblogs.com/files/tgyf/Tutorial.rar。
对于有引导界面的App,刚安装或使用后将其数据清除(Setting-Apps-...),启动后就会出现引导界面,目的是向用户介绍本款应用的使用方法或主要功能。
App引导过程的页面数一般为为3到6个,特殊的如刷机后的SetupWizard设置页面将近10个。除了非常必要,放过多页面会影响用户体验,虽然可以在界面上添加“跳过”按钮(最近较为常用的按钮为“立即体验”)为不需要被引导的用户提供进入App的捷径。
有两种操作方式让用户左/右翻动页面:点击按钮和手势滑动。前者需要在界面上添加两个按钮(一般以左/右箭头图标作为显示内容),而后者直接识别用户手指在屏幕上的滑动操作,不过两者最终实现的页面切换方法是相同的。随着时间的推移,很多App为了界面的简洁及美观而只为用户提供手势滑动来翻动页面,当然还是有一些App仍然同时提供了上述的两种操作方式。
先给出一张常见的引导界面图(网络上找的):
2、判断是否是第一次启动
无论之前有没有这方面的开发经验,都不难想到:要判断App是否是第一次启动,需要从某个地方读取一个记录启动状态(或者说启动次数)的变量值,而且这个变量值不能随着应用的关闭而消失,除非将其数据清除或卸载。将这种类型的数据保存在文件中是不错的方法,但这里不用File类,因为Android提供了一个非常好用的类——SharedPreferences。
记录启动状态的数据是在App启动后的类中进行读写的(非引导界面相关类),这里是主类MainActivity。直接上代码:
package com.example.tutorial; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.Toast; public class MainActivity extends ActionBarActivity { @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); SharedPreferences googleActivitySP = getSharedPreferences("Tutorial", Context.MODE_PRIVATE);
boolean firstStart = googleActivitySP.getBoolean("first_start", true);
if(firstStart == true){ Intent intent = new Intent(this, TutorialIntroPageActivity.class);
startActivity(intent); Toast.makeText(this, "Tutorial first start", Toast.LENGTH_SHORT).show();
Editor edit = googleActivitySP.edit();
edit.putBoolean("first_start", false);
edit.commit();
} } }
如代码中所示,记录App启动状态的变量为boolean型first_start,约定第一次启动时其值为true,否则为false。
刚开始这样使用SharedPreferences类的时候,相信也有人和我一样会疑惑:如代码18、19行,一上来就是获取文件与变量值,原来不存在怎么办?这就是该类智能的地方,类似File又胜于File,当文件不存在时就创建,当变量不存在时就返回给定的默认值。即:
a、App初次启动时会在相应目录中新建一个文件,这里是data/data/com.example.tutorial/Tutorial.xml,私有模式。注意默认是xml格式,文件名称与模式分别由方法getSharedPreferences()的第一、二参数决定。若想看其是否生成可以通过Eclipse的DDMS,若想看其内容可以通过在CMD下的adb shell命令进入Shell模式,cd定位到目录后用cat filename查看。
b、初次启动时start_first变量并不存在,所以返回值为给定的默认值true,方法getBoolean()第一、二参数分别指定了需要获取的变量名与默认返回值,若变量存在就返回实际值,不存在就返回给定的默认值,不会因为变量不存在而报异常。不过该类还提供了判断变量是否存在的方法,感兴趣的朋友可以自己研究。
第一次启动App,获取的start_first变量值为true,所以如代码22、23行利用Intent类打开引导界面——TutorialIntroPageActivity类实现的Activity(稍候会讲解)。接着如代码26-28行将变量值设置为false,以后运行该App获取的start_first变量值均为false,就不会打开引导界面了。
实现过后会发现,这些曾不敢触碰以为会很高深的点也不过如此。所以,要成长就要勇于探索、犯错、总结。
3、引导界面的实现
常见Activity的差别除了打开次数外,引导界面做的事情简单,主要是向用户展示App的使用说明与主要功能,做多加上几个按钮。
引导界面Activity在类TutorialIntroPageActivity中进行实现,先给出代码:
1、布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" > <LinearLayout android:id="@+id/tutorial_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginLeft="60dp"
android:layout_marginRight="60dp"
android:gravity="center"
android:orientation="horizontal" > <ImageView
android:id="@+id/image_tutorial"
android:layout_width="310dp"
android:layout_height="564dp"
android:background="@drawable/image1" /> </LinearLayout> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true"
android:layout_marginLeft="60dp"
android:layout_marginRight="60dp"
android:layout_marginBottom="20dp" > <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="horizontal" > <ImageView
android:id="@+id/tutorial_indicator1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/indicator_page" /> <ImageView
android:id="@+id/tutorial_indicator2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/indicator_dot" /> <ImageView
android:id="@+id/tutorial_indicator3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/indicator_dot" /> <ImageView
android:id="@+id/tutorial_indicator4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/indicator_dot" /> </LinearLayout> <LinearLayout
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_alignParentRight="true"
android:gravity="center"
android:orientation="horizontal" > <Button
android:id="@+id/skip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="skip"
android:textSize="20dp"
android:textColor="#323232"
android:background="@android:color/transparent"
android:drawableRight="@drawable/skip"
android:drawablePadding="10dp" /> <Button
android:id="@+id/done_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="done"
android:textSize="20dp"
android:textColor="#323232"
android:background="@android:color/transparent"
android:drawableRight="@drawable/done"
android:drawablePadding="10dp"
android:visibility="gone" /> </LinearLayout> </RelativeLayout> </RelativeLayout>
界面上的组件很简单:
a、中间为一个显示主要信息的ImageView,页面切换时只需要改变其显示的图片;
b、下方为四个指示点+一个按钮,四个指示点对应着有四个页面,按钮用来结束该引导界面;
注意在文件的最后其实放置了两个按钮,当页面在前三页时显示前者——“跳过”(Skip),第四页时显示后者——完成(Done),默认将完成按钮隐藏,切换在Java代码中随着页面的改变而进行。当然,也可以只放置一个按钮,在Java中另加文本及图标的改变。
2、Java实现文件
package com.example.tutorial; import com.example.tutorial.R; import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.OnGestureListener;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView; public class TutorialIntroPageActivity extends Activity implements OnTouchListener{
private static final float LIMIT_ANGLE_TAN = 1.5f; private ImageView mTutorialImage; private ImageView mIndicator1;
private ImageView mIndicator2;
private ImageView mIndicator3;
private ImageView mIndicator4; private Button mSkipButton;
private Button mDoneButton; private GestureDetector mDetector = null;
private int mStep = 0; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); setContentView(R.layout.tutorial_info_page); mIndicator1 = (ImageView)findViewById(R.id.tutorial_indicator1);
mIndicator2 = (ImageView)findViewById(R.id.tutorial_indicator2);
mIndicator3 = (ImageView)findViewById(R.id.tutorial_indicator3);
mIndicator4 = (ImageView)findViewById(R.id.tutorial_indicator4); mTutorialImage = (ImageView)findViewById(R.id.image_tutorial);
mDetector = new GestureDetector(this, new TutorialImageGesture());
mTutorialImage.setOnTouchListener(this); mSkipButton = (Button) findViewById(R.id.skip_button);
mSkipButton.setOnClickListener(mOnSkipOrDoneButtonClickListener);
mDoneButton = (Button) findViewById(R.id.done_button);
mDoneButton.setOnClickListener(mOnSkipOrDoneButtonClickListener); if(savedInstanceState != null)
{
mStep = savedInstanceState.getInt("pageStep");
} boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; switch(mStep){
case 0:
break;
case 1:
mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
mIndicator2.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image2);
else
mTutorialImage.setBackgroundResource(R.drawable.image2);
break;
case 2:
mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
mIndicator3.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image3);
else
mTutorialImage.setBackgroundResource(R.drawable.image3);
break;
case 3:
mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
mIndicator4.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image4);
else
mTutorialImage.setBackgroundResource(R.drawable.image4);
mDoneButton.setVisibility(View.VISIBLE);
mSkipButton.setVisibility(View.GONE);
break;
}
} @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("pageStep", mStep);
} private void showPrePage(){
boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; if(mStep == 3){
mIndicator3.setBackgroundResource(R.drawable.indicator_page);
mIndicator4.setBackgroundResource(R.drawable.indicator_dot);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image3);
else
mTutorialImage.setBackgroundResource(R.drawable.image3);
mDoneButton.setVisibility(View.GONE);
mSkipButton.setVisibility(View.VISIBLE);
mStep--;
}else if(mStep == 2){
mIndicator2.setBackgroundResource(R.drawable.indicator_page);
mIndicator3.setBackgroundResource(R.drawable.indicator_dot);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image2);
else
mTutorialImage.setBackgroundResource(R.drawable.image2);
mStep--;
}else if(mStep == 1){
mIndicator1.setBackgroundResource(R.drawable.indicator_page);
mIndicator2.setBackgroundResource(R.drawable.indicator_dot);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image1);
else
mTutorialImage.setBackgroundResource(R.drawable.image1);
mStep--;
}
} private void showNextPage(){
boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; if(mStep == 0){
mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
mIndicator2.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image2);
else
mTutorialImage.setBackgroundResource(R.drawable.image2);
mStep++;
}else if(mStep == 1){
mIndicator2.setBackgroundResource(R.drawable.indicator_dot);
mIndicator3.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image3);
else
mTutorialImage.setBackgroundResource(R.drawable.image3);
mStep++;
}else if(mStep == 2){
mIndicator3.setBackgroundResource(R.drawable.indicator_dot);
mIndicator4.setBackgroundResource(R.drawable.indicator_page);
if(isLandscape)
mTutorialImage.setBackgroundResource(R.drawable.image4);
else
mTutorialImage.setBackgroundResource(R.drawable.image4);
mDoneButton.setVisibility(View.VISIBLE);
mSkipButton.setVisibility(View.GONE);
mStep++;
}
} private OnClickListener mOnSkipOrDoneButtonClickListener = new OnClickListener() { @Override
public void onClick(View arg0) {
Intent intent = new Intent(TutorialIntroPageActivity.this, MainActivity.class);
if(getIntent().getParcelableExtra(Intent.EXTRA_INTENT) != null){
intent.putExtra(Intent.EXTRA_INTENT, getIntent().getParcelableExtra(Intent.EXTRA_INTENT));
}
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
//finish();
} }; @Override
public boolean onTouchEvent(MotionEvent event) {
mDetector.onTouchEvent(event);
return true;
} public class TutorialImageGesture implements OnGestureListener { @Override
public boolean onDown(MotionEvent arg0) {
// TODO Auto-generated method stub
return false;
} @Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float ver = Math.abs(e1.getY() - e2.getY());
float hor = Math.abs(e1.getX() - e2.getX());
if ( ver / hor > LIMIT_ANGLE_TAN || Math.abs(velocityX)<500) {
return false;
} if (e2.getX() - e1.getX() < 0) {
showNextPage();
}
else {
showPrePage();
}
return true;
} @Override
public void onLongPress(MotionEvent arg0) {
// TODO Auto-generated method stub } @Override
public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2,
float arg3) {
// TODO Auto-generated method stub
return false;
} @Override
public void onShowPress(MotionEvent arg0) {
// TODO Auto-generated method stub } @Override
public boolean onSingleTapUp(MotionEvent arg0) {
// TODO Auto-generated method stub
return false;
} } @Override
public boolean onTouch(View arg0, MotionEvent arg1) {
// TODO Auto-generated method stub
return false;
}
}
实现过程没有特别复杂的地方,接下来对几个地方值得回味的进行讲解,以后也许会用到。
a、代码61、102、134行对设备方向的获取,因为一般Activity会随着设备的横/竖屏切换时而重启,且两种状态下的布局样式往往是不一样的,所以需要根据方向来实时调整显示的界面组件。本例给出的图像是一样的,所以看不出差别。
b、showprePage()和showNextPage()除了判断设备方向以外,主要负责引导页面、页面指示点及按钮状态的切换。
c、前面a中提到横/竖屏转换时Activity会重启(再次调用onCreate()方法,还有其他一些原因也会引起该结果),那么就需要暂时记录重启前用户看到哪个页面,以便重启后能马上恢复。代码95-99行重载了Acticity的onSaveInstanceState(),利用变量mStep作为页面的索引。Activity重启后,获取mStep值并恢复引导界面的工作由onCreate()方法完成。
d、手势识别类TutorialImageGesture重载的方法onFling(),当手势滑动的斜率大于1.5或水平距离小于500时,设定此种情形为不满足页面切换条件,不进行页面切换;否则,根据水平方向上的X坐标来判断向左还是向右切换页面。
4、结果图
虽然界面寒酸,还是拉出来溜溜。
四张引导界面(细心的朋友会发现其实是一张图片截成了四部分):
手指在屏幕的图片上进行滑动时,页面会进行相应的切换;按SKIP、DONE按钮或Back键时引导过程结束,App界面出现。
注意,当点击界面上的SKIP或者DONE按钮时,如代码169-178行打开App对应的Activity,并设置其Flag属性为Intent.FLAG_ACTIVITY_CLEAR_TOP,效果和按手机Back类似,将引导Activity彻底销毁。
5、后记
上面的实现过程在显示主要引导信息(图片)时采用的是ImageView组件,那么当页面向左/右变动时只能靠很笨的方法。后来改用ViewPager组件再次进行了实现,当用户在屏幕上进行左/右滑动时可以调用其showNext()/showPrevious()方法来进行页面的跳转。
同时,还有几个地方可以进行简化处理。
1、页面下方指示点的变换,将四个ImageView变量(mIndicate1/mIndicate2/mIndicate3/mIndicate4)写成一个数组mIndicates[],在页面变化时只需要两句代码就可以实现指示点做相应的改变,而不是通过if来判断3-4次。如页面左滑的处理代码:
mIndicates[mStep].setBackgroundResource(R.drawable.asus_tutorial_indicator_dot);
mIndicates[mStep-1].setBackgroundResource(R.drawable.asus_tutorial_indicator_page);
2、在横/竖屏切换时,ViewFlipper组件的显示页面也要进行相应的改变,因为其初始化时是显示自身包含的第一个元素。ViewPager类提供了一个很好用的方法setDisplayedChild(int index),可以让其直接显示指定位置的子元素。而不用傻傻地通过循环来进行页面的跳转,虽然也可以达到目的。如用for循环实现的代码为:
for(int i=0;i<mStep;++i){
mViewFlipperImage.showNext();
}
App 引导界面的更多相关文章
- 使用UIPageControl UIScrollView制作APP引导界面
1. 新建两个视图控制器类(继承自UIViewController), 在AppDelegate.m中指定根视图控制器 #import "AppDelegate.h" #impor ...
- App引导界面,可以这么玩
什么是ViewPager,刚一听到这个词,我们可能感觉很奇怪,但是我相信我们大部分人都曾见到过这些界面的.其实它就是我们在安装好一个app之后第一次使用时的那些引导界面的效果.这就是通过ViewPag ...
- android——利用SharedPreference做引导界面
很久以前就接触过sharedPreference这个android中的存储介质.但是一直没有实际使用过,今天在看之前做的“民用机型大全”的app时,突然想到可以使用sharedPreference类来 ...
- 【Android UI设计与开发】第05期:引导界面(五)实现应用程序只启动一次引导界面
[Android UI设计与开发]第05期:引导界面(五)实现应用程序只启动一次引导界面 jingqing 发表于 2013-7-11 14:42:02 浏览(229501) 这篇文章算是对整个引导界 ...
- Android UI开发第四十一篇——墨迹天气3.0引导界面及动画实现
周末升级了墨迹天气,看着引导界面做的不错,模仿一下,可能与原作者的代码实现不一样,但是实现的效果还是差不多的.先分享一篇以前的文章,android动画的基础知识,<Android UI开发第十二 ...
- 【Android UI设计与开发】3.引导界面(三)实现应用程序只启动一次引导界面
大部分的引导界面基本上都是千篇一律的,只要熟练掌握了一个,基本上也就没什么好说的了,要想实现应用程序只启动一次引导界面这样的效果,只要使用SharedPreferences类,就会让程序变的非常简单, ...
- 【Android】首次进入应用时加载引导界面
参考文章: [1]http://blog.csdn.net/wsscy2004/article/details/7611529 [2]http://www.androidlearner.net/and ...
- SharedPreference 存储小量数据,一般首次启动显示引导界面就用这个。
写://添加一个SharedPreference并传入数据SharedPreference sharedPreferences = getSharedPreferences("share_d ...
- 转-ViewPager组件(仿微信引导界面)
http://www.cnblogs.com/lichenwei/p/3970053.html 这2天事情比较多,都没时间更新博客,趁周末,继续继续~ 今天来讲个比较新潮的组件——ViewPager ...
随机推荐
- Git分布式版本控制学习
git和SVN都是版本控制系统.git是命令行操作,不喜欢的就算了,看完如果有身体不适还请及时就医~ git WIN32百度网盘下载地址:http://pan.baidu.com/s/1c1AeY9 ...
- C#登入例子--第一个程序
第一步:在数据库创建一个存放账号密码的表单 第二步:创建一个登入项目 拆分成三层: CS层: BLL层: DAL层: Common层: Web.config:
- ae 地理坐标与投影坐标转换 [转]
转载地址:http://blog.163.com/lai_xiao_hui/blog/static/123037324201151443221942/ 代码是将WGS84地理坐标转换为WGS84UTM ...
- Unable to extract 64-bitimage. Run Process Explorer from a writeable directory
Unable to extract 64-bitimage. Run Process Explorer from a writeable directory When we run Process E ...
- iOS之App加急审核详细步骤
申请加急网址:https://developer.apple.com/appstore/contact/appreviewteam/index.html 补充:加急审核说明是可以写中文的 提交加急审核 ...
- EventBus3.0源码解析
本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...
- 【转】Android Studio下加入百度地图的使用 (一)——环境搭建
最近有学 生要做毕业设计,会使用到定位及地图信息的功能,特此研究了一下,供大家参考,百度定位SDK已经更新到了5.0,地图SDK已经更新到了3.5,但是在 AndroidStudio中使用还是存在一些 ...
- 47个过程(PMBOK2008)
项目管理过程 知识领域 过程组 含义 之前应完成 之后要进行 制定项目章程 整合 启动 编写一份正式批准项目并授权项目经理使用组织资源的文件的过程 无 制定项目管理计划 制定项目管理计划 整合 规划 ...
- 优化临时表使用,SQL语句性能提升100倍
[问题现象] 线上mysql数据库爆出一个慢查询,DBA观察发现,查询时服务器IO飙升,IO占用率达到100%, 执行时间长达7s左右.SQL语句如下:SELECT DISTINCT g.*, cp. ...
- Linux系统中 安装Vmware Toolst工具
前提: 安装虚拟机.可以参考:在Windows上安装虚拟机详细图文教程 安装Linux.可以参考:在VMware Workstation里的Linux操作系统的安装——红旗桌面7.0 本文作者:sou ...