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。直接上代碼:

 1 package com.example.tutorial;
 2 
 3 import android.content.Context;
 4 import android.content.Intent;
 5 import android.content.SharedPreferences;
 6 import android.content.SharedPreferences.Editor;
 7 import android.os.Bundle;
 8 import android.support.v7.app.ActionBarActivity;
 9 import android.widget.Toast;
10 
11 public class MainActivity extends ActionBarActivity {
12 
13     @Override
14     protected void onCreate(Bundle savedInstanceState) {
15         super.onCreate(savedInstanceState);
16         setContentView(R.layout.activity_main);
17         
18         SharedPreferences googleActivitySP = getSharedPreferences("Tutorial", Context.MODE_PRIVATE);
19         boolean firstStart = googleActivitySP.getBoolean("first_start", true);
20         if(firstStart == true){
21             
22             Intent intent = new Intent(this, TutorialIntroPageActivity.class);
23             startActivity(intent);
24             
25             Toast.makeText(this, "Tutorial first start", Toast.LENGTH_SHORT).show();
26             Editor edit = googleActivitySP.edit();
27             edit.putBoolean("first_start", false);
28             edit.commit();
29         }
30 
31     }
32     
33 }

  如代碼中所示,記錄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、布局文件

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent" >   
 5     
 6     <LinearLayout android:id="@+id/tutorial_layout"
 7         android:layout_width="match_parent"
 8         android:layout_height="wrap_content"
 9         android:layout_centerInParent="true"
