• 自定义侧滑菜单栏代码实现
  • 步骤
  • 界面样式
  • 先写布局吧
  • 菜单布局menuxml
  • 关于ScrollView
  • 主界面布局mainxml
  • Activity布局activity_mainxml
  • SlideMenu类的内容
  • view的绘制
  • 测量获取宽高onMeaure
  • 确定子控件位置onLayout
  • 滑动监听onTouchEvent
  • 限制滑动距离
  • 从上次滑动的位置开始滑动
  • 判断停手位置
  • 为滑动设定时间
  • 添加监听
  • 一些BUG


自定义侧滑菜单栏代码实现

最近学习安卓自定义控件,把最近做的一些东西和详细过程分享,我会把详细的步骤和以及涉及到的知识加以说明.

步骤

  1. 布局:主界面和菜单ScrollView
  2. 自定义控件,继承Viewgroup
  3. 在自定义view中设置view滑动监听和显示,这是重点
  4. 在Activity中应用

界面样式

其实就是大家熟悉的QQ侧滑菜单样式,这是精简版的.

* 主界面

* 主界面非常简单,只是为了演示

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_界面


* 菜单栏

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_控件_02


菜单栏是可以上下滑动的,没错,菜单栏就是用的ScrollView

界面样式就介绍完了,下面开始正文.

先写布局吧

代码未动,布局先行!
我们要写的有三个布局:
1. 菜单栏的布局menu.xml
2. 主界面的布局main.xml
3. 应用Activity的布局activity_main.xml,在activity_main.xml中会把main.xml和menu.xml引用(include)进来
下面让我们开始写吧!

菜单布局menu.xml

关于ScrollView
  1. 前面有提到菜单布局使用的是ScrollView,ScrollView可以实现当view里面内容过多时,会自动显示滑动来填充更多的内容,正适合用在菜单栏中,这样当我们往菜单中增加新的条目时就不用担心装不下了.
  2. 应该注意的是: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:drawableLeftandroid:drawablePadding,初学者可能不知道.drawableLeft是在textview的左侧添加一张图片,同样的还有drawableRight,drawableTop,drawableBotom属性,drawablePadding正是包裹这张图片边距为8dp.效果如下图:

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_布局_03


4.好了,menu.xml文件就介绍完了

主界面布局main.xml

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_控件_04


主界面的布局就非常简单了,整体是一个相对布局,里面嵌套了一个线性和一个TextView,这里不做没有意义的粘贴了,后面附有源码下载.

Activity布局activity_main.xml

写到这里,我们写了两个布局了,那我们应该怎么显示它们呢,想像一下,从主界面向右滑动就会出现菜单

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_控件_05


没错,正如图片中所说的,我们要做的就是要把这现个布局放在一个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距离的位置,右侧就是屏幕左侧的位置,上下不变.

这时我们就可以运行一下了,正确的话如下图所示:

Android 侧滑菜单 抽屉 安卓侧滑菜单实现_控件_06


我们向右滑动,却显示不出菜单,哦~~~原来我们还没添加滑动监听呢?

滑动监听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);
  1. 再次运行一下是不是可以从上一次开始的位置滑动了呢

判断停手位置

虽然已经实现了一些功能,但是这样的滑动肯定 是我们希望的效果,在停手时,它应该自动判断,如果滑动距离够长,而且没有完全把菜单拖出来,它应该自己滑出来,反之,它应该自己回去.其实很简单只需要判断一下当前滑动距离是否有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可能是任意值,而不受限制.