Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發實例 >> Android自定義ViewGroup打造各種風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

編輯:Android開發實例

  首先我們先來看一看效果圖,第一個效果圖是一個最普通的側滑菜單,我們一會兒會先做出這種側滑菜單,然後再在此基礎上實現另外兩個效果

  第一種

Android自定義ViewGroup打造各種風格的SlidingMenu

  第二種

Android自定義ViewGroup打造各種風格的SlidingMenu

  第三種

Android自定義ViewGroup打造各種風格的SlidingMenu

  實現第一種側滑菜單,繼承自ViewGroup

  繼承自ViewGroup需要我們自己來測量,布局,實現滑動的效果,處理滑動沖突,這些都是一些新手無從下手的知識點,希望看了這篇文章後可以對大家有一個幫助

  自定義ViewGroup的一般思路是重寫onMeasure方法,在onMeasure方法中調用measureChild來測量子View,然後調用setMeasuredDimension來測量自己的大小。然後重寫onLayout方法,在onLayout中調用子View的layout方法來確定子View的位置,下面我們先來做好這兩件工作

Android自定義ViewGroup打造各種風格的SlidingMenu

  初始時候我們的Content應該是顯示在屏幕中的,而Menu應該是顯示在屏幕外的。當Menu打開時,應該是這種樣子的

Android自定義ViewGroup打造各種風格的SlidingMenu

  mMenuRightPadding是Menu距屏幕右側的一個距離,因為我們Menu打開後,Content還是會留一部分,而不是完全隱藏的

Java代碼
  1. public class MySlidingMenu extends ViewGroup {  
  2. public MySlidingMenu(Context context) {  
  3.         this(context, null, 0);  
  4.     }  
  5.   
  6.     public MySlidingMenu(Context context, AttributeSet attrs) {  
  7.         this(context, attrs, 0);  
  8.     }  
  9.   
  10.     public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {  
  11.         super(context, attrs, defStyleAttr);  
  12.         DisplayMetrics metrics = new DisplayMetrics();  
  13.         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
  14.         wm.getDefaultDisplay().getMetrics(metrics);  
  15.          //獲取屏幕的寬和高  
  16.         mScreenWidth = metrics.widthPixels;  
  17.         mScreenHeight = metrics.heightPixels;     
  18.          //設置Menu距離屏幕右側的距離,convertToDp是將代碼中的100轉換成100dp  
  19.         mMenuRightPadding = convertToDp(context,100);       
  20.     }  
  21.   
  22.  @Override  
  23.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  24.         //拿到Menu,Menu是第0個孩子  
  25.         mMenu = (ViewGroup) getChildAt(0);  
  26.         //拿到Content,Content是第1個孩子  
  27.         mContent = (ViewGroup) getChildAt(1);  
  28.         //設置Menu的寬為屏幕的寬度減去Menu距離屏幕右側的距離  
  29.         mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding;  
  30.         //設置Content的寬為屏幕的寬度  
  31.         mContentWidth = mContent.getLayoutParams().width = mScreenWidth;  
  32.         //測量Menu  
  33.         measureChild(mMenu,widthMeasureSpec,heightMeasureSpec);  
  34.         //測量Content  
  35.         measureChild(mContent, widthMeasureSpec, heightMeasureSpec);  
  36.         //測量自己,自己的寬度為Menu寬度加上Content寬度,高度為屏幕高度  
  37.         setMeasuredDimension(mMenuWidth + mContentWidth, mScreenHeight);  
  38.     }  
  39.   
  40. @Override  
  41.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  42.         //擺放Menu的位置,根據上面圖可以確定上下左右的坐標  
  43.         mMenu.layout(-mMenuWidth, 0, 0, mScreenHeight);  
  44.         //擺放Content的位置  
  45.         mContent.layout(0, 0, mScreenWidth, mScreenHeight);  
  46.     }  
  47.   
  48.   
  49. /** 
  50.      * 將傳進來的數轉化為dp 
  51.      */  
  52.     private int convertToDp(Context context , int num){  
  53.         return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,num,context.getResources().getDisplayMetrics());  
  54.     }  
  55. }  

  目前我們的側滑菜單中的兩個子View的位置應該是這個樣子

Android自定義ViewGroup打造各種風格的SlidingMenu

  接下來我們編寫xml布局文件

  left_menu.xml 左側菜單的布局文件,是一個ListView

XML/HTML代碼
  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.    <ListView  
  6.        android:id="@+id/menu_listview"  
  7.        android:layout_width="wrap_content"  
  8.        android:divider="@null"  
  9.        android:dividerHeight="0dp"  
  10.        android:scrollbars="none"  
  11.        android:layout_height="wrap_content">  
  12.    </ListView>  
  13. </RelativeLayout>  

  其中ListView的Item布局為left_menu_item.xml

XML/HTML代碼
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="horizontal" android:layout_width="match_parent"  
  4.     android:gravity="center_vertical"  
  5.     android:layout_height="match_parent">  
  6.     <ImageView  
  7.         android:id="@+id/menu_imageview"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:src="@drawable/menu_1"  
  11.         android:padding="20dp"  
  12.         />  
  13.     <TextView  
  14.         android:id="@+id/menu_textview"  
  15.         android:layout_width="wrap_content"  
  16.         android:layout_height="wrap_content"  
  17.         android:text="菜單1"  
  18.         android:textColor="#000000"  
  19.         android:textSize="20sp"  
  20.         />  
  21. </LinearLayout>  

  我們再來編寫內容區域的布局文件 content.xml 其中有一個header,header中有一個ImageView,這個ImageView是menu的開關,我們點擊他的時候可以自動開關menu,然後header下面也是一個listview

XML/HTML代碼
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="vertical" android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.     <LinearLayout  
  6.         android:layout_width="match_parent"  
  7.         android:layout_height="65dp"  
  8.         android:background="#000000"  
  9.         android:gravity="center_vertical"  
  10.         android:orientation="horizontal"  
  11.         >  
  12.         <ImageView  
  13.             android:id="@+id/menu_toggle"  
  14.             android:layout_width="40dp"  
  15.             android:layout_height="40dp"  
  16.             android:src="@drawable/toggle"  
  17.             android:paddingLeft="10dp"  
  18.             />  
  19.     </LinearLayout>  
  20.         <ListView  
  21.             android:id="@+id/content_listview"  
  22.             android:layout_width="match_parent"  
  23.             android:layout_height="wrap_content"  
  24.             android:dividerHeight="0dp"  
  25.             android:divider="@null"  
  26.             android:scrollbars="none"  
  27.             />  
  28. </LinearLayout>  

  content的item的布局文件為 content_item.xml

XML/HTML代碼
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="horizontal" android:layout_width="match_parent"  
  4.     android:gravity="center_vertical"  
  5.     android:background="#ffffff"  
  6.     android:layout_height="match_parent">  
  7.     <ImageView  
  8.         android:id="@+id/content_imageview"  
  9.         android:layout_width="80dp"  
  10.         android:layout_height="80dp"  
  11.         android:src="@drawable/content_1"  
  12.         android:layout_margin="20dp"  
  13.         />  
  14.     <TextView  
  15.         android:id="@+id/content_textview"  
  16.         android:layout_width="wrap_content"  
  17.         android:layout_height="wrap_content"  
  18.         android:text="Content - 1"  
  19.         android:textColor="#000000"  
  20.         android:textSize="20sp"/>  
  21.   
  22.   
  23. </LinearLayout>  

  在activity_main.xml中,我們將menu和content添加到我們的slidingMenu中

XML/HTML代碼
  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#aaaaaa"  
  5.    >  
  6. <com.example.user.slidingmenu.MySlidingMenu  
  7.     android:id="@+id/slidingmenu"  
  8.     android:layout_width="wrap_content"  
  9.     android:layout_height="match_parent"  
  10.     >  
  11.         <include  
  12.             android:id="@+id/menu"  
  13.             layout="@layout/left_menu"  
  14.             />  
  15.         <include  
  16.             android:id="@+id/content"  
  17.             layout="@layout/content"  
  18.             />  
  19. </com.example.user.slidingmenu.MySlidingMenu>  
  20.   
  21. </RelativeLayout>  

  現在應該是這種效果

Android自定義ViewGroup打造各種風格的SlidingMenu

  左側菜單是隱藏在屏幕左側外部的,但是現在還不能滑動,如果想要實現滑動功能,我們可以使用View的scrollTo和scrollBy方法,這兩個方法的區別是scrollTo是直接將view移動到指定的位置,scrollBy是相對於當前的位置移動一個偏移量,所以我們應該重寫onTouchEvent方法,用來計算出當前手指的一個偏移量,然後使用scrollBy方法一點一點的移動,就形成了一個可以跟隨手指移動的view的動畫效果了

  在寫代碼之前,我們先掃清一下障礙,我們先來弄清楚這些坐標是怎麼回事

Android自定義ViewGroup打造各種風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

  好了,把這些坐標弄清楚後,我們就簡單多了,下面直接看onTouchEvent方法

Java代碼
  1. @Override  
  2. public boolean onTouchEvent(MotionEvent event) {  
  3.     int action = event.getAction();  
  4.     switch (action){  
  5.         case MotionEvent.ACTION_DOWN:  
  6.             mLastX = (int) event.getX();  
  7.             mLastY = (int) event.getY();  
  8.             break;  
  9.         case MotionEvent.ACTION_MOVE:  
  10.             int currentX = (int) event.getX();  
  11.             int currentY = (int) event.getY();  
  12.             //拿到x方向的偏移量  
  13.             int dx = currentX - mLastX;  
  14.             if (dx < 0){//向左滑動  
  15.                 //邊界控制,如果Menu已經完全顯示,再滑動的話  
  16.                 //Menu左側就會出現白邊了,進行邊界控制  
  17.                 if (getScrollX() + Math.abs(dx) >= 0) {  
  18.                     //直接移動到(0,0)位置,不會出現白邊  
  19.                     scrollTo(0, 0);  
  20.   
  21.                 } else {//Menu沒有完全顯示呢  
  22.                     //其實這裡dx還是-dx,大家不用刻意去記  
  23.                     //大家可以先使用dx,然後運行一下,發現  
  24.                     //移動的方向是相反的,那麼果斷這裡加個負號就可以了  
  25.                     scrollBy(-dx, 0);  
  26.   
  27.                 }  
  28.   
  29.             }else{//向右滑動  
  30.                 //邊界控制,如果Content已經完全顯示,再滑動的話  
  31.                 //Content右側就會出現白邊了,進行邊界控制  
  32.                 if (getScrollX() - dx <= -mMenuWidth) {  
  33.                     //直接移動到(-mMenuWidth,0)位置,不會出現白邊  
  34.                     scrollTo(-mMenuWidth, 0);  
  35.   
  36.                 } else {//Content沒有完全顯示呢  
  37.                     //根據手指移動  
  38.                     scrollBy(-dx, 0);  
  39.   
  40.                 }  
  41.   
  42.             }  
  43.             mLastX = currentX;  
  44.             mLastY = currentY;  
  45.   
  46.             break;  
  47.   
  48.   
  49.     }  
  50.     return true;  
  51. }  

  現在我們的SlidingMenu依然是不能夠水平滑動的,但是listview可以豎直滑動,原因是我們的SlidingMenu默認是不攔截事件的,那麼事件會傳遞給他的子View去執行,也就是說傳遞給了Content的ListView去執行了,所以listview是可以滑動的,為了簡單,我們先重寫onInterceptTouchEvent方法,我們返回true,讓SlidingMenu攔截事件,我們的SlidingMenu就能夠滑動了,但是ListView是不能滑動的,等下我們會進行滑動沖突的處理,現在先實現SlidingMenu的功能

Java代碼
  1. @Override  
  2. public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.     return true;  
  4. }  

  好了,現在我們可以自由的滑動我們的SlidingMenu了,並且進行了很好的邊界控制,現在我們再添加個功能,就是當Menu打開大於二分之一時,松開手指,Menu自動打開。當Menu打開小於二分之一時,松開手指,Menu自動關閉。自動滑動的功能我們要借助Scroller來實現

  我們在構造方法中初始化一個Scroller

Java代碼
  1. public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) {  
  2.         super(context, attrs, defStyleAttr);  
  3.         ...  
  4.         mScroller = new Scroller(context);  
  5.         ...  
  6. }  

  然後重寫computeScroll方法,這個方法是保證Scroller自動滑動的必須方法,這是一個模板方法,到哪裡都這麼些就好了

Java代碼
  1. @Override  
  2. public void computeScroll() {  
  3.     if (mScroller.computeScrollOffset()){  
  4.         scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  5.         invalidate();  
  6.     }  
  7. }  

  接著我們在onTouchEvent的ACTION_UP中進行判斷,判斷當前menu打開了多少

Java代碼
  1. case MotionEvent.ACTION_UP:  
  2.                 if (getScrollX() < -mMenuWidth / 2){//打開Menu  
  3.                     //調用startScroll方法,第一個參數是起始X坐標,第二個參數  
  4.                     //是起始Y坐標,第三個參數是X方向偏移量,第四個參數是Y方向偏移量  
  5.                     mScroller.startScroll(getScrollX(), 0, -mMenuWidth - getScrollX(), 0, 300);  
  6.                     //設置一個已經打開的標識,當實現點擊開關自動打開關閉功能時會用到  
  7.                     isOpen = true;  
  8.                     //一定不要忘了調用這個方法重繪,否則沒有動畫效果  
  9.                     invalidate();  
  10.                 }else{//關閉Menu  
  11.                     //同上  
  12.                     mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 300);  
  13.                     isOpen = false;  
  14.                     invalidate();  
  15.                 }  
  16.   
  17.                 break;  

  關於startScroll中的startX和startY好判斷,那麼dx和dy怎麼計算呢?其實也非常簡單,比如我們startX坐標為30,我們想移動到-100,那麼startX+dx = -100 –> dx = -100 - startX –> dx = -130

  好了現在我們就可以實現松開手指後自動滑動的動畫效果了

  現在我們還需要點擊content中左上角的一個三角,如果當前menu沒有打開,則自動打開,如果已經打開,則自動關閉的功能,自動滑動的效果我們要借助Scroller.startScroll方法

Java代碼
  1.  /** 
  2.  * 點擊開關,開閉Menu,如果當前menu已經打開,則關閉,如果當前menu已經關閉,則打開 
  3.  */  
  4. public void toggleMenu(){  
  5.     if (isOpen){  
  6.         closeMenu();  
  7.     }else{  
  8.         openMenu();  
  9.     }  
  10. }  
  11.   
  12. /** 
  13.  * 關閉menu 
  14.  */  
  15. private void closeMenu() {  
  16.     //也是使用startScroll方法,dx和dy的計算方法一樣  
  17.     mScroller.startScroll(getScrollX(),0,-getScrollX(),0,500);  
  18.     invalidate();  
  19.     isOpen = false;  
  20. }  
  21.   
  22. /** 
  23.  * 打開menu 
  24.  */  
  25. private void openMenu() {  
  26.     mScroller.startScroll(getScrollX(),0,-mMenuWidth-getScrollX(),0,500);  
  27.     invalidate();  
  28.     isOpen = true;  
  29. }  

  然後我們可以在MainActivity中拿到我們content左上角三角形的imageview,然後給他設置一個點擊事件,調用我們的toggleMenu方法

Java代碼
  1. mMenuToggle.setOnClickListener(new View.OnClickListener() {  
  2.     @Override  
  3.     public void onClick(View v) {  
  4.         mSlidingMenu.toggleMenu();  
  5.     }  
  6. });  

  處理滑動沖突

  由於我們的menu和content是listview,listview是支持豎直滑動的,而我們的slidingMenu是支持水平滑動的,因此會出現滑動的沖突。剛才我們直接在onInterceptTouchEvent中返回了true,因此SlidingMenu就會攔截所有的事件,而ListView接收不到任何的事件,因此ListView不能滑動了,我們要解決這個滑動沖突很簡單,只需要判斷當前是水平滑動還是豎直滑動,如果是水平滑動的話則讓SlidingMenu攔截事件,如果是豎直滑動的話就不攔截事件,把事件交給子View的ListView去執行

Java代碼
  1. @Override  
  2. public boolean onInterceptTouchEvent(MotionEvent ev) {  
  3.     boolean intercept = false;  
  4.     int x = (int) ev.getX();  
  5.     int y = (int) ev.getY();  
  6.     switch (ev.getAction()){  
  7.         case MotionEvent.ACTION_DOWN:  
  8.             intercept = false;  
  9.             break;  
  10.         case MotionEvent.ACTION_MOVE:  
  11.             int deltaX = (int) ev.getX() - mLastXIntercept;  
  12.             int deltaY = (int) ev.getY() - mLastYIntercept;  
  13.             if (Math.abs(deltaX) > Math.abs(deltaY)){//橫向滑動  
  14.                 intercept = true;  
  15.             }else{//縱向滑動  
  16.                 intercept = false;  
  17.             }  
  18.             break;  
  19.         case MotionEvent.ACTION_UP:  
  20.             intercept = false;  
  21.             break;  
  22.     }  
  23.     mLastX = x;  
  24.     mLastY = y;  
  25.     mLastXIntercept = x;  
  26.     mLastYIntercept = y;  
  27.     return intercept;  
  28. }  

  好了,現在我們的滑動沖突就解決了,我們既可以水平滑動SlidingMenu,又可以豎直滑動ListView,那麼第一種SlidingMenu就已經實現了,我們再來看看另外兩種怎麼去實現

  實現第二種QQ V6.2.3風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

  這種SlidingMenu是和QQ v6.2.3 的側滑菜單風格一致的,我們發現Menu和Content的滑動速度是有一個速度差的,實際上我們可以通過修改Menu的偏移量來達到這種效果

Android自定義ViewGroup打造各種風格的SlidingMenu

  此時Menu的偏移量為mMenuWidth的2/3,當我們慢慢打開Menu的同時,修改Menu的偏移量,最終修改為0

Android自定義ViewGroup打造各種風格的SlidingMenu

  這樣就達到了一種速度差的效果,我們只需要在onTouchEvent的ACTION_MOVE和computeScroll中添加一行如下代碼就可以

Java代碼
  1. mMenu.setTranslationX(2*(mMenuWidth+getScrollX())/3);  

  我們分析一下,在最開始,mMenuWidth+getScrollX=mMenuWidth,再乘以2/3,得到的就是mMenuWidth的2/3 , 當我們滑動至Menu完全打開時,mMenuWidth+getScrollX=0 , 這就達到了我們的效果

  為什麼要在computeScroll中也添加這一行代碼呢,因為當我們滑動過程中,如果我們手指離開屏幕,ACTION_MOVE肯定就不執行了,但是當我們手指離開屏幕後,會有一段自動打開或者關閉的動畫,那麼這段動畫應該繼續去設置Menu的偏移量,因此我們在computeScroll中也要添加這一行代碼。

  好了,效果我們已經實現了,只需要去設置Menu的偏移量就可以了,是不是非常簡單

  實現第三種QQ V5.0風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

  這個效果中Menu有一個偏移的效果,透明度的變化以及放大的效果。Content中有一個縮小的效果。

  首先我們要有一個變量,用來記錄當前menu已經打開了多少百分比。

Android自定義ViewGroup打造各種風格的SlidingMenu

Android自定義ViewGroup打造各種風格的SlidingMenu

  這裡我們要注意,getScrollX得到的數值正好是負值,所以我們計算的時候要將getScrollX的值取絕對值再去計算,我們在onTouchEvent的MOVE中要計算這個值,同時在computeScroll方法中也要計算這個值,因為當我們手指抬起時,可能會執行一段自動打開或者關閉的動畫,那麼我們在MOVE中的計算肯定停止了,但是在執行動畫的過程中,是Scroller在起作用,那麼computeScroll就會執行直到動畫結束,因此我們要在computeScroll中同樣進行計算

Java代碼
  1. scale = Math.abs((float)getScrollX()) / (float) mMenuWidth;  

  scale的值是[0,1]的,因此我們就可以根據這個值來對menu的偏移量進行設置。

  我們可以通過設置View的setScaleX和setScaleY來對View進行放大縮小,當然這個縮放比例要根據我們的scale值來改變,首先我們的Menu有一個放大的效果,我們就指定為Menu從0.7放大到1.0,那麼我們就可以這樣寫

Java代碼
  1. mMenu.setScaleX(0.7f + 0.3f*scale);  
  2. mMenu.setScaleY(0.7f + 0.3f*scale);  

  透明度是從0到1的,所以我們直接用scale的值就可以了

Java代碼
  1. mMenu.setAlpha(scale);  

  我還給Menu設置了一個偏移量,這個偏移量大家可以自己計算,我是這樣計算的

Java代碼
  1. mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/2)*(1.0f-scale));  

  設置完Menu後,我們再來設置Content,Content的大小是從1.0縮小到0.7,因此我們這樣寫

Java代碼
  1. mContent.setScaleX(1 - 0.3f*scale);  
  2. mContent.setPivotX(0);  
  3. mContent.setScaleY(1.0f - 0.3f * scale);  

  其中mContent.setPivotX(0)是讓Content的縮放中心店的X軸坐標為0點

  我們可以將這個變化的過程抽取為一個方法

Java代碼
  1. private void slidingMode3(){  
  2.     mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/2)*(1.0f-scale));  
  3.     mMenu.setScaleX(0.7f + 0.3f*scale);  
  4.     mMenu.setScaleY(0.7f + 0.3f*scale);  
  5.     mMenu.setAlpha(scale);  
  6.   
  7.     mContent.setScaleX(1 - 0.3f*scale);  
  8.     mContent.setPivotX(0);  
  9.     mContent.setScaleY(1.0f - 0.3f * scale);  
  10. }  

  將這個方法添加到onTouchEvent的ACTION_MOVE和computeScroll中就可以了。

  我們看到所有的滑動風格都是在基於第一種基礎上,修改Menu或者Content的translationX或者scaleX scaleY的值來決定的,因此我們可以打造各種各樣的SlidingMenu來。

  完整代碼大家可以到GitHub中下載

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved