Android滑动菜单框架完全解析,教你如何一分钟实现滑动菜单特效

之前我向大家介绍了史上最简单的滑动菜单的实现方式,相信大家都还记得。如果忘记了其中的实现原理或者还没看过的朋友,请先去看一遍之前的文章 Android滑动菜单特效实现,仿人人客户端侧滑效果,史上最简单的侧滑实现 ,因为我们今天要实现的滑动菜单框架也是基于同样的原理的。

之前的文章中在最后也提到了,如果是你的应用程序中有很多个Activity都需要加入滑动菜单的功能,那么每个Activity都要写上百行的代码才能实现效果,再简单的滑动菜单实现方案也没用。因此我们今天要实现一个滑动菜单的框架,然后在任何Activity中都可以一分钟引入滑动菜单功能。

首先还是讲一下实现原理。说是滑动菜单的框架,其实说白了也很简单,就是我们自定义一个布局,在这个自定义布局中实现好滑动菜单的功能,然后只要在Activity的布局文件里面引入我们自定义的布局,这个Activity就拥有了滑动菜单的功能了。原理讲完了,是不是很简单?下面我们来动手实现吧。

在Eclipse中新建一个Android项目,项目名就叫做RenRenSlidingLayout。

新建一个类,名叫SlidingLayout,这个类是继承自LinearLayout的,并且实现了OnTouchListener接口,具体代码如下:

[java] view plain copy
  1. public class SlidingLayout extends LinearLayout implements OnTouchListener {  
  2.   
  3.     /** 
  4.      * 滚动显示和隐藏左侧布局时,手指滑动需要达到的速度。 
  5.      */  
  6.     public static final int SNAP_VELOCITY = 200;  
  7.   
  8.     /** 
  9.      * 屏幕宽度值。 
  10.      */  
  11.     private int screenWidth;  
  12.   
  13.     /** 
  14.      * 左侧布局最多可以滑动到的左边缘。值由左侧布局的宽度来定,marginLeft到达此值之后,不能再减少。 
  15.      */  
  16.     private int leftEdge;  
  17.   
  18.     /** 
  19.      * 左侧布局最多可以滑动到的右边缘。值恒为0,即marginLeft到达0之后,不能增加。 
  20.      */  
  21.     private int rightEdge = 0;  
  22.   
  23.     /** 
  24.      * 左侧布局完全显示时,留给右侧布局的宽度值。 
  25.      */  
  26.     private int leftLayoutPadding = 80;  
  27.   
  28.     /** 
  29.      * 记录手指按下时的横坐标。 
  30.      */  
  31.     private float xDown;  
  32.   
  33.     /** 
  34.      * 记录手指移动时的横坐标。 
  35.      */  
  36.     private float xMove;  
  37.   
  38.     /** 
  39.      * 记录手机抬起时的横坐标。 
  40.      */  
  41.     private float xUp;  
  42.   
  43.     /** 
  44.      * 左侧布局当前是显示还是隐藏。只有完全显示或隐藏时才会更改此值,滑动过程中此值无效。 
  45.      */  
  46.     private boolean isLeftLayoutVisible;  
  47.   
  48.     /** 
  49.      * 左侧布局对象。 
  50.      */  
  51.     private View leftLayout;  
  52.   
  53.     /** 
  54.      * 右侧布局对象。 
  55.      */  
  56.     private View rightLayout;  
  57.   
  58.     /** 
  59.      * 用于监听侧滑事件的View。 
  60.      */  
  61.     private View mBindView;  
  62.   
  63.     /** 
  64.      * 左侧布局的参数,通过此参数来重新确定左侧布局的宽度,以及更改leftMargin的值。 
  65.      */  
  66.     private MarginLayoutParams leftLayoutParams;  
  67.   
  68.     /** 
  69.      * 右侧布局的参数,通过此参数来重新确定右侧布局的宽度。 
  70.      */  
  71.     private MarginLayoutParams rightLayoutParams;  
  72.   
  73.     /** 
  74.      * 用于计算手指滑动的速度。 
  75.      */  
  76.     private VelocityTracker mVelocityTracker;  
  77.   
  78.     /** 
  79.      * 重写SlidingLayout的构造函数,其中获取了屏幕的宽度。 
  80.      *  
  81.      * @param context 
  82.      * @param attrs 
  83.      */  
  84.     public SlidingLayout(Context context, AttributeSet attrs) {  
  85.         super(context, attrs);  
  86.         WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
  87.         screenWidth = wm.getDefaultDisplay().getWidth();  
  88.     }  
  89.   
  90.     /** 
  91.      * 绑定监听侧滑事件的View,即在绑定的View进行滑动才可以显示和隐藏左侧布局。 
  92.      *  
  93.      * @param bindView 
  94.      *            需要绑定的View对象。 
  95.      */  
  96.     public void setScrollEvent(View bindView) {  
  97.         mBindView = bindView;  
  98.         mBindView.setOnTouchListener(this);  
  99.     }  
  100.   
  101.     /** 
  102.      * 将屏幕滚动到左侧布局界面,滚动速度设定为30. 
  103.      */  
  104.     public void scrollToLeftLayout() {  
  105.         new ScrollTask().execute(30);  
  106.     }  
  107.   
  108.     /** 
  109.      * 将屏幕滚动到右侧布局界面,滚动速度设定为-30. 
  110.      */  
  111.     public void scrollToRightLayout() {  
  112.         new ScrollTask().execute(-30);  
  113.     }  
  114.   
  115.     /** 
  116.      * 左侧布局是否完全显示出来,或完全隐藏,滑动过程中此值无效。 
  117.      *  
  118.      * @return 左侧布局完全显示返回true,完全隐藏返回false。 
  119.      */  
  120.     public boolean isLeftLayoutVisible() {  
  121.         return isLeftLayoutVisible;  
  122.     }  
  123.   
  124.     /** 
  125.      * 在onLayout中重新设定左侧布局和右侧布局的参数。 
  126.      */  
  127.     @Override  
  128.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  129.         super.onLayout(changed, l, t, r, b);  
  130.         if (changed) {  
  131.             // 获取左侧布局对象  
  132.             leftLayout = getChildAt(0);  
  133.             leftLayoutParams = (MarginLayoutParams) leftLayout.getLayoutParams();  
  134.             // 重置左侧布局对象的宽度为屏幕宽度减去leftLayoutPadding  
  135.             leftLayoutParams.width = screenWidth - leftLayoutPadding;  
  136.             // 设置最左边距为负的左侧布局的宽度  
  137.             leftEdge = -leftLayoutParams.width;  
  138.             leftLayoutParams.leftMargin = leftEdge;  
  139.             leftLayout.setLayoutParams(leftLayoutParams);  
  140.             // 获取右侧布局对象  
  141.             rightLayout = getChildAt(1);  
  142.             rightLayoutParams = (MarginLayoutParams) rightLayout.getLayoutParams();  
  143.             rightLayoutParams.width = screenWidth;  
  144.             rightLayout.setLayoutParams(rightLayoutParams);  
  145.         }  
  146.     }  
  147.   
  148.     @Override  
  149.     public boolean onTouch(View v, MotionEvent event) {  
  150.         createVelocityTracker(event);  
  151.         switch (event.getAction()) {  
  152.         case MotionEvent.ACTION_DOWN:  
  153.             // 手指按下时,记录按下时的横坐标  
  154.             xDown = event.getRawX();  
  155.             break;  
  156.         case MotionEvent.ACTION_MOVE:  
  157.             // 手指移动时,对比按下时的横坐标,计算出移动的距离,来调整左侧布局的leftMargin值,从而显示和隐藏左侧布局  
  158.             xMove = event.getRawX();  
  159.             int distanceX = (int) (xMove - xDown);  
  160.             if (isLeftLayoutVisible) {  
  161.                 leftLayoutParams.leftMargin = distanceX;  
  162.             } else {  
  163.                 leftLayoutParams.leftMargin = leftEdge + distanceX;  
  164.             }  
  165.             if (leftLayoutParams.leftMargin < leftEdge) {  
  166.                 leftLayoutParams.leftMargin = leftEdge;  
  167.             } else if (leftLayoutParams.leftMargin > rightEdge) {  
  168.                 leftLayoutParams.leftMargin = rightEdge;  
  169.             }  
  170.             leftLayout.setLayoutParams(leftLayoutParams);  
  171.             break;  
  172.         case MotionEvent.ACTION_UP:  
  173.             // 手指抬起时,进行判断当前手势的意图,从而决定是滚动到左侧布局,还是滚动到右侧布局  
  174.             xUp = event.getRawX();  
  175.             if (wantToShowLeftLayout()) {  
  176.                 if (shouldScrollToLeftLayout()) {  
  177.                     scrollToLeftLayout();  
  178.                 } else {  
  179.                     scrollToRightLayout();  
  180.                 }  
  181.             } else if (wantToShowRightLayout()) {  
  182.                 if (shouldScrollToContent()) {  
  183.                     scrollToRightLayout();  
  184.                 } else {  
  185.                     scrollToLeftLayout();  
  186.                 }  
  187.             }  
  188.             recycleVelocityTracker();  
  189.             break;  
  190.         }  
  191.         return isBindBasicLayout();  
  192.     }  
  193.   
  194.     /** 
  195.      * 判断当前手势的意图是不是想显示右侧布局。如果手指移动的距离是负数,且当前左侧布局是可见的,则认为当前手势是想要显示右侧布局。 
  196.      *  
  197.      * @return 当前手势想显示右侧布局返回true,否则返回false。 
  198.      */  
  199.     private boolean wantToShowRightLayout() {  
  200.         return xUp - xDown < 0 && isLeftLayoutVisible;  
  201.     }  
  202.   
  203.     /** 
  204.      * 判断当前手势的意图是不是想显示左侧布局。如果手指移动的距离是正数,且当前左侧布局是不可见的,则认为当前手势是想要显示左侧布局。 
  205.      *  
  206.      * @return 当前手势想显示左侧布局返回true,否则返回false。 
  207.      */  
  208.     private boolean wantToShowLeftLayout() {  
  209.         return xUp - xDown > 0 && !isLeftLayoutVisible;  
  210.     }  
  211.   
  212.     /** 
  213.      * 判断是否应该滚动将左侧布局展示出来。如果手指移动距离大于屏幕的1/2,或者手指移动速度大于SNAP_VELOCITY, 
  214.      * 就认为应该滚动将左侧布局展示出来。 
  215.      *  
  216.      * @return 如果应该滚动将左侧布局展示出来返回true,否则返回false。 
  217.      */  
  218.     private boolean shouldScrollToLeftLayout() {  
  219.         return xUp - xDown > screenWidth / 2 || getScrollVelocity() > SNAP_VELOCITY;  
  220.     }  
  221.   
  222.     /** 
  223.      * 判断是否应该滚动将右侧布局展示出来。如果手指移动距离加上leftLayoutPadding大于屏幕的1/2, 
  224.      * 或者手指移动速度大于SNAP_VELOCITY, 就认为应该滚动将右侧布局展示出来。 
  225.      *  
  226.      * @return 如果应该滚动将右侧布局展示出来返回true,否则返回false。 
  227.      */  
  228.     private boolean shouldScrollToContent() {  
  229.         return xDown - xUp + leftLayoutPadding > screenWidth / 2  
  230.                 || getScrollVelocity() > SNAP_VELOCITY;  
  231.     }  
  232.   
  233.     /** 
  234.      * 判断绑定滑动事件的View是不是一个基础layout,不支持自定义layout,只支持四种基本layout, 
  235.      * AbsoluteLayout已被弃用。 
  236.      *  
  237.      * @return 如果绑定滑动事件的View是LinearLayout,RelativeLayout,FrameLayout, 
  238.      *         TableLayout之一就返回true,否则返回false。 
  239.      */  
  240.     private boolean isBindBasicLayout() {  
  241.         if (mBindView == null) {  
  242.             return false;  
  243.         }  
  244.         String viewName = mBindView.getClass().getName();  
  245.         return viewName.equals(LinearLayout.class.getName())  
  246.                 || viewName.equals(RelativeLayout.class.getName())  
  247.                 || viewName.equals(FrameLayout.class.getName())  
  248.                 || viewName.equals(TableLayout.class.getName());  
  249.     }  
  250.   
  251.     /** 
  252.      * 创建VelocityTracker对象,并将触摸事件加入到VelocityTracker当中。 
  253.      *  
  254.      * @param event 
  255.      *            右侧布局监听控件的滑动事件 
  256.      */  
  257.     private void createVelocityTracker(MotionEvent event) {  
  258.         if (mVelocityTracker == null) {  
  259.             mVelocityTracker = VelocityTracker.obtain();  
  260.         }  
  261.         mVelocityTracker.addMovement(event);  
  262.     }  
  263.   
  264.     /** 
  265.      * 获取手指在右侧布局的监听View上的滑动速度。 
  266.      *  
  267.      * @return 滑动速度,以每秒钟移动了多少像素值为单位。 
  268.      */  
  269.     private int getScrollVelocity() {  
  270.         mVelocityTracker.computeCurrentVelocity(1000);  
  271.         int velocity = (int) mVelocityTracker.getXVelocity();  
  272.         return Math.abs(velocity);  
  273.     }  
  274.   
  275.     /** 
  276.      * 回收VelocityTracker对象。 
  277.      */  
  278.     private void recycleVelocityTracker() {  
  279.         mVelocityTracker.recycle();  
  280.         mVelocityTracker = null;  
  281.     }  
  282.   
  283.     class ScrollTask extends AsyncTask {  
  284.   
  285.         @Override  
  286.         protected Integer doInBackground(Integer... speed) {  
  287.             int leftMargin = leftLayoutParams.leftMargin;  
  288.             // 根据传入的速度来滚动界面,当滚动到达左边界或右边界时,跳出循环。  
  289.             while (true) {  
  290.                 leftMargin = leftMargin + speed[0];  
  291.                 if (leftMargin > rightEdge) {  
  292.                     leftMargin = rightEdge;  
  293.                     break;  
  294.                 }  
  295.                 if (leftMargin < leftEdge) {  
  296.                     leftMargin = leftEdge;  
  297.                     break;  
  298.                 }  
  299.                 publishProgress(leftMargin);  
  300.                 // 为了要有滚动效果产生,每次循环使线程睡眠20毫秒,这样肉眼才能够看到滚动动画。  
  301.                 sleep(20);  
  302.             }  
  303.             if (speed[0] > 0) {  
  304.                 isLeftLayoutVisible = true;  
  305.             } else {  
  306.                 isLeftLayoutVisible = false;  
  307.             }  
  308.             return leftMargin;  
  309.         }  
  310.   
  311.         @Override  
  312.         protected void onProgressUpdate(Integer... leftMargin) {  
  313.             leftLayoutParams.leftMargin = leftMargin[0];  
  314.             leftLayout.setLayoutParams(leftLayoutParams);  
  315.         }  
  316.   
  317.         @Override  
  318.         protected void onPostExecute(Integer leftMargin) {  
  319.             leftLayoutParams.leftMargin = leftMargin;  
  320.             leftLayout.setLayoutParams(leftLayoutParams);  
  321.         }  
  322.     }  
  323.   
  324.     /** 
  325.      * 使当前线程睡眠指定的毫秒数。 
  326.      *  
  327.      * @param millis 
  328.      *            指定当前线程睡眠多久,以毫秒为单位 
  329.      */  
  330.     private void sleep(long millis) {  
  331.         try {  
  332.             Thread.sleep(millis);  
  333.         } catch (InterruptedException e) {  
  334.             e.printStackTrace();  
  335.         }  
  336.     }  
  337. }  
