- 自定义侧滑菜单栏代码实现
- 步骤
- 界面样式
- 先写布局吧
- 菜单布局menuxml
- 关于ScrollView
- 主界面布局mainxml
- Activity布局activity_mainxml
- SlideMenu类的内容
- view的绘制
- 测量获取宽高onMeaure
- 确定子控件位置onLayout
- 滑动监听onTouchEvent
- 限制滑动距离
- 从上次滑动的位置开始滑动
- 判断停手位置
- 为滑动设定时间
- 添加监听
- 一些BUG
自定义侧滑菜单栏代码实现
最近学习安卓自定义控件,把最近做的一些东西和详细过程分享,我会把详细的步骤和以及涉及到的知识加以说明.
步骤
- 布局:主界面和菜单ScrollView
- 自定义控件,继承Viewgroup
- 在自定义view中设置view滑动监听和显示,这是重点
- 在Activity中应用
界面样式
其实就是大家熟悉的QQ侧滑菜单样式,这是精简版的.
* 主界面
* 主界面非常简单,只是为了演示
* 菜单栏
菜单栏是可以上下滑动的,没错,菜单栏就是用的ScrollView
界面样式就介绍完了,下面开始正文.
先写布局吧
代码未动,布局先行!
我们要写的有三个布局:
1. 菜单栏的布局menu.xml
2. 主界面的布局main.xml
3. 应用Activity的布局activity_main.xml,在activity_main.xml中会把main.xml和menu.xml引用(include)进来
下面让我们开始写吧!
菜单布局menu.xml
关于ScrollView
- 前面有提到菜单布局使用的是ScrollView,ScrollView可以实现当view里面内容过多时,会自动显示滑动来填充更多的内容,正适合用在菜单栏中,这样当我们往菜单中增加新的条目时就不用担心装不下了.
- 应该注意的是:ScrollView只能有一个子View,这一个子View下可以有多个子view,也就是说只能有一个儿子,可以有多个孙子.
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="200dp"
android:background="@drawable/menu_bg"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="200dp"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:text="新闻"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_news" />
<TextView
android:text="订阅"
style="@style/TextView_Style"
android:id="@+id/tv_read"
android:drawableLeft="@drawable/tab_read" />
<TextView
android:text="本地"
style="@style/TextView_Style"
android:id="@+id/tv_local"
android:drawableLeft="@drawable/tab_local" />
<TextView
android:text="跟帖"
style="@style/TextView_Style"
android:id="@+id/tv_ties"
android:drawableLeft="@drawable/tab_ties" />
<TextView
android:text="图片"
style="@style/TextView_Style"
android:id="@+id/tv_pics"
android:drawableLeft="@drawable/tab_pics" />
<TextView
android:id="@+id/tv_ugc"
style="@style/TextView_Style"
android:drawableLeft="@drawable/tab_ugc"
android:text="话题" />
<TextView
android:text="投票"
style="@style/TextView_Style"
android:id="@+id/tv_vote"
android:drawableLeft="@drawable/tab_vote" />
<TextView
android:text="聚合阅读"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_focus" />
<TextView
android:text="聚合阅读1"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_focus" />
<TextView
android:text="聚合阅读2"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_focus" />
<TextView
android:text="聚合阅读3"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_focus" />
<TextView
android:text="聚合阅读4"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_focus" />
</LinearLayout>
</ScrollView>
为了显示更多条目时,菜单有滑动效果,所以添加了多个重复的TextView,读者必感到困惑.
下面我们来说明一下这个布局:
1. 根布局:
可以看到根布局是ScrollView,宽度我们设置成了固定值,Google建议ScrollView的宽度不应该超过240dp,我这里固定了200dp.高度填充父窗体
2. 子View,ScrollView只能有一个子view, 我们使用一个LinearLayout,LinearLayout下面有多个TextView,以新闻条目为例
<TextView
android:text="新闻"
style="@style/TextView_Style"
android:id="@+id/tv_news"
android:drawableLeft="@drawable/tab_news" />
因为多个TextView的相似程度很高,所以抽取了样式,放在res->values->styles.xml文件中,如下所示,这样在我们使用时,不用重复设置相同的属性,只需要引用样式即可,引用方式:style="@style/TextView_Style"
<style name="TextView_Style">
<item name="android:padding">5dp</item>
<item name="android:gravity">center_vertical</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">22sp</item>
<item name="android:textColor">#ffffff</item>
<item name="android:drawablePadding">8dp</item>
<item name="android:background">#000</item>
</style>
3. 关于TextView中的几个属性
布局中定义的TextView有这样几个属性android:drawableLeft和android:drawablePadding,初学者可能不知道.drawableLeft是在textview的左侧添加一张图片,同样的还有drawableRight,drawableTop,drawableBotom属性,drawablePadding正是包裹这张图片边距为8dp.效果如下图:
4.好了,menu.xml文件就介绍完了
主界面布局main.xml
主界面的布局就非常简单了,整体是一个相对布局,里面嵌套了一个线性和一个TextView,这里不做没有意义的粘贴了,后面附有源码下载.
Activity布局activity_main.xml
写到这里,我们写了两个布局了,那我们应该怎么显示它们呢,想像一下,从主界面向右滑动就会出现菜单
没错,正如图片中所说的,我们要做的就是要把这现个布局放在一个view中,使用include属性,在写这个布局之前,请先新建一个类,从ViewGroup继承,这里名为SlideMenu,具体关于这个类的内容,我们后面讲,新建后,它会自动帮我产们重写onLayout()方法,而且要求我们重写构造方法,因为我们这里使用的是xml文件定义的,所以重写有两个参数的构造方法即可(如果对三个构造方法的具体联系区别有疑问请看我别一篇文章).这时就暂时不要管这个类了,我们先来写布局文件吧.
代码如下:
<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"
tools:context=".MainActivity">
<com.example.slidemenu.SlideMenu
android:id="@+id/slideMenu"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--必须使用match_parent -->
<include layout="@layout/main" />
<include layout="@layout/menu" />
</com.example.slidemenu.SlideMenu>
</RelativeLayout>
可以看到根布局使用的是相对布局,使用线性布局也是可以的,重点是如何引用自定义的view和inclue其它布局文件
1. 使用自定义的view,必须使用全类名,也就是包名+类名
2. 宽高都应该使用填充父窗体
3. 引用其它布局文件<include layout="@layout/main" />
,如果要引用其它的,只要把main换成其它的布局文件名即可
4. 还有重要的一点:现在main和menu的布局都已经是我们定义的SlideView的子View了,根据我们的引用顺序,第0个子view是main,第1个子view是menu,后面 们会用到
5. 好了,布局文件至此结束了,下面开始进入核心的内容了.
SlideMenu类的内容
首先我们先来简单了解一下View的绘制机制,这样有利于我们下一步的理解.
view的绘制
view中有三个方法onMeaure(), onLayout(), onDraw()
首先:系统会进行测量宽高调用onMeaure()方法,这时会确定所要绘制view的宽度和高度
然后:进行布局,调用onLayout()方法,应用布局参数
最后:进行绘制,调用onDraw()方法,这时,view就呈现在我们的手机屏幕上了
了解了这些之后我们就开始绘制我们自己的View了
1. 测量:获取宽高onMeaure()
上面已经讲到,获取宽高系统调用的是onMeasure()方法,因此我们要手动重写onMeaure(),代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//super.onMeasure()会测量屏幕的宽高,由于主界面的高度和菜单相同,所以保留super
main = getChildAt(0);
menu = getChildAt(1);
//getWidth()View在设定好布局后整个展示出来的view的宽度
menuWidth = menu.getLayoutParams().width;
//菜单高度就是屏幕的高度,宽度已经设置好200dp
menu.measure(menuWidth, heightMeasureSpec);
//主界面和屏幕相同
main.measure(widthMeasureSpec, heightMeasureSpec);
}
main = getChildAt(0);
在讲布局文件activity_main.xml的时候说过,我们通过下标来获取子view,getChild(index),获取了main和menu两个view getMeasuredWidth()
想像一下我们向右滑动,显示出菜单,那么菜单的宽度就是我们最终没到的距离吧,所以这里我们要获取宽度,但是要注意,不能使用getWidth()方法,因为getWidth()获取的是可见的控件宽度,因为菜单刚开始是不可见的,只有我们向右滑动了它才出现,所以使用getWidth()获取的宽度是0.使用menu.getLayoutParams().width获取的是整个内容的宽度 menu.measure(menuWidth, heightMeasureSpec);
可以这样认为:measure()函数是在设置当前控件的宽高,但是它并不实际对宽高作出改变,而是调用onMeaure()方法来实际修改view尺寸
好了,宽高我们获取到了,下面我们要确定子控件的位置了
2. 确定子控件位置:onLayout()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//默认传进来的就是屏幕的位置
//设置主页面的位置就是屏幕中间
main.layout(l, t, r, b);
//设置菜单的位置
menu.layout(l - menuWidth, t, l, b);
scroller = new Scroller(getContext());
}
main.layout(l, t, r, b);
layout()设置的是当前控件相对于父控件的位置,参数l, t, r, b分别是左,上,右, 下的位置,想像一下:主界面就是应用在整个屏幕上,所以我们设置main的位置就是屏幕的上下左右的位置
menu.layout(l - menuWidth, t, l, b);
菜单刚开始是隐藏的,可以想像的到它位于main的右边的位置,menu左侧位置是父控件向左menuWidth距离的位置,右侧就是屏幕左侧的位置,上下不变.
这时我们就可以运行一下了,正确的话如下图所示:
我们向右滑动,却显示不出菜单,哦~~~原来我们还没添加滑动监听呢?
滑动监听onTouchEvent()
为了响应触摸事件,我们需要重写onTouchEvent()方法,生成不要以为你添加监听器一样使用setOnTouchtListener()类似的方法,这里我们可以自己定义一个控件哦.
代码处理如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取按下位置的x坐标
startX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
//移动时,设置当前坐标
//1. 获取移动距离
float endX = event.getX();
float distance = endX - startX;
scrollTo(distance);
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return true;
}
event.getX()
获取当前按下的的x坐标, 这个x坐标是 view自身左上角的坐标,注意不要使用getRawX(),getRawX()获取的是相对于屏幕左上角的坐标
//这段代码是在触摸滑动过程中,获取滑动位置,并计算滑动距离,再调用scrollTo()方法
float endX = event.getX();
float distance = endX - startX;
scrollTo(distance);
下面是scrollTo()方法的代码
private void scrollTo(float distance) {
scrollTo((int)-distance, 0);
}
可见,scrollTo()方法内封装了View的scrollTo()方法,因为我们只需要移动x坐标
你可能会发现在调用View的scrollTo((int)-distance, 0),我们在distance前面加了负号,这是为什么呢?
因为系统scrollTo()方法是从当前位置滑动到指定的位置,那么达个位置怎么确定呢, 这个坐标是有方向的,这里只简单说一下View边缘坐标-View内容边缘坐标结果为正的是正向
好了,写到这里我们就可以再次运行一下了,这时向右滑动就会惊喜发现可以滑动啦!
限制滑动距离
虽然可以滑动了,但是问题也来了,我们发现,向右滑动时如果滑动距离过长,菜单右边的空白也会显现出来,同时,一般情况我们是不允许向左滑动的.因此我们需要限制一下滑动距离,最大滑动距离是menuWidth,也就是distance的大小,如果太大或者为负就进行处理
在case ACTION_MOVE:中添加如下代码:
if(distance < 0) {
distance = 0;
}
if(distance > menuWidth) {
distance = menuWidth;
}
这是我们运行一下就会发现,现在可以自由滑动啦!
从上次滑动的位置开始滑动
在滑动完一次后,如果停留在一半的位置,再次滑动时,它会先回到0的位置,再开始滑动到指定位置,这是因为我们没有记录上次滑动停止的位置.
解决办法是记录每次次滑动停止的位置(也就是distance),下次滑动时,要把这个位置加上
1. 首先在抬起时获取当前位置lastX = event.getX()
2. 在计算distance时加上lastXdistance = endX - startX + lastX;
3. 代码如下:
int endX = (int) event.getX();
distance = endX - startX + lastX;
scrollTo(distance);
- 再次运行一下是不是可以从上一次开始的位置滑动了呢
判断停手位置
虽然已经实现了一些功能,但是这样的滑动肯定 是我们希望的效果,在停手时,它应该自动判断,如果滑动距离够长,而且没有完全把菜单拖出来,它应该自己滑出来,反之,它应该自己回去.其实很简单只需要判断一下当前滑动距离是否有menuWidth的一半就行了,大于等于一半就测出,否则就隐藏
显然,这段逻辑是写在ACTION_UP分支里面的:
if(distance>=menuWidth/2){
lastX = menuWidth;
}else{
lastX = 0;
}
scrollTo(lastX);
再运行一下,是不是体验好了很多呢,但是马上你就会发现,松手以后它滑动得太快了,不是吗?
为滑动设定时间
为了实现松手后缓慢滑动的效果,可以使用一个新的API–Scroller.为了了解这个API如何使用,可以查看 一下源码,源码中介绍:这个类是对滑动的封装,我们可以使用这个类来产生我们需要滑动的数据,这些数据是根据滑动期间的不同时刻产生的,但是它并不会为我们实现滑动的效果,实现的滑动需要我们自己实现,也就是调用scrollTo()方法
我们下面将会用到Scroller类的以下几个方法:
1. computeScrollOffset(),返回当前的滑动是否已经完成,没有返回true,完成返回false
2. getCurrX(),如果滑动没有完成,就调用该方法,返回当前的X的偏移量
3. startScroll(),开始计算滑动的值
说到这里就很明白了,我们不断得通过getCurrX()获取偏移,然后调用scrollTo(),再调用getCurrX(),再调用scrollTo()这样不断得循环,就好了.
那么知道了使用Scroller,该如何实现这样的一个循环呢?我们先把代码写出来,再详细解释.
//定义一个方法启动计算偏移
private void startScroll() {
int startx2 = MygetScrollX();
int distanceX = lastX - startx2;
scroller.startScroll(startx2, 0, distanceX, 0, 1500);
invalidate();
}
public int MygetScrollX() {
//返回当前view显示部分的左边界位置
return -getScrollX();
}
在startScroll()方法中,我们先获取当前的可见控件的左侧边缘的位置,为什么我们要返回一个负值呢?因为getScrollX()返回的mScrollX,mScrollX的值跟我们平时的坐标系是相反的,这里不多说,可以看我另外一篇文章:xxxxxxxxxxxx
然后让lastX-startX2获取要移动的位移,这时的laxtX就是我们最终要移动到的位置.
然后启动scroller.startScroll(startx2, 0, distanceX, 0, 1500);
四个参数分别为:开始X位置,开始Y位置,X方向位移,Y方向位移,限定移动时间
这些设置好以后我们再来启动一下,是不是变得很顺滑了呢~
添加监听
我们要做的就是在SlideMenu中添加一个方法,用来切换当前菜单的状态,isShow是我定义的一个变量,用来表示菜单是否已经打开.
public void switchMenu() {
if(isShow){
//说明菜单处于打开状态
lastX = 0;
isShow = false;
}else{
lastX = menuWidth;
isShow = true;
}
startScroll();
}
然后在Activity中调用就好啦
slideMenu = (SlidingMenu) findViewById(R.id.slideMenu);
ImageView iv_back = (ImageView) findViewById(R.id.iv_back);
iv_back.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
slideMenu.switchMenu();
}
});
现在运行一下吧.
到这里,侧滑功能基本实现了,功能 就写到这里,当然还有一些BUG存在,我简单说一下,目前我还没有找到合适的办法解决,哪位兄弟解决了请留言,多谢!
一些BUG
在onTouchEvent()方法中,在抬手时,我们记录了这时的值,并保存在了lastX中,当下次滑动时,就会把lastX加上,用来计算这时的移动位移,假设存在这样一种情况:如果我们只作了一次点击而没有滑动,那么这时就不会执行CASE MotionEvent.ACTION_MOVE方法,而只会执行MotionEvent.ACTION_DOWN和MotionEvent.ACTION_UP,那么下次再拖动时,如果执行了MotionEvent.ACTION_MOVE,这时的lastX就不对了,因为这个lastX可能是任意值,而不受限制.