首先我们先来看一看效果图,第一个效果图是一个最普通的侧滑菜单,我们一会儿会先做出这种侧滑菜单,然后再在此基础上实现另外两个效果

  第一种

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, null0);  
  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, 00, mScreenHeight);  
  44.         //摆放Content的位置  
  45.         mContent.layout(00, 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(00);  
  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(), 0300);  
  6.                     //设置一个已经打开的标识,当实现点击开关自动打开关闭功能时会用到  
  7.                     isOpen = true;  
  8.                     //一定不要忘了调用这个方法重绘,否则没有动画效果  
  9.                     invalidate();  
  10.                 }else{//关闭Menu  
  11.                     //同上  
  12.                     mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0300);  
  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中下载

本文发布:Android开发网
本文地址:http://www.jizhuomi.com/android/example/537.html
2016年2月22日
发布:鸡啄米 分类:Android开发实例 浏览: 评论:0