看到这里,我相信大家一定会觉得这些代码非常熟悉。没错,基本上这些代码和之前那篇文章的代码大同小异,只不过以前这些代码是写在Activity里的,而现在我们移动到了自定义的View当中。

接着我来说明一下和以前不同的部分。我们可以看到,这里将onLayout方法进行了重写,使用getChildAt(0)获取到的布局作为左边布局,使用getChildAt(1)获取到的布局作为右边布局。并将左边布局的宽度重定义为屏幕宽度减去leftLayoutPadding,将右侧布局的宽度重定义为屏幕宽度。然后让左边布局偏移出屏幕,这样能看到的就只有右边布局了。因此在这里我们也可以看出,使用SlidingLayout这个布局的前提条件,必须为这个布局提供两个子元素,第一个元素会作为左边布局偏移出屏幕,第二个元素会作为右边布局显示在屏幕上。

然后我们看一下setScrollEvent方法,这个方法接收一个View作为参数,然后为这个View绑定了一个touch事件。这是什么意思呢?让我们来想象一个场景,如果右侧布局是一个LinearLayout,我可以通过监听LinearLayout上的touch事件来控制左侧布局的显示和隐藏。但是如果右侧布局的LinearLayout里面加入了一个ListView,而这个ListView又充满了整个LinearLayout,这个时候LinearLayout将不可能再被touch到了,这个时候我们就需要将touch事件注册到ListView上。setScrollEvent方法也就是提供了一个注册接口,touch事件将会注册到传入的View上。

最后还有一个陌生的方法,isBindBasicLayout。这个方法就是判断了一下注册touch事件的View是不是四个基本布局之一,如果是就返回true,否则返回false。这个方法在整个SlidingLayout中起着非常重要的作用,主要用于控制onTouch事件是返回true还是false,这将影响到布局当中的View的功能是否可用。由于里面牵扯到了Android的事件转发机制,内容比较多,就不在这里详细解释了,我会考虑以后专门写一篇文章来介绍Android的事件机制。这里就先简单记住如果是基本布局就返回true,否则就返回false。

好了,我们的SlidingLayout写完了,接下来就是见证奇迹的时刻,让我们一起看看如何一分钟在Activity中引入滑动菜单功能。

创建或打开layout目录下的activity_main.xml文件,加入如下代码:

[html] view plain copy
  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.   
  8.       
  9.   
  10.     <com.example.slide.SlidingLayout  
  11.         android:id="@+id/slidingLayout"  
  12.         android:layout_width="fill_parent"  
  13.         android:layout_height="fill_parent"  
  14.         android:orientation="horizontal" >  
  15.   
  16.         

你可能感兴趣的:(Android应用,侧滑菜单)