10         android:layout_marginLeft="60dp"
11         android:layout_marginRight="60dp"
12         android:gravity="center"
13         android:orientation="horizontal" >
14 
15         <ImageView
16             android:id="@+id/image_tutorial"
17             android:layout_width="310dp"
18             android:layout_height="564dp"
19             android:background="@drawable/image1" />
20         
21     </LinearLayout>
22     
23     <RelativeLayout 
24         android:layout_width="match_parent"
25         android:layout_height="60dp"
26         android:layout_alignParentBottom="true"
27         android:layout_marginLeft="60dp"
28         android:layout_marginRight="60dp"
29         android:layout_marginBottom="20dp" >
30         
31         <LinearLayout
32             android:layout_width="wrap_content"
33             android:layout_height="wrap_content"
34             android:layout_centerInParent="true"
35             android:gravity="center"
36             android:orientation="horizontal" >
37             
38             <ImageView
39                 android:id="@+id/tutorial_indicator1"
40                 android:layout_width="wrap_content"
41                 android:layout_height="wrap_content"
42                 android:background="@drawable/indicator_page" />
43             
44             <ImageView
45                 android:id="@+id/tutorial_indicator2"
46                 android:layout_width="wrap_content"
47                 android:layout_height="wrap_content"
48                 android:background="@drawable/indicator_dot" />
49                     
50             <ImageView
51                 android:id="@+id/tutorial_indicator3"
52                 android:layout_width="wrap_content"
53                 android:layout_height="wrap_content"
54                 android:background="@drawable/indicator_dot" />
55                             
56             <ImageView
57                 android:id="@+id/tutorial_indicator4"
58                 android:layout_width="wrap_content"
59                 android:layout_height="wrap_content"
60                 android:background="@drawable/indicator_dot" />
61         
62         </LinearLayout>
63             
64         <LinearLayout 
65             android:layout_width="wrap_content"
66             android:layout_height="60dp"
67             android:layout_alignParentRight="true"
68             android:gravity="center"
69             android:orientation="horizontal" >
70   
71             <Button 
72                 android:id="@+id/skip_button"
73                 android:layout_width="wrap_content"
74                 android:layout_height="wrap_content"
75                 android:text="skip"
76                 android:textSize="20dp"
77                 android:textColor="#323232"
78                 android:background="@android:color/transparent"
79                 android:drawableRight="@drawable/skip"
80                 android:drawablePadding="10dp" />
81             
82             <Button 
83                 android:id="@+id/done_button"
84                 android:layout_width="wrap_content"
85                 android:layout_height="wrap_content"
86                 android:text="done"
87                 android:textSize="20dp"
88                 android:textColor="#323232"
89                 android:background="@android:color/transparent"
90                 android:drawableRight="@drawable/done"
91                 android:drawablePadding="10dp"
92                 android:visibility="gone" />
93 
94         </LinearLayout>
95     
96     </RelativeLayout>
97     
98 </RelativeLayout>

  界面上的組件很簡單:

    a、中間為一個顯示主要信息的ImageView,頁面切換時只需要改變其顯示的圖片;

    b、下方為四個指示點+一個按鈕,四個指示點對應着有四個頁面,按鈕用來結束該引導界面;

  注意在文件的最后其實放置了兩個按鈕,當頁面在前三頁時顯示前者——“跳過”(Skip),第四頁時顯示后者——完成(Done),默認將完成按鈕隱藏,切換在Java代碼中隨着頁面的改變而進行。當然,也可以只放置一個按鈕,在Java中另加文本及圖標的改變。

  2、Java實現文件

  1 package com.example.tutorial;
  2 
  3 import com.example.tutorial.R;
  4 
  5 import android.app.Activity;
  6 import android.content.Intent;
  7 import android.content.res.Configuration;
  8 import android.os.Bundle;
  9 import android.view.GestureDetector;
 10 import android.view.MotionEvent;
 11 import android.view.View;
 12 import android.view.GestureDetector.OnGestureListener;
 13 import android.view.View.OnClickListener;
 14 import android.view.View.OnTouchListener;
 15 import android.widget.Button;
 16 import android.widget.ImageButton;
 17 import android.widget.ImageView;
 18 import android.widget.TextView;
 19 
 20 public class TutorialIntroPageActivity extends Activity implements OnTouchListener{
 21     private static final float LIMIT_ANGLE_TAN = 1.5f;
 22     
 23     private ImageView mTutorialImage;
 24     
 25     private ImageView mIndicator1;
 26     private ImageView mIndicator2;
 27     private ImageView mIndicator3;
 28     private ImageView mIndicator4;
 29     
 30     private Button mSkipButton;
 31     private Button mDoneButton;
 32     
 33     private GestureDetector mDetector = null;
 34     private int mStep = 0;
 35     
 36     @Override
 37     protected void onCreate(Bundle savedInstanceState) {
 38         super.onCreate(savedInstanceState);
 39         
 40         setContentView(R.layout.tutorial_info_page);
 41         
 42         mIndicator1 = (ImageView)findViewById(R.id.tutorial_indicator1);
 43         mIndicator2 = (ImageView)findViewById(R.id.tutorial_indicator2);
 44         mIndicator3 = (ImageView)findViewById(R.id.tutorial_indicator3);
 45         mIndicator4 = (ImageView)findViewById(R.id.tutorial_indicator4);
 46         
 47         mTutorialImage = (ImageView)findViewById(R.id.image_tutorial);
 48         mDetector = new GestureDetector(this, new TutorialImageGesture());
 49         mTutorialImage.setOnTouchListener(this);
 50 
 51         mSkipButton = (Button) findViewById(R.id.skip_button);
 52         mSkipButton.setOnClickListener(mOnSkipOrDoneButtonClickListener);
 53         mDoneButton = (Button) findViewById(R.id.done_button);
 54         mDoneButton.setOnClickListener(mOnSkipOrDoneButtonClickListener);
 55         
 56         if(savedInstanceState != null)
 57         {
 58             mStep = savedInstanceState.getInt("pageStep");
 59         }
 60         
 61         boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
 62         
 63         switch(mStep){
 64         case 0:
 65             break;
 66         case 1:
 67             mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
 68             mIndicator2.setBackgroundResource(R.drawable.indicator_page);
 69             if(isLandscape)
 70                 mTutorialImage.setBackgroundResource(R.drawable.image2);
 71             else
 72                 mTutorialImage.setBackgroundResource(R.drawable.image2);
 73             break;
 74         case 2:
 75             mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
 76             mIndicator3.setBackgroundResource(R.drawable.indicator_page);
 77             if(isLandscape)
 78                 mTutorialImage.setBackgroundResource(R.drawable.image3);
 79             else
 80                 mTutorialImage.setBackgroundResource(R.drawable.image3);
 81             break;
 82         case 3:
 83             mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
 84             mIndicator4.setBackgroundResource(R.drawable.indicator_page);
 85             if(isLandscape)
 86                 mTutorialImage.setBackgroundResource(R.drawable.image4);
 87             else
 88                 mTutorialImage.setBackgroundResource(R.drawable.image4);
 89             mDoneButton.setVisibility(View.VISIBLE);
 90             mSkipButton.setVisibility(View.GONE);
 91             break;
 92         }
 93     }
 94     
 95     @Override
 96     protected void onSaveInstanceState(Bundle outState) {
 97         super.onSaveInstanceState(outState);    
 98         outState.putInt("pageStep", mStep);
 99     }
100     
101     private void showPrePage(){
102         boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
103 
104         if(mStep == 3){
105             mIndicator3.setBackgroundResource(R.drawable.indicator_page);
106             mIndicator4.setBackgroundResource(R.drawable.indicator_dot);
107             if(isLandscape)
108                 mTutorialImage.setBackgroundResource(R.drawable.image3);
109             else
110                 mTutorialImage.setBackgroundResource(R.drawable.image3);
111             mDoneButton.setVisibility(View.GONE);
112             mSkipButton.setVisibility(View.VISIBLE);
113             mStep--;
114         }else if(mStep == 2){
115             mIndicator2.setBackgroundResource(R.drawable.indicator_page);
116             mIndicator3.setBackgroundResource(R.drawable.indicator_dot);
117             if(isLandscape)
118                 mTutorialImage.setBackgroundResource(R.drawable.image2);
119             else
120                 mTutorialImage.setBackgroundResource(R.drawable.image2);
121             mStep--;
122         }else if(mStep == 1){
123             mIndicator1.setBackgroundResource(R.drawable.indicator_page);
124             mIndicator2.setBackgroundResource(R.drawable.indicator_dot);
125             if(isLandscape)
126                 mTutorialImage.setBackgroundResource(R.drawable.image1);
127             else
128                 mTutorialImage.setBackgroundResource(R.drawable.image1);
129             mStep--;
130         }
131     }
132     
133     private void showNextPage(){
134         boolean isLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
135 
136         if(mStep == 0){
137             mIndicator1.setBackgroundResource(R.drawable.indicator_dot);
138             mIndicator2.setBackgroundResource(R.drawable.indicator_page);
139             if(isLandscape)
140                 mTutorialImage.setBackgroundResource(R.drawable.image2);
141             else
142                 mTutorialImage.setBackgroundResource(R.drawable.image2);
143             mStep++;
144         }else if(mStep == 1){
145             mIndicator2.setBackgroundResource(R.drawable.indicator_dot);
146             mIndicator3.setBackgroundResource(R.drawable.indicator_page);
147             if(isLandscape)
148                 mTutorialImage.setBackgroundResource(R.drawable.image3);
149             else
150                 mTutorialImage.setBackgroundResource(R.drawable.image3);
151             mStep++;
152         }else if(mStep == 2){
153             mIndicator3.setBackgroundResource(R.drawable.indicator_dot);
154             mIndicator4.setBackgroundResource(R.drawable.indicator_page);
155             if(isLandscape)
156                 mTutorialImage.setBackgroundResource(R.drawable.image4);
157             else
158                 mTutorialImage.setBackgroundResource(R.drawable.image4);
159             mDoneButton.setVisibility(View.VISIBLE);
160             mSkipButton.setVisibility(View.GONE);
161             mStep++;
162         }
163     }
164     
165     private OnClickListener mOnSkipOrDoneButtonClickListener = new OnClickListener() {
166 
167         @Override
168         public void onClick(View arg0) {
169             Intent intent = new Intent(TutorialIntroPageActivity.this, MainActivity.class);
170             if(getIntent().getParcelableExtra(Intent.EXTRA_INTENT) != null){
171                 intent.putExtra(Intent.EXTRA_INTENT, getIntent().getParcelableExtra(Intent.EXTRA_INTENT));
172             }
173             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
174             startActivity(intent);
175             //finish();
176         }
177         
178     };
179     
180     @Override
181     public boolean onTouchEvent(MotionEvent event) {
182         mDetector.onTouchEvent(event);
183         return true;
184     }
185     
186     public class TutorialImageGesture implements OnGestureListener {
187 
188         @Override
189         public boolean onDown(MotionEvent arg0) {
190             // TODO Auto-generated method stub
191             return false;
192         }
193 
194         @Override
195         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
196             float ver = Math.abs(e1.getY() - e2.getY());
197             float hor = Math.abs(e1.getX() - e2.getX());
198                 if ( ver / hor > LIMIT_ANGLE_TAN || Math.abs(velocityX)<500) {
199                     return false;
200                 }
201 
202                 if (e2.getX() - e1.getX() < 0) {
203                     showNextPage();
204                 }
205                 else {
206                     showPrePage();
207                 }
208                 return true;
209         }
210 
211         @Override
212         public void onLongPress(MotionEvent arg0) {
213             // TODO Auto-generated method stub
214             
215         }
216 
217         @Override
218         public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2,
219                 float arg3) {
220             // TODO Auto-generated method stub
221             return false;
222         }
223 
224         @Override
225         public void onShowPress(MotionEvent arg0) {
226             // TODO Auto-generated method stub
227             
228         }
229 
230         @Override
231         public boolean onSingleTapUp(MotionEvent arg0) {
232             // TODO Auto-generated method stub
233             return false;
234         }
235         
236     }
237 
238     @Override
239     public boolean onTouch(View arg0, MotionEvent arg1) {
240         // TODO Auto-generated method stub
241         return false;
242     }
243 }

  實現過程沒有特別復雜的地方,接下來對幾個地方值得回味的進行講解,以后也許會用到。

  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次。如頁面左滑的處理代碼:

1 mIndicates[mStep].setBackgroundResource(R.drawable.asus_tutorial_indicator_dot);
2 mIndicates[mStep-1].setBackgroundResource(R.drawable.asus_tutorial_indicator_page);

 2、在橫/豎屏切換時,ViewFlipper組件的顯示頁面也要進行相應的改變,因為其初始化時是顯示自身包含的第一個元素。ViewPager類提供了一個很好用的方法setDisplayedChild(int index),可以讓其直接顯示指定位置的子元素。而不用傻傻地通過循環來進行頁面的跳轉,雖然也可以達到目的。如用for循環實現的代碼為:

1 for(int i=0;i<mStep;++i){
2   mViewFlipperImage.showNext();
3 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM