Android 实现侧滑菜单

从早期的 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);
}