Android仿人人客戶端滑動選單的側滑選單效果

Android仿人人客戶端滑動選單的側滑選單效果

人人客戶端有一個特效還是挺吸引人的,在主介面手指向右滑動,就可以將選單展示出來,而主介面會被隱藏大部分,但是仍有左側的一小部分同選單一起展示。

據說人人客戶端的這個特效是從facebook客戶端模仿來的,至於facebook是不是又從其它地方模仿來的就不得而知了。好,今天我們就一起來實現這個效果,總之我第一次看到這個特效是在人人客戶端看到的,我也就主觀性地認為我是在模仿人人客戶端的特效了。

雖然現在網上類似這種效果的實現也非常多,可是我發現實現方案大都非常複雜,並不容易理解。但其實這種效果並不難實現,因此我今天給大家帶來的也是史上最簡單的滑動選單實現方案。

首先還是講一下實現原理。在一個Activity的佈局中需要有兩部分,一個是選單(menu)的佈局,一個是內容(content)的佈局。兩個佈局橫向排列,選單佈局在左,內容佈局在右。初始化的時候將選單佈局向左偏移,以至於能夠完全隱藏,這樣內容佈局就會完全顯示在Activity中。然後通過監聽手指滑動事件,來改變選單佈局的左偏移距離,從而控制選單佈局的顯示和隱藏。

原理圖如下:

將選單佈局的左偏移值改成0時。

效果圖如下:

好,我們開始用程式碼來實現。首先在Eclipse中新建一個Android專案,專案名就叫做RenRenSlideMenuDemo。然後寫一下佈局檔案,建立或開啟layout目錄下的activity_main.xml檔案,加入如下程式碼:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:orientation="horizontal" 
tools:context=".MainActivity" > 
<LinearLayout 
android:id="@ id/menu" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:background="@drawable/menu" > 
</LinearLayout> 
<LinearLayout 
android:id="@ id/content" 
android:layout_width="fill_parent" 
android:layout_height="fill_parent" 
android:background="@drawable/content" > 
</LinearLayout> 
</LinearLayout>

這個佈局檔案的最外層佈局是一個LinearLayout,排列方向是水平方向排列。這個LinearLayout下面巢狀了兩個子LinearLayout,分別就是選單的佈局和內容的佈局。這裡為了要讓佈局儘量簡單,選單佈局和內容佈局裡面沒有加入任何控制元件,只是給這兩個佈局各新增了一張背景圖片,這兩張背景圖片是我從人人客戶端上截下來的圖。這樣我們可以把注意力都集中在如何實現滑動選單的效果上面,不用關心裡面各種複雜的佈局了。

建立或開啟MainActivity,這個類仍然是程式的主Activity,也是這次demo唯一的Activity,在裡面加入如下程式碼:


