1. 程式人生 > >Android滑動選單特效實現,側滑選單實現

Android滑動選單特效實現,側滑選單實現

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

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

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

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

將選單佈局的左偏移值改成0時,效果圖如下:

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

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="fill_parent"  
  4.     android:layout_height="fill_parent"  
  5.     android:orientation="horizontal"  
  6.     tools:context=".MainActivity" >  
  7.     <LinearLayout  
  8.         android:id="@ id/menu"  
  9.         android:layout_width="fill_parent"  
  10.         android:layout_height="fill_parent"  
  11.         android:background="@drawable/menu" >  
  12.     </LinearLayout>  
  13.     <LinearLayout  
  14.         android:id="@ id/content"  
  15.         android:layout_width="fill_parent"  
  16.         android:layout_height="fill_parent"  
  17.         android:background="@drawable/content" >  
  18.     </LinearLayout>  
  19. </LinearLayout>  

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

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

  1. public class MainActivity extends Activity implements OnTouchListener {  
  2.     /** 
  3.      * 滾動顯示和隱藏menu時,手指滑動需要達到的速度。 
  4.      */  
  5.     public static final int SNAP_VELOCITY = 200;  
  6.     /** 
  7.      * 螢幕寬度值。 
  8.      */  
  9.     private int screenWidth;  
  10.     /** 
  11.      * menu最多可以滑動到的左邊緣。值由menu佈局的寬度來定,marginLeft到達此值之後,不能再減少。 
  12.      */  
  13.     private int leftEdge;  
  14.     /** 
  15.      * menu最多可以滑動到的右邊緣。值恆為0,即marginLeft到達0之後,不能增加。 
  16.      */  
  17.     private int rightEdge = 0;  
  18.     /** 
  19.      * menu完全顯示時,留給content的寬度值。 
  20.      */  
  21.     private int menuPadding = 80;  
  22.     /** 
  23.      * 主內容的佈局。 
  24.      */  
  25.     private View content;  
  26.     /** 
  27.      * menu的佈局。 
  28.      */  
  29.     private View menu;  
  30.     /** 
  31.      * menu佈局的引數,通過此引數來更改leftMargin的值。 
  32.      */  
  33.     private LinearLayout.LayoutParams menuParams;  
  34.     /** 
  35.      * 記錄手指按下時的橫座標。 
  36.      */  
  37.     private float xDown;  
  38.     /** 
  39.      * 記錄手指移動時的橫座標。 
  40.      */  
  41.     private float xMove;  
  42.     /** 
  43.      * 記錄手機擡起時的橫座標。 
  44.      */  
  45.     private float xUp;  
  46.     /** 
  47.      * menu當前是顯示還是隱藏。只有完全顯示或隱藏menu時才會更改此值,滑動過程中此值無效。 
  48.      */  
  49.     private boolean isMenuVisible;  
  50.     /** 
  51.      * 用於計算手指滑動的速度。 
  52.      */  
  53.     private VelocityTracker mVelocityTracker;  
  54.     @Override  
  55.     protected void onCreate(Bundle savedInstanceState) {  
  56.         super.onCreate(savedInstanceState);  
  57.         setContentView(R.layout.activity_main);  
  58.         initValues();  
  59.         content.setOnTouchListener(this);  
  60.     }  
  61.     /** 
  62.      * 初始化一些關鍵性資料。包括獲取螢幕的寬度,給content佈局重新設定寬度,給menu佈局重新設定寬度和偏移距離等。 
  63.      */  
  64.     private void initValues() {  
  65.         WindowManager window = (WindowManager) getSystemService(Context.WINDOW_SERVICE);  
  66.         screenWidth = window.getDefaultDisplay().getWidth();  
  67.         content = findViewById(R.id.content);  
  68.         menu = findViewById(R.id.menu);  
  69.         menuParams = (LinearLayout.LayoutParams) menu.getLayoutParams();  
  70.         // 將menu的寬度設定為螢幕寬度減去menuPadding  
  71.         menuParams.width = screenWidth - menuPadding;  
  72.         // 左邊緣的值賦值為menu寬度的負數  
  73.         leftEdge = -menuParams.width;  
  74.         // menu的leftMargin設定為左邊緣的值,這樣初始化時menu就變為不可見  
  75.         menuParams.leftMargin = leftEdge;  
  76.         // 將content的寬度設定為螢幕寬度  
  77.         content.getLayoutParams().width = screenWidth;  
  78.     }  
  79.     @Override  
  80.     public boolean onTouch(View v, MotionEvent event) {  
  81.         createVelocityTracker(event);  
  82.         switch (event.getAction()) {  
  83.         case MotionEvent.ACTION_DOWN:  
  84.             // 手指按下時,記錄按下時的橫座標  
  85.             xDown = event.getRawX();  
  86.             break;  
  87.         case MotionEvent.ACTION_MOVE:  
  88.             // 手指移動時,對比按下時的橫座標,計算出移動的距離,來調整menu的leftMargin值,從而顯示和隱藏menu  
  89.             xMove = event.getRawX();  
  90.             int distanceX = (int) (xMove - xDown);  
  91.             if (isMenuVisible) {  
  92.                 menuParams.leftMargin = distanceX;  
  93.             } else {  
  94.                 menuParams.leftMargin = leftEdge   distanceX;  
  95.             }  
  96.             if (menuParams.leftMargin < leftEdge) {  
  97.                 menuParams.leftMargin = leftEdge;  
  98.             } else if (menuParams.leftMargin > rightEdge) {  
  99.                 menuParams.leftMargin = rightEdge;  
  100.             }  
  101.             menu.setLayoutParams(menuParams);  
  102.             break;  
  103.         case MotionEvent.ACTION_UP:  
  104.             // 手指擡起時,進行判斷當前手勢的意圖,從而決定是滾動到menu介面,還是滾動到content介面  
  105.             xUp = event.getRawX();  
  106.             if (wantToShowMenu()) {  
  107.                 if (shouldScrollToMenu()) {  
  108.                     scrollToMenu();  
  109.                 } else {  
  110.                     scrollToContent();  
  111.                 }  
  112.             } else if (wantToShowContent()) {  
  113.                 if (shouldScrollToContent()) {  
  114.                     scrollToContent();  
  115.                 } else {  
  116.                     scrollToMenu();  
  117.                 }  
  118.             }  
  119.             recycleVelocityTracker();  
  120.             break;  
  121.         }  
  122.         return true;  
  123.     }  
  124.     /** 
  125.      * 判斷當前手勢的意圖是不是想顯示content。如果手指移動的距離是負數,且當前menu是可見的,則認為當前手勢是想要顯示content。 
  126.      *  
  127.      * @return 當前手勢想顯示content返回true,否則返回false。 
  128.      */  
  129.     private boolean wantToShowContent() {  
  130.         return xUp - xDown < 0 && isMenuVisible;  
  131.     }  
  132.     /** 
  133.      * 判斷當前手勢的意圖是不是想顯示menu。如果手指移動的距離是正數,且當前menu是不可見的,則認為當前手勢是想要顯示menu。 
  134.      *  
  135.      * @return 當前手勢想顯示menu返回true,否則返回false。 
  136.      */  
  137.     private boolean wantToShowMenu() {  
  138.         return xUp - xDown > 0 && !isMenuVisible;  
  139.     }  
  140.     /** 
  141.      * 判斷是否應該滾動將menu展示出來。如果手指移動距離大於螢幕的1/2,或者手指移動速度大於SNAP_VELOCITY, 
  142.      * 就認為應該滾動將menu展示出來。 
  143.      *  
  144.      * @return 如果應該滾動將menu展示出來返回true,否則返回false。 
  145.      */  
  146.     private boolean shouldScrollToMenu() {  
  147.         return xUp - xDown > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;  
  148.     }  
  149.     /** 
  150.      * 判斷是否應該滾動將content展示出來。如果手指移動距離加上menuPadding大於螢幕的1/2, 
  151.      * 或者手指移動速度大於SNAP_VELOCITY, 就認為應該滾動將content展示出來。 
  152.      *  
  153.      * @return 如果應該滾動將content展示出來返回true,否則返回false。 
  154.      */  
  155.     private boolean shouldScrollToContent() {  
  156.         return xDown - xUp   menuPadding > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;  
  157.     }  
  158.     /** 
  159.      * 將螢幕滾動到menu介面,滾動速度設定為30. 
  160.      */  
  161.     private void scrollToMenu() {  
  162.         new ScrollTask().execute(30);  
  163.     }  
  164.     /** 
  165.      * 將螢幕滾動到content介面,滾動速度設定為-30. 
  166.      */  
  167.     private void scrollToContent() {  
  168.         new ScrollTask().execute(-30);  
  169.     }  
  170.     /** 
  171.      * 建立VelocityTracker物件,並將觸控content介面的滑動事件加入到VelocityTracker當中。 
  172.      *  
  173.      * @param event 
  174.      *            content介面的滑動事件 
  175.      */  
  176.     private void createVelocityTracker(MotionEvent event) {  
  177.         if (mVelocityTracker == null) {  
  178.             mVelocityTracker = VelocityTracker.obtain();  
  179.         }  
  180.         mVelocityTracker.addMovement(event);  
  181.     }  
  182.     /** 
  183.      * 獲取手指在content介面滑動的速度。 
  184.      *  
  185.      * @return 滑動速度,以每秒鐘移動了多少畫素值為單位。 
  186.      */  
  187.     private int getScrollVelocity() {  
  188.         mVelocityTracker.computeCurrentVelocity(1000);  
  189.         int velocity = (int) mVelocityTracker.getXVelocity();  
  190.         return Math.abs(velocity);  
  191.     }  
  192.     /** 
  193.      * 回收VelocityTracker物件。 
  194.      */  
  195.     private void recycleVelocityTracker() {  
  196.         mVelocityTracker.recycle();  
  197.         mVelocityTracker = null;  
  198.     }  
  199.     class ScrollTask extends AsyncTask<Integer, Integer, Integer> {  
  200.         @Override  
  201.         protected Integer doInBackground(Integer... speed) {  
  202.             int leftMargin = menuParams.leftMargin;  
  203.             // 根據傳入的速度來滾動介面,當滾動到達左邊界或右邊界時,跳出迴圈。  
  204.             while (true) {  
  205.                 leftMargin = leftMargin   speed[0];  
  206.                 if (leftMargin > rightEdge) {  
  207.                     leftMargin = rightEdge;  
  208.                     break;  
  209.                 }  
  210.                 if (leftMargin < leftEdge) {  
  211.                     leftMargin = leftEdge;  
  212.                     break;  
  213.                 }  
  214.                 publishProgress(leftMargin);  
  215.                 // 為了要有滾動效果產生,每次迴圈使執行緒睡眠20毫秒,這樣肉眼才能夠看到滾動動畫。  
  216.                 sleep(20);  
  217.             }  
  218.             if (speed[0] > 0) {  
  219.                 isMenuVisible = true;  
  220.             } else {  
  221.                 isMenuVisible = false;  
  222.             }  
  223.             return leftMargin;  
  224.         }  
  225.         @Override  
  226.         protected void onProgressUpdate(Integer... leftMargin) {  
  227.             menuParams.leftMargin = leftMargin[0];  
  228.             menu.setLayoutParams(menuParams);  
  229.         }  
  230.         @Override  
  231.         protected void onPostExecute(Integer leftMargin) {  
  232.             menuParams.leftMargin = leftMargin;  
  233.             menu.setLayoutParams(menuParams);  
  234.         }  
  235.     }  
  236.     /** 
  237.      * 使當前執行緒睡眠指定的毫秒數。 
  238.      *  
  239.      * @param millis 
  240.      *            指定當前執行緒睡眠多久,以毫秒為單位 
  241.      */  
  242.     private void sleep(long millis) {  
  243.         try {  
  244.             Thread.sleep(millis);  
  245.         } catch (InterruptedException e) {  
  246.             e.printStackTrace();  
  247.         }  
  248.     }  
  249. }  

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

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

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

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     package="com.example.renrenslidemenudemo"  
  4.     android:versionCode="1"  
  5.     android:versionName="1.0" >  
  6.     <uses-sdk  
  7.         android:minSdkVersion="8"  
  8.         android:targetSdkVersion="8" />  
  9.     <application  
  10.         android:allowBackup="true"  
  11.         android:icon="@drawable/ic_launcher"  
  12.         android:label="@string/app_name"  
  13.         android:theme="@android:style/Theme.NoTitleBar" >  
  14.         <activity  
  15.             android:name="com.example.renrenslidemenudemo.MainActivity"  
  16.             android:label="@string/app_name" >  
  17.             <intent-filter>  
  18.                 <action android:name="android.intent.action.MAIN" />  
  19.                 <category android:name="android.intent.category.LAUNCHER" />  
  20.             </intent-filter>  
  21.         </activity>  
  22.     </application>  
  23. </manifest>  

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

                             

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