上篇给大家介绍QQ5.0侧滑菜单的视频课程,对于侧滑的时的动画效果的实现有了新的认识,似乎打通了任督二脉,目前可以实现任意效果的侧滑菜单了,感谢鸿洋大大!!
用的是HorizontalScrollView来实现的侧滑菜单功能,HorizontalScrollView的好处是为我们解决了滑动功能,处理了滑动冲突问题,让我们使用起来非常方便,但是滑动和冲突处理都是android中的难点,是我们应该掌握的知识点,掌握了这些,我们可以不依赖于系统的API,随心所欲打造我们想要的效果,因此这篇文章我将直接自定义ViewGroup来实现侧滑菜单功能
首先我们先来看一看效果图,第一个效果图是一个最普通的侧滑菜单,我们一会儿会先做出这种侧滑菜单,然后再在此基础上实现另外两个效果
第一种
第二种
第三种
实现第一种侧滑菜单,继承自ViewGroup
继承自ViewGroup需要我们自己来测量,布局,实现滑动的效果,处理滑动冲突,这些都是一些新手无从下手的知识点,希望看了这篇文章后可以对大家有一个帮助
自定义ViewGroup的一般思路是重写onMeasure方法,在onMeasure方法中调用measureChild来测量子View,然后调用setMeasuredDimension来测量自己的大小。然后重写onLayout方法,在onLayout中调用子View的layout方法来确定子View的位置,下面我们先来做好这两件工作
初始时候我们的Content应该是显示在屏幕中的,而Menu应该是显示在屏幕外的。当Menu打开时,应该是这种样子的
mMenuRightPadding是Menu距屏幕右侧的一个距离,因为我们Menu打开后,Content还是会留一部分,而不是完全隐藏的
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
|
public class MySlidingMenu extends ViewGroup { public MySlidingMenu(Context context) { this (context, null , 0 ); } public MySlidingMenu(Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); DisplayMetrics metrics = new DisplayMetrics(); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getMetrics(metrics); //获取屏幕的宽和高 mScreenWidth = metrics.widthPixels; mScreenHeight = metrics.heightPixels; //设置Menu距离屏幕右侧的距离,convertToDp是将代码中的100转换成100dp mMenuRightPadding = convertToDp(context, 100 ); } @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { //拿到Menu,Menu是第0个孩子 mMenu = (ViewGroup) getChildAt( 0 ); //拿到Content,Content是第1个孩子 mContent = (ViewGroup) getChildAt( 1 ); //设置Menu的宽为屏幕的宽度减去Menu距离屏幕右侧的距离 mMenuWidth = mMenu.getLayoutParams().width = mScreenWidth - mMenuRightPadding; //设置Content的宽为屏幕的宽度 mContentWidth = mContent.getLayoutParams().width = mScreenWidth; //测量Menu measureChild(mMenu,widthMeasureSpec,heightMeasureSpec); //测量Content measureChild(mContent, widthMeasureSpec, heightMeasureSpec); //测量自己,自己的宽度为Menu宽度加上Content宽度,高度为屏幕高度 setMeasuredDimension(mMenuWidth + mContentWidth, mScreenHeight); } @Override protected void onLayout( boolean changed, int l, int t, int r, int b) { //摆放Menu的位置,根据上面图可以确定上下左右的坐标 mMenu.layout(-mMenuWidth, 0 , 0 , mScreenHeight); //摆放Content的位置 mContent.layout( 0 , 0 , mScreenWidth, mScreenHeight); } /** * 将传进来的数转化为dp */ private int convertToDp(Context context , int num){ return ( int ) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,num,context.getResources().getDisplayMetrics()); } } |
目前我们的侧滑菜单中的两个子View的位置应该是这个样子
接下来我们编写xml布局文件
left_menu.xml 左侧菜单的布局文件,是一个ListView
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?xml version= "1.0" encoding= "utf-8" ?> <RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" > <ListView android:id= "@+id/menu_listview" android:layout_width= "wrap_content" android:divider= "@null" android:dividerHeight= "0dp" android:scrollbars= "none" android:layout_height= "wrap_content" > </ListView> </RelativeLayout> |
其中ListView的Item布局为left_menu_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "horizontal" android:layout_width= "match_parent" android:gravity= "center_vertical" android:layout_height= "match_parent" > <ImageView android:id= "@+id/menu_imageview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:src= "@drawable/menu_1" android:padding= "20dp" /> <TextView android:id= "@+id/menu_textview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:text= "菜单1" android:textColor= "#000000" android:textSize= "20sp" /> </LinearLayout> |
我们再来编写内容区域的布局文件 content.xml 其中有一个header,header中有一个ImageView,这个ImageView是menu的开关,我们点击他的时候可以自动开关menu,然后header下面也是一个listview
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
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "vertical" android:layout_width= "match_parent" android:layout_height= "match_parent" > <LinearLayout android:layout_width= "match_parent" android:layout_height= "65dp" android:background= "#000000" android:gravity= "center_vertical" android:orientation= "horizontal" > <ImageView android:id= "@+id/menu_toggle" android:layout_width= "40dp" android:layout_height= "40dp" android:src= "@drawable/toggle" android:paddingLeft= "10dp" /> </LinearLayout> <ListView android:id= "@+id/content_listview" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:dividerHeight= "0dp" android:divider= "@null" android:scrollbars= "none" /> </LinearLayout> |
content的item的布局文件为 content_item.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
<?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:orientation= "horizontal" android:layout_width= "match_parent" android:gravity= "center_vertical" android:background= "#ffffff" android:layout_height= "match_parent" > <ImageView android:id= "@+id/content_imageview" android:layout_width= "80dp" android:layout_height= "80dp" android:src= "@drawable/content_1" android:layout_margin= "20dp" /> <TextView android:id= "@+id/content_textview" android:layout_width= "wrap_content" android:layout_height= "wrap_content" android:text= "Content - 1" android:textColor= "#000000" android:textSize= "20sp" /> </LinearLayout> |
在activity_main.xml中,我们将menu和content添加到我们的slidingMenu中
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" android:layout_width= "match_parent" android:layout_height= "match_parent" android:background= "#aaaaaa" > <com.example.user.slidingmenu.MySlidingMenu android:id= "@+id/slidingmenu" android:layout_width= "wrap_content" android:layout_height= "match_parent" > <include android:id= "@+id/menu" layout= "@layout/left_menu" /> <include android:id= "@+id/content" layout= "@layout/content" /> </com.example.user.slidingmenu.MySlidingMenu> </RelativeLayout> |
现在应该是这种效果
左侧菜单是隐藏在屏幕左侧外部的,但是现在还不能滑动,如果想要实现滑动功能,我们可以使用View的scrollTo和scrollBy方法,这两个方法的区别是scrollTo是直接将view移动到指定的位置,scrollBy是相对于当前的位置移动一个偏移量,所以我们应该重写onTouchEvent方法,用来计算出当前手指的一个偏移量,然后使用scrollBy方法一点一点的移动,就形成了一个可以跟随手指移动的view的动画效果了
在写代码之前,我们先扫清一下障碍,我们先来弄清楚这些坐标是怎么回事
好了,把这些坐标弄清楚后,我们就简单多了,下面直接看onTouchEvent方法
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
|
@Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action){ case MotionEvent.ACTION_DOWN: mLastX = ( int ) event.getX(); mLastY = ( int ) event.getY(); break ; case MotionEvent.ACTION_MOVE: int currentX = ( int ) event.getX(); int currentY = ( int ) event.getY(); //拿到x方向的偏移量 int dx = currentX - mLastX; if (dx < 0 ){ //向左滑动 //边界控制,如果Menu已经完全显示,再滑动的话 //Menu左侧就会出现白边了,进行边界控制 if (getScrollX() + Math.abs(dx) >= 0 ) { //直接移动到(0,0)位置,不会出现白边 scrollTo( 0 , 0 ); } else { //Menu没有完全显示呢 //其实这里dx还是-dx,大家不用刻意去记 //大家可以先使用dx,然后运行一下,发现 //移动的方向是相反的,那么果断这里加个负号就可以了 scrollBy(-dx, 0 ); } } else { //向右滑动 //边界控制,如果Content已经完全显示,再滑动的话 //Content右侧就会出现白边了,进行边界控制 if (getScrollX() - dx <= -mMenuWidth) { //直接移动到(-mMenuWidth,0)位置,不会出现白边 scrollTo(-mMenuWidth, 0 ); } else { //Content没有完全显示呢 //根据手指移动 scrollBy(-dx, 0 ); } } mLastX = currentX; mLastY = currentY; break ; } return true ; } |
现在我们的SlidingMenu依然是不能够水平滑动的,但是listview可以竖直滑动,原因是我们的SlidingMenu默认是不拦截事件的,那么事件会传递给他的子View去执行,也就是说传递给了Content的ListView去执行了,所以listview是可以滑动的,为了简单,我们先重写onInterceptTouchEvent方法,我们返回true,让SlidingMenu拦截事件,我们的SlidingMenu就能够滑动了,但是ListView是不能滑动的,等下我们会进行滑动冲突的处理,现在先实现SlidingMenu的功能
1
2
3
4
|
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return true ; } |
好了,现在我们可以自由的滑动我们的SlidingMenu了,并且进行了很好的边界控制,现在我们再添加个功能,就是当Menu打开大于二分之一时,松开手指,Menu自动打开。当Menu打开小于二分之一时,松开手指,Menu自动关闭。自动滑动的功能我们要借助Scroller来实现
我们在构造方法中初始化一个Scroller
1
2
3
4
5
6
|
public MySlidingMenu(Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); ... mScroller = new Scroller(context); ... } |
然后重写computeScroll方法,这个方法是保证Scroller自动滑动的必须方法,这是一个模板方法,到哪里都这么些就好了
1
2
3
4
5
6
7
|
@Override public void computeScroll() { if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); invalidate(); } } |
接着我们在onTouchEvent的ACTION_UP中进行判断,判断当前menu打开了多少
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
case MotionEvent.ACTION_UP: if (getScrollX() < -mMenuWidth / 2 ){ //打开Menu //调用startScroll方法,第一个参数是起始X坐标,第二个参数 //是起始Y坐标,第三个参数是X方向偏移量,第四个参数是Y方向偏移量 mScroller.startScroll(getScrollX(), 0 , -mMenuWidth - getScrollX(), 0 , 300 ); //设置一个已经打开的标识,当实现点击开关自动打开关闭功能时会用到 isOpen = true ; //一定不要忘了调用这个方法重绘,否则没有动画效果 invalidate(); } else { //关闭Menu //同上 mScroller.startScroll(getScrollX(), 0 , -getScrollX(), 0 , 300 ); isOpen = false ; invalidate(); } break ; |
关于startScroll中的startX和startY好判断,那么dx和dy怎么计算呢?其实也非常简单,比如我们startX坐标为30,我们想移动到-100,那么startX+dx = -100 –> dx = -100 - startX –> dx = -130
好了现在我们就可以实现松开手指后自动滑动的动画效果了
现在我们还需要点击content中左上角的一个三角,如果当前menu没有打开,则自动打开,如果已经打开,则自动关闭的功能,自动滑动的效果我们要借助Scroller.startScroll方法
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
|
/** * 点击开关,开闭Menu,如果当前menu已经打开,则关闭,如果当前menu已经关闭,则打开 */ public void toggleMenu(){ if (isOpen){ closeMenu(); } else { openMenu(); } } /** * 关闭menu */ private void closeMenu() { //也是使用startScroll方法,dx和dy的计算方法一样 mScroller.startScroll(getScrollX(), 0 ,-getScrollX(), 0 , 500 ); invalidate(); isOpen = false ; } /** * 打开menu */ private void openMenu() { mScroller.startScroll(getScrollX(), 0 ,-mMenuWidth-getScrollX(), 0 , 500 ); invalidate(); isOpen = true ; } |
然后我们可以在MainActivity中拿到我们content左上角三角形的imageview,然后给他设置一个点击事件,调用我们的toggleMenu方法
1
2
3
4
5
6
|
mMenuToggle.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { mSlidingMenu.toggleMenu(); } }); |
处理滑动冲突
由于我们的menu和content是listview,listview是支持竖直滑动的,而我们的slidingMenu是支持水平滑动的,因此会出现滑动的冲突。刚才我们直接在onInterceptTouchEvent中返回了true,因此SlidingMenu就会拦截所有的事件,而ListView接收不到任何的事件,因此ListView不能滑动了,我们要解决这个滑动冲突很简单,只需要判断当前是水平滑动还是竖直滑动,如果是水平滑动的话则让SlidingMenu拦截事件,如果是竖直滑动的话就不拦截事件,把事件交给子View的ListView去执行
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
|
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false ; int x = ( int ) ev.getX(); int y = ( int ) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercept = false ; break ; case MotionEvent.ACTION_MOVE: int deltaX = ( int ) ev.getX() - mLastXIntercept; int deltaY = ( int ) ev.getY() - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)){ //横向滑动 intercept = true ; } else { //纵向滑动 intercept = false ; } break ; case MotionEvent.ACTION_UP: intercept = false ; break ; } mLastX = x; mLastY = y; mLastXIntercept = x; mLastYIntercept = y; return intercept; } |
好了,现在我们的滑动冲突就解决了,我们既可以水平滑动SlidingMenu,又可以竖直滑动ListView,那么第一种SlidingMenu就已经实现了,我们再来看看另外两种怎么去实现
实现第二种QQ V6.2.3风格的SlidingMenu
这种SlidingMenu是和QQ v6.2.3 的侧滑菜单风格一致的,我们发现Menu和Content的滑动速度是有一个速度差的,实际上我们可以通过修改Menu的偏移量来达到这种效果
此时Menu的偏移量为mMenuWidth的2/3,当我们慢慢打开Menu的同时,修改Menu的偏移量,最终修改为0
这样就达到了一种速度差的效果,我们只需要在onTouchEvent的ACTION_MOVE和computeScroll中添加一行如下代码就可以
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
这个效果中Menu有一个偏移的效果,透明度的变化以及放大的效果。Content中有一个缩小的效果。
首先我们要有一个变量,用来记录当前menu已经打开了多少百分比。
这里我们要注意,getScrollX得到的数值正好是负值,所以我们计算的时候要将getScrollX的值取绝对值再去计算,我们在onTouchEvent的MOVE中要计算这个值,同时在computeScroll方法中也要计算这个值,因为当我们手指抬起时,可能会执行一段自动打开或者关闭的动画,那么我们在MOVE中的计算肯定停止了,但是在执行动画的过程中,是Scroller在起作用,那么computeScroll就会执行直到动画结束,因此我们要在computeScroll中同样进行计算
1
|
scale = Math.abs(( float )getScrollX()) / ( float ) mMenuWidth; |
scale的值是[0,1]的,因此我们就可以根据这个值来对menu的偏移量进行设置。
我们可以通过设置View的setScaleX和setScaleY来对View进行放大缩小,当然这个缩放比例要根据我们的scale值来改变,首先我们的Menu有一个放大的效果,我们就指定为Menu从0.7放大到1.0,那么我们就可以这样写
1
2
|
mMenu.setScaleX( 0 .7f + 0 .3f*scale); mMenu.setScaleY( 0 .7f + 0 .3f*scale); |
透明度是从0到1的,所以我们直接用scale的值就可以了
1
|
mMenu.setAlpha(scale); |
我还给Menu设置了一个偏移量,这个偏移量大家可以自己计算,我是这样计算的
1
|
mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/ 2 )*( 1 .0f-scale)); |
设置完Menu后,我们再来设置Content,Content的大小是从1.0缩小到0.7,因此我们这样写
1
2
3
|
mContent.setScaleX( 1 - 0 .3f*scale); mContent.setPivotX( 0 ); mContent.setScaleY( 1 .0f - 0 .3f * scale); |
其中mContent.setPivotX(0)是让Content的缩放中心店的X轴坐标为0点
我们可以将这个变化的过程抽取为一个方法
1
2
3
4
5
6
7
8
9
10
|
private void slidingMode3(){ mMenu.setTranslationX(mMenuWidth + getScrollX() - (mMenuWidth/ 2 )*( 1 .0f-scale)); mMenu.setScaleX( 0 .7f + 0 .3f*scale); mMenu.setScaleY( 0 .7f + 0 .3f*scale); mMenu.setAlpha(scale); mContent.setScaleX( 1 - 0 .3f*scale); mContent.setPivotX( 0 ); mContent.setScaleY( 1 .0f - 0 .3f * scale); } |
将这个方法添加到onTouchEvent的ACTION_MOVE和computeScroll中就可以了。
我们看到所有的滑动风格都是在基于第一种基础上,修改Menu或者Content的translationX或者scaleX scaleY的值来决定的,因此我们可以打造各种各样的SlidingMenu来。
以上所述是小编给大家介绍的Android自定义ViewGroup打造各种风格的SlidingMenu的相关知识,希望对大家有所帮助!