public class MainActivity extends Activity implements OnTouchListener { 
/** 
* 滾動顯示和隱藏menu時,手指滑動需要達到的速度。 
*/ 
public static final int SNAP_VELOCITY = 200; 
/** 
* 螢幕寬度值。 
*/ 
private int screenWidth; 
/** 
* menu最多可以滑動到的左邊緣。值由menu佈局的寬度來定,marginLeft到達此值之後,不能再減少。 
*/ 
private int leftEdge; 
/** 
* menu最多可以滑動到的右邊緣。值恆為0,即marginLeft到達0之後,不能增加。 
*/ 
private int rightEdge = 0; 
/** 
* menu完全顯示時,留給content的寬度值。 
*/ 
private int menuPadding = 80; 
/** 
* 主內容的佈局。 
*/ 
private View content; 
/** 
* menu的佈局。 
*/ 
private View menu; 
/** 
* menu佈局的引數,通過此引數來更改leftMargin的值。 
*/ 
private LinearLayout.LayoutParams menuParams; 
/** 
* 記錄手指按下時的橫座標。 
*/ 
private float xDown; 
/** 
* 記錄手指移動時的橫座標。 
*/ 
private float xMove; 
/** 
* 記錄手機擡起時的橫座標。 
*/ 
private float xUp; 
/** 
* menu當前是顯示還是隱藏。只有完全顯示或隱藏menu時才會更改此值,滑動過程中此值無效。 
*/ 
private boolean isMenuVisible; 
/** 
* 用於計算手指滑動的速度。 
*/ 
private VelocityTracker mVelocityTracker; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_main); 
initValues(); 
content.setOnTouchListener(this); 
} 
/** 
* 初始化一些關鍵性資料。包括獲取螢幕的寬度,給content佈局重新設定寬度,給menu佈局重新設定寬度和偏移距離等。 
*/ 
private void initValues() { 
WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 
screenWidth = window.getDefaultDisplay().getWidth(); 
content = findViewById(R.id.content); 
menu = findViewById(R.id.menu); 
menuParams = (LinearLayout.LayoutParams) menu.getLayoutParams(); 
// 將menu的寬度設定為螢幕寬度減去menuPadding 
menuParams.width = screenWidth - menuPadding; 
// 左邊緣的值賦值為menu寬度的負數 
leftEdge = -menuParams.width; 
// menu的leftMargin設定為左邊緣的值,這樣初始化時menu就變為不可見 
menuParams.leftMargin = leftEdge; 
// 將content的寬度設定為螢幕寬度 
content.getLayoutParams().width = screenWidth; 
} 
@Override 
public boolean onTouch(View v, MotionEvent event) { 
createVelocityTracker(event); 
switch (event.getAction()) { 
case MotionEvent.ACTION_DOWN: 
// 手指按下時,記錄按下時的橫座標 
xDown = event.getRawX(); 
break; 
case MotionEvent.ACTION_MOVE: 
// 手指移動時,對比按下時的橫座標,計算出移動的距離,來調整menu的leftMargin值,從而顯示和隱藏menu 
xMove = event.getRawX(); 
int distanceX = (int) (xMove - xDown); 
if (isMenuVisible) { 
menuParams.leftMargin = distanceX; 
} else { 
menuParams.leftMargin = leftEdge   distanceX; 
} 
if (menuParams.leftMargin < leftEdge) { 
menuParams.leftMargin = leftEdge; 
} else if (menuParams.leftMargin > rightEdge) { 
menuParams.leftMargin = rightEdge; 
} 
menu.setLayoutParams(menuParams); 
break; 
case MotionEvent.ACTION_UP: 
// 手指擡起時,進行判斷當前手勢的意圖,從而決定是滾動到menu介面,還是滾動到content介面 
xUp = event.getRawX(); 
if (wantToShowMenu()) { 
if (shouldScrollToMenu()) { 
scrollToMenu(); 
} else { 
scrollToContent(); 
} 
} else if (wantToShowContent()) { 
if (shouldScrollToContent()) { 
scrollToContent(); 
} else { 
scrollToMenu(); 
} 
} 
recycleVelocityTracker(); 
break; 
} 
return true; 
} 
/** 
* 判斷當前手勢的意圖是不是想顯示content。如果手指移動的距離是負數,且當前menu是可見的,則認為當前手勢是想要顯示content。 
* 
* @return 當前手勢想顯示content返回true,否則返回false。 
*/ 
private boolean wantToShowContent() { 
return xUp - xDown < 0 && isMenuVisible; 
} 
/** 
* 判斷當前手勢的意圖是不是想顯示menu。如果手指移動的距離是正數,且當前menu是不可見的,則認為當前手勢是想要顯示menu。 
* 
* @return 當前手勢想顯示menu返回true,否則返回false。 
*/ 
private boolean wantToShowMenu() { 
return xUp - xDown > 0 && !isMenuVisible; 
} 
/** 
* 判斷是否應該滾動將menu展示出來。如果手指移動距離大於螢幕的1/2,或者手指移動速度大於SNAP_VELOCITY, 
* 就認為應該滾動將menu展示出來。 
* 
* @return 如果應該滾動將menu展示出來返回true,否則返回false。 
*/ 
private boolean shouldScrollToMenu() { 
return xUp - xDown > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY; 
} 
/** 
* 判斷是否應該滾動將content展示出來。如果手指移動距離加上menuPadding大於螢幕的1/2, 
* 或者手指移動速度大於SNAP_VELOCITY, 就認為應該滾動將content展示出來。 
* 
* @return 如果應該滾動將content展示出來返回true,否則返回false。 
*/ 
private boolean shouldScrollToContent() { 
return xDown - xUp   menuPadding > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY; 
} 
/** 
* 將螢幕滾動到menu介面,滾動速度設定為30. 
*/ 
private void scrollToMenu() { 
new ScrollTask().execute(30); 
} 
/** 
* 將螢幕滾動到content介面,滾動速度設定為-30. 
*/ 
private void scrollToContent() { 
new ScrollTask().execute(-30); 
} 
/** 
* 建立VelocityTracker物件,並將觸控content介面的滑動事件加入到VelocityTracker當中。 
* 
* @param event 
* content介面的滑動事件 
*/ 
private void createVelocityTracker(MotionEvent event) { 
if (mVelocityTracker == null) { 
mVelocityTracker = VelocityTracker.obtain(); 
} 
mVelocityTracker.addMovement(event); 
} 
/** 
* 獲取手指在content介面滑動的速度。 
* 
* @return 滑動速度,以每秒鐘移動了多少畫素值為單位。 
*/ 
private int getScrollVelocity() { 
mVelocityTracker.computeCurrentVelocity(1000); 
int velocity = (int) mVelocityTracker.getXVelocity(); 
return Math.abs(velocity); 
} 
/** 
* 回收VelocityTracker物件。 
*/ 
private void recycleVelocityTracker() { 
mVelocityTracker.recycle(); 
mVelocityTracker = null; 
} 
class ScrollTask extends AsyncTask<Integer, Integer, Integer> { 
@Override 
protected Integer doInBackground(Integer... speed) { 
int leftMargin = menuParams.leftMargin; 
// 根據傳入的速度來滾動介面,當滾動到達左邊界或右邊界時,跳出迴圈。 
while (true) { 
leftMargin = leftMargin   speed[0]; 
if (leftMargin > rightEdge) { 
leftMargin = rightEdge; 
break; 
} 
if (leftMargin < leftEdge) { 
leftMargin = leftEdge; 
break; 
} 
publishProgress(leftMargin); 
// 為了要有滾動效果產生,每次迴圈使執行緒睡眠20毫秒,這樣肉眼才能夠看到滾動動畫。 
sleep(20); 
} 
if (speed[0] > 0) { 
isMenuVisible = true; 
} else { 
isMenuVisible = false; 
} 
return leftMargin; 
} 
@Override 
protected void onProgressUpdate(Integer... leftMargin) { 
menuParams.leftMargin = leftMargin[0]; 
menu.setLayoutParams(menuParams); 
} 
@Override 
protected void onPostExecute(Integer leftMargin) { 
menuParams.leftMargin = leftMargin; 
menu.setLayoutParams(menuParams); 
} 
} 
/** 
* 使當前執行緒睡眠指定的毫秒數。 
* 
* @param millis 
* 指定當前執行緒睡眠多久,以毫秒為單位 
*/ 
private void sleep(long millis) { 
try { 
Thread.sleep(millis); 
} catch (InterruptedException e) { 
e.printStackTrace(); 
} 
} 
} 

