从早期的 SlidingMenu 再到 AndroidResideMenu 最后到Android自带的DrawerLayout,无处不体现着侧滑菜单的诱人魅力。侧滑菜单可以拓展app的内容,充分利用手机屏幕,增加程序的可玩性。
看完了上面的gif,想不想自己也写一个呢,那还等什么,一起来看看喽。
首先来说一下侧滑菜单实现的思路:侧滑菜单的布局为MenuLayout,还有主页的布局为MainLayout。MenuLayout在MainLayout的左边,当手指向右滑动的时候,MainLayout就向右滑动,同时MenuLayout跟着向右滑动,于是就显示出了侧滑菜单。以下是示意图:
大概地了解思路以后,我们先来看看布局文件。
layout_slidemenu.xml(侧滑菜单的布局):
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="300dp" android:layout_height ="wrap_content" android:background ="@drawable/menu_bg" android:orientation ="vertical" > <ListView android:id ="@+id/lv_menu" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:cacheColorHint ="@null" /> </LinearLayout >
layout_activity_main.xml(主界面的布局):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" android:background ="#55666666" android:orientation ="vertical" > <LinearLayout android:layout_width ="match_parent" android:layout_height ="60dp" android:background ="@drawable/top_bar_bg" android:gravity ="center_vertical" > <ImageView android:id ="@+id/iv_menu" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_margin ="10dp" android:layout_gravity ="center_vertical" android:background ="@drawable/img_menu" /> <TextView android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:layout_marginLeft ="15dp" android:text ="SlidingMenu" android:layout_gravity ="center_vertical" android:textColor ="#ffffff" android:textSize ="22sp" /> </LinearLayout > </LinearLayout >
layout_main.xml(activity的布局),注意,主界面的布局一定要放在菜单布局的后面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <RelativeLayout xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:tools ="http://schemas.android.com/tools" android:layout_width ="match_parent" android:layout_height ="match_parent" > <com.yuqirong.slidingmenu.view.SlidingMenu android:id ="@+id/slideMenu1" android:layout_width ="wrap_content" android:layout_height ="wrap_content" > <include layout ="@layout/layout_slidemenu" /> <include layout ="@layout/layout_activity_main" /> </com.yuqirong.slidingmenu.view.SlidingMenu > </RelativeLayout >
看完了布局文件,下面我们就来看看代码(以下为部分代码,并非全部):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class SlidingMenu extends FrameLayout { private ViewDragHelper mdDragHelper; public SlidingMenu (Context context) { this (context, null ); } public SlidingMenu (Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public SlidingMenu (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); mdDragHelper = ViewDragHelper.create(this , callback); } }
我们创建一个类名叫SlidingMenu,继承自FrameLayout,然后重写构造器。在构造器中新建了一个ViewDragHelper的对象。如果你还不知道ViewDragHelper为何物,建议你去看看鸿洋_ 的《Android ViewDragHelper完全解析 自定义ViewGroup神器》 ,这里就不展开叙述了。在ViewDragHelper.create(Context context,ViewDragHelper.Callback callback)
里我们传入了一个回调callback,那接下来就来看看这个callback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 Callback callback = new Callback () { @Override public boolean tryCaptureView (View view, int arg1) { return true ; } public int getViewHorizontalDragRange (View child) { return menuWidth; } @Override public int clampViewPositionHorizontal (View child, int left, int dx) { if (child == mainView) { if (left < 0 ) return 0 ; else if (left > menuWidth) return menuWidth; else return left; } else if (child == menuView) { if (left > 0 ) return 0 ; else if (left > menuWidth) return menuWidth; else return left; } return 0 ; } public void onViewPositionChanged (View changedView, int left, int top, int dx, int dy) { if (changedView == mainView) menuView.offsetLeftAndRight(dx); else mainView.offsetLeftAndRight(dx); invalidate(); } ; public void onViewReleased (View releasedChild, float xvel, float yvel) { if (releasedChild == mainView) { if (status == Status.Open) { close(); return ; } if (xvel == 0 && Math.abs(mainView.getLeft()) > menuWidth / 2.0f ) { open(); } else if (xvel > 0 ) { open(); } else { close(); } } else { if (xvel == 0 && Math.abs(mainView.getLeft()) > menuWidth / 2.0f ) { open(); }else if (xvel > 0 ) { open(); } else { close(); } } } };
我们发现在callback中几乎完成了绝大部分的逻辑。首先在tryCaptureView(View view, int arg1)
直接返回了true,因为无论在mainView(主View)还是在menuView(菜单View)都应该去捕获,而getViewHorizontalDragRange(View child)
返回的应该是menuView的宽度,也就是说滑动的时候最多能滑menuWidth的距离。而menuWidth是在onFinishInflate()
中得到的。至于clampViewPositionHorizontal(View child, int left, int dx)
和onViewPositionChanged(View changedView, int left, int top,int dx, int dy)
两个方法逻辑很简单,相信大家都看得懂。最后在onViewReleased(View releasedChild, float xvel, float yvel)
方法中判断了菜单打开或关闭的逻辑,比如在菜单关闭的情况下,只要手指向右滑或是停止滑动时侧滑菜单在屏幕中的宽度大于menuWidth/2这两种情况下,侧滑菜单都是执行open()方法,其它的情况以此类推。下面就来看看open()和close()方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public void open () { if (mdDragHelper.smoothSlideViewTo(mainView, menuWidth, 0 )) { ViewCompat.postInvalidateOnAnimation(this ); } preStatus = status; status = Status.Open; if (listener != null && preStatus == Status.Close) { listener.statusChanged(status); } } public void close () { if (mdDragHelper.smoothSlideViewTo(mainView, 0 , 0 )) { ViewCompat.postInvalidateOnAnimation(this ); } preStatus = status; status = Status.Close; if (listener != null && preStatus == Status.Open) { listener.statusChanged(status); } } public void toggle () { if (status == Status.Close) { open(); } else { close(); } } @Override public void computeScroll () { super .computeScroll(); if (mdDragHelper.continueSettling(true )) { ViewCompat.postInvalidateOnAnimation(this ); } }
我们发现在open()
和close()
两个方法中都调用了ViewCompat.postInvalidateOnAnimation(this);
而postInvalidateOnAnimation(View view)
需要重写computeScroll()
来实现平滑滚动的效果,一般的写法都如上代码所示,不需要改动。再重新回到open()
和close()
两个方法,其中的listener就是菜单开关状态的监听器,当状态改变的时候都会回调listener的statusChanged(Status status)
方法。
最后的最后,别忘了在onLayout(boolean changed, int left, int top, int right, int bottom)
中把menuView设置在mainView的左边。而menuView和mainView都是在onFinishInflate()
中得到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override protected void onFinishInflate () { super .onFinishInflate(); if (getChildCount()!=2 ){ throw new IllegalArgumentException ("子view的数量必须为2个" ); } menuView = getChildAt(0 ); mainView = getChildAt(1 ); menuWidth = menuView.getLayoutParams().width; } @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { menuView.layout(-menuWidth, 0 , 0 , menuView.getMeasuredHeight()); mainView.layout(0 , 0 , right, bottom); }