全部的程式碼都在這裡了,我們可以看到,加上註釋總共才兩百多行的程式碼就能實現滑動選單的特效。下面我來對以上程式碼解釋一下,首先初始化的時候呼叫initValues方法,在這裡面將內容佈局的寬度設定為螢幕的寬度,選單佈局的寬度設定為螢幕的寬度減去menuPadding值,這樣可以保證在選單佈局展示的時候,仍有一部分內容佈局可以看到。如果不在初始化的時候重定義兩個佈局寬度,就會按照layout檔案裡面宣告的一樣,兩個佈局都是fill_parent,這樣就無法實現滑動選單的效果了。然後將選單佈局的左偏移量設定為負的選單佈局的寬度,這樣選單佈局就會被完全隱藏,只有內容佈局會顯示在介面上。

之後給內容佈局註冊監聽事件,這樣當手指在內容佈局上滑動的時候就會觸發onTouch事件。在onTouch事件裡面,根據手指滑動的距離會改變選單佈局的左偏移量,從而控制選單佈局的顯示和隱藏。當手指離開螢幕的時候,會判斷應該滑動到選單佈局還是內容佈局,判斷依據是根據手指滑動的距離或者滑動的速度,細節可以看程式碼中的註釋。

最後還是給出AndroidManifest.xml的程式碼,都是自動生成的,非常簡單:


<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android" 
package="com.example.renrenslidemenudemo" 
android:versionCode="1" 
android:versionName="1.0" > 
<uses-sdk 
android:minSdkVersion="8" 
android:targetSdkVersion="8" /> 
<application 
android:allowBackup="true" 
android:icon="@drawable/ic_launcher" 
android:label="@string/app_name" 
android:theme="@android:style/Theme.NoTitleBar" > 
<activity 
android:name="com.example.renrenslidemenudemo.MainActivity" 
android:label="@string/app_name" > 
<intent-filter> 
<action android:name="android.intent.action.MAIN" /> 
<category android:name="android.intent.category.LAUNCHER" /> 
</intent-filter> 
</activity> 
</application> 
</manifest> 

好了,現在我們執行一下,看一下效果吧,首先是程式剛開啟的時候,顯示的是內容佈局。用手指在介面向右滑動,可以看到選單佈局出現。

                 

而當選單佈局完全展示的時候,效果如下圖:

今天大家看到了史上最簡單的滑動選單實現方案,確實是非常簡單。那麼有朋友也許會問了,在一個Activity當中這樣實現滑動選單是很簡單,可是如果我的應用程式有好多個Activity都需要滑動選單,每個Activity裡都這麼實現一遍,也變得複雜了。沒錯,當前的這個解決方案只適用於單個Activity中,如果是想在多個Activity中都實現滑動選單的效果,請參考我的另一篇文章 Android實現滑動選單特效的完全解析

有對雙向滑動選單感興趣的朋友請轉閱 Android實現雙向滑動特效的例項程式碼

好了,今天的講解到此結束,有疑問的朋友可以在下面留言。

原始碼下載,請點選這裡