自定义组件开发九 侧边栏

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011733020/article/details/81003248

概述

侧边栏是一种常见的 UI 结构,用户使用手指左滑或者右滑,可以像抽屉一样拉出隐藏在屏
幕边界之外的内容,既能增加 App 的 UI 的内容,又能给用户带来更新鲜的用户体验,网易新闻、QQ(如图所示)等主流 App 都有类似的设计。
这里写图片描述

侧边栏和上一章节讲的触摸滑屏有很多的类似之处。在侧边栏自定义容器中,定义两个子容
器,一个用作侧边栏,另一个用作主界面,默认情况下隐藏侧边栏,用户向右滑动手指后,侧边栏从左边滑出,主要的功能如下:

Ø 手指从左侧边沿滑动手指,侧边栏滑出,如果在屏幕中间滑动不起作用;
Ø 在屏幕任意处向左滑动,可以关闭侧边栏;
Ø 侧边栏滑出后,松开手指,侧边栏继续滑动到指定位置;
Ø 支持分割线;
Ø 支持侧边栏宽度设置;
Ø 支持手指滑动感应宽度设置;
Ø 提供直接打开或关闭的功能接口。

从技术角度来说,其实也没什么新的技术点,主要还是自定义 ViewGroup 容器并利用
Scroller 实现惯性滚动。

我们阅读 Android SDK 的源码时,大量应用了位操作,包括按位与(&)、按位或(|)、按位
取反(~)、左位移(<<)和右位移(>>)等等,位操作主要用于二进制运算,在保存一些标识数据时被大量使用,这也给我们理解代码带来了一些困难。本章将使用位来保存一些标识数据,希望能起到抛砖引玉的作用。

使用二进制保存标识数据

位运算符

对于那些“是”或“否”的数据,可以使用位来保存,在 Java 中,一个 int 类型的数据占 4 个字
节,也就是 32 位,一位就能代表一种信息,前面学习到的调用 onMeasure()方法对自定义组件进行尺寸测量时,参数 widthMeasureSpec 和 heightMeasureSpec 中前 2 位保存了尺寸模式,后 30位保存了尺寸大小就是一种典型的位应用。再比如在本章的侧边栏组件中是否显示侧边栏的分割线,为 1 表示为显示,为 0 表示为不显示;侧边栏的打开状态也可以用一位表示,为 1 时表示已打开,为 0 时表示隐藏。我们使用最后一位来表示是否显示分割线,使用倒数第二位来表示侧边栏的打开和关闭状态。示意图如图所示。
这里写图片描述

所有编程语言的位操作运算符都大同小异,本章不打算将位操作讲得多么完整和透彻(但阅
读后您将会有非常细致的理解),只向您介绍将会用的位操作运算符,主要有:
Ø 按位与(&)
Ø 按位或(|)
Ø 按位取反(~)
Ø 左位移(<<)
Ø 右位移(>>、>>>)

按位与(&):两个二进制值按位与运算,逢 0 则 0。即:1 & 1 = 1、1 & 0 = 0、0 & 1 = 0、0
& 0 = 0;
按位或(|):两个二进制值按位或运算,逢 1 则 1。即:1 | 1 = 1、1 | 0 = 1、0 | 1 = 1、0 |
0 = 0;
按位取反(~):两个二进制值按位取反运算,0 取反为 1,1 取反为 0。即:~0 = 1、~1 =
0;
左位移(<<):将二进制数向左移动若干位,最低位补 0。比如 0000 1000 向左移 2 位,变
成 0010 0000;
右位移(>>):带符号右位移,负数最高位补 1,正数补 0。比如 0000 1000 向右移 2 位,
变成 0000 0010;1111 0000 向右移 2 位(以 1 开头即为负数),变成 1111 1100;
右位移(>>>):不带符号右位移,负数和正数高位都补 0,比如 1111 0000 向右移 2 位,
变成 0011 1100。

对于移位运算符来说,最好使用完整的位进行运算,这样的结果才精确。同时,对于二进制
来说,最高位代表符号,如果最高位为 1 则表示负数,为 0 则表示正数。
我们通过几个案例来演示二进制的运算规则,如果以前概念比较模糊请在这个小节中理解
透。

案例 1:10 & -4 = ?

进行位运算时,要先将十进制转换成二进制。负数转换成二进制比较麻烦,原则是将负数的
绝对值转换成二进制,再取反加 1,有一个基本的判断标准,负数转换为二进制后,一定是以 1开头。

java 中 int 型占 4 个字节,共 32 位,数字 10 转换成二进制后如下:
0000 0000 0000 0000 0000 0000 0000 1010

下面整理出-4 转换成二进制的过程:
1. 先取-4 的绝对值,结果为 4
2. 得到 4 的 2 进制值:0000 0000 0000 0000 0000 0000 0000 0100
3. 将 2 进制的值取反:1111 1111 1111 1111 1111 1111 1111 1011
4. 将第 3)步的结果+1:1111 1111 1111 1111 1111 1111 1111 1100

上面 4 步计算出了-4 的二进制值:1111 1111 1111 1111 1111 1111 1111 1100,再次强调,如
果是负数,转换成二进制后,一定是以 1 开头。

10 和-4 都是十进制,先将十进制转换成二进制后,再进行 10 & -4 的计算,过程如下:
这里写图片描述
根据上面的结果得知, 10 & -4 的结果为 8。可以写一个 Java Application 对该结果进行验证。Integer 有一个静态方法 toBinaryString(int n)能将参数 n 以二进制格式返回。

案例 2:10 | -4 = ?
为了简单起见,我们不再使用新的数字,只是改了一下运算符,这次我们计算 10 和-4 按位
或运算之后的结果,前面已经知道了 10 和-4 的二进制表示,我们通过公式计算如下:
这里写图片描述
案例 1 的计算结果以 0 开头,说明是正数,无需再计算换算。但案例 2 的计算结果是以 1 开
头,说明是负数,负数的二进制如何转换成十进制呢?运算规则其实是相反的:减 1 取反加负
号。我们的演算过程如下:

1111 1111 1111 1111 1111 1111 1111 1110
减 1-> 1111 1111 1111 1111 1111 1111 1111 1101
取反-> 0000 0000 0000 0000 0000 0000 0000 0010 -> 2

取反后结果为 2,加个负号,即为-2,也就是说,10 | -4 = -2。

案例 3:10 << 4 = ?
本案例要求将 10 向左移 4 位。10 的二进制为:0000 0000 0000 0000 0000 0000 0000 1010,向左移 4 位变成: 0000 0000 0000 0000 0000 0000 1010 0000,转换成二进制为 160。所以,10<< 4 = 160。

案例 4:-4 >> 4 = ?
本案例演示带符号右移位运算,-4 向右移 4 位,因为是负数,最高位补 1。即 1111 1111 1111
1111 1111 1111 1111 1100 向右移动 4 位后,结果为:1111 1111 1111 1111 1111 1111 1111 1111,每一位都是 1 了,转换成 10 进制又是多少呢?我们演算一下:

负数-> 1111 1111 1111 1111 1111 1111 1111 1111
减 1-> 1111 1111 1111 1111 1111 1111 1111 1110
取反-> 0000 0000 0000 0000 0000 0000 0000 0001 -> 1

取反后为 1,加负数即为-1,也就是说-4 >> 4 = -1。

通过上面 4 个案例,我们是不是都掌握了位运算的基本运算规则?位运算的难点主要是当
有负数参与运算时需要转换,而转换的规则也是如此容易。

位运算的常用功能

对于每一个“位”来说,主要的操作有置 1、置 0 和判断是否为 1 等三个操作。如果使用一
个“位”表示“有”或“无”的概念,则一般来说,1 表示有,0 表示无。假设使用 int 型的最后一位
表示“是一位帅哥”,倒数第二位表示“还是一个小鲜肉”,则:

0000 0000 0000 0000 0000 0000 0000 0011 表示是小鲜肉又是帅哥
0000 0000 0000 0000 0000 0000 0000 0001 表示不是小鲜肉是帅哥
0000 0000 0000 0000 0000 0000 0000 0000 表示不是小鲜肉也不是帅哥
0000 0000 0000 0000 0000 0000 0000 0010 表示是小鲜肉但不是帅哥

上述案例中,程序在运行时,可能要对后面的两位进行变值操作,换句话说,需要将某一位
进行置 0 或置 1 的操作,不同的“位”需要和不同的值进行位运算,如果是第 n 位(从低位算
起,n 从 0 开始),则该值为 2 的 n 次方,如图所示。
这里写图片描述
上述中的 2 的 n 次方其实是有章可循的,比如是 2 0 是 1,2 1 是 10,2 2 是 100,2 3 是 1000,2 4 是 10000……依此类推。

如果有个 int 类型的数 N,现针对第 n 位进行置 0、置 1 和判断是否为 1 的操作,规则如下:
Ø 置 0:N & ~ 2 n
Ø 置 1:N | 2 n
Ø 判断是否为 1:2 n == N & 2 n ,如果相等,表示为 1

我们通过一个场景来模拟试验上面的 3 个操作。首先,我们定义三个变量或常量:
private static final int HANDSOME_MASK = 0x1; //帅哥掩码,0001
private static final int FLESH_MEAT_MASK = HANDSOME_MASK << 1; //小鲜肉掩码,向左移动 1位,二进制就是 0010
private int me = 0;//初始化为 0,即不是小鲜肉,也不是帅哥

变量 me 是我最开始的特征,初始为 0,表示即不是小鲜肉,也不是帅哥,现在我要变成小
鲜肉:me = me | FLESH_MEAT_MASK,也就是:me = 0000 | 0010 = 0010,此时,倒数第二位变成了 1,这就是置 1 操作。通过努力的健身,肌肉出来了,线条出来了,我又变成帅哥了,me = me| HANDSOME_MASK。上一步 me 的值为 0010,此时,me = 0010 | 0001 = 0011,倒数第 2 位和倒数第 1 位全成了 1,此时的我即是小鲜肉又是小帅哥,耶!

吃了睡睡了吃,现在的我不是小鲜肉也不是帅哥了,而是一头大肥猪。需要
把后面两个位的 1 全变成 0:me = me & ~ FLESH_MEAT_MASK 表示不再是小鲜肉,上面的运算中,me 为 0011,这时,me = 0011 & ~0010 = 0011 & 1101 = 0001,是的,倒数第二位变成了 0,已不再是小鲜肉,但还是帅哥哦,继续运算:me = me & ~ HANDSOME_MASK,即:me = 0001 & ~0001= 0001 & 1110 = 0000,啊,全部置 0,已完全去除了小鲜肉和帅哥的身份,悲催!

如果要判断当前的我是小鲜肉还是帅哥呢?假设 me 变量的值为 0001,判断是不是小鲜肉:
me & FLESH_MEAT_MASK = 0001 & 0010 = 0000,结果为 0000,与 FLESH_MEAT_MASK 不相等,说明不是小鲜肉(相等才是小鲜肉)。那么是不是帅哥呢?me & HANDSOME_MASK = 0001 & 0001 =0001,嗯,结果 0001 与 HANDSOME_MASK 是相等的,真凭实据证明就是帅哥。

分析到这里差不多就结束了,其实二进制还有其他的作用,大家可以多去分析和理解。最重
要的是,看二进制要和看十二进制一样自然,这样就不需要在脑海里转换来转换去了。

继承自ViewGroup的侧边栏

提供侧边栏的两种实现方案。其一是继承自 ViewGroup,其二是继承自HorizontalScrollView。显然,第一种实现会更加复杂,第二种要相对简单些。显然这两种都是应该掌握的。

不管是哪一种方法,侧边栏的结构都是由两部分构成:侧边栏和主界面
这里写图片描述

首先,我们在 attrs.xml 文件中定义 3 个属性:分割线宽度、侧边栏宽度和手指滑动感应宽
度。

    <declare-styleable name="SliderMenu">
        <!-- 侧边栏宽度 -->
        <attr name="sliding_width" format="dimension"/>
        <!-- 分割线宽度 -->
        <attr name="separator" format="dimension"/>
        <!-- 手指滑动感应宽度 -->
        <attr name="touch_width" format="dimension"/>
    </declare-styleable>

定义一个名为 SliderMenu 的类,继承自 ViewGroup,定义相关的常量、变量,其次定义构造
方法完成初始化操作:获取属性值、定义绘制分割线需要的 Paint 对象、实例化 Scroller 对象用于完成惯性滚动。

public class SliderMenu extends ViewGroup {
    private static final String TAG = "SliderMenu";
    private static final int DO_MOVING = 0x001;//可以滑动
    private static final int NOT_MOVING = 0x002;//不可以滑动
    private int moving = NOT_MOVING; //是否可以滑动,默认不能滑动
    private static final int FLAG_SEPARATOR = 0x1;//标记变量,是否有分割线,占用最后一位

    private static final int FLAG_IS_OPEN = FLAG_SEPARATOR << 1;//标记变量,是否已打开,占用倒数第二位
    private int flags = FLAG_SEPARATOR >> 1;//存储标记变量
    private int slidingWidth;//侧边栏宽度
    private float separator;//分割线宽度
    private int touchWidth;//感应宽度
    private int screenWidth;//屏幕宽度
    private Paint paint;
    private int preX, firstX;
    private Scroller scroller;

    public SliderMenu(Context context) {
        this(context, null, 0);
    }
    public SliderMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    /**
     * 初始化
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public SliderMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.SliderMenu);
        slidingWidth = a.getDimensionPixelSize(
                R.styleable.SliderMenu_sliding_width,
                (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 150,
                        getResources().getDisplayMetrics()));
        separator = a.getDimensionPixelSize(R.styleable.SliderMenu_separator,
                (int)TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 1
                        , getResources().getDisplayMetrics()));
        touchWidth = a.getDimensionPixelSize(
                R.styleable.SliderMenu_touch_width,
                (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 50,
                        getResources().getDisplayMetrics()));
        if(separator > 0)
            flags = flags | FLAG_SEPARATOR;
        a.recycle();
        screenWidth = getScreenWidth(context);
        setBackgroundColor(Color.alpha(255));
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(separator);
        scroller = new Scroller(context);
    }
    /**
     * 子元素不能超过 2 个
     * @param child
     * @param index
     * @param params
     */
    @Override
    public void addView(View child, int index, LayoutParams params) {
        super.addView(child, index, params);
        if(getChildCount() > 2){
            throw new ArrayIndexOutOfBoundsException("Children count can't be more than 2.");
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        System.out.println("SlideMenu的位置left:" + left + " top:" + top + " right:" + right + " bottom:" + bottom);// left:0 top:0 right:540 bottom:863
        //SlideMenu的位置就是上面的System.out.println输出的log
        //这里需要确定的是SlideMenu的子View的位置
        //侧边栏子View初始位置是隐藏在手机屏幕的左边的,注意,此时menuView经过measure之后,menuView.getMeasuredWidth()就等于了menuWidth
        menuView.layout(-menuWidth, 0, 0, menuView.getMeasuredHeight());
        //主布局子View的位置,初始是显示在整个屏幕中
        mainView.layout(0, 0, mainView.getMeasuredWidth(), mainView.getMeasuredHeight());
    }

    private View menuView;//侧边栏子View
    private View mainView;//主体子View
    private int menuWidth;//侧边栏的宽度

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        menuView = getChildAt(0); //侧边栏布局
        mainView = getChildAt(1); //主体布局
        menuWidth = menuView.getLayoutParams().width;//侧边栏的宽度
    }
    /**
     * 获取屏幕宽度
     * @param context
     * @return
     */
    private int getScreenWidth(Context context){
        WindowManager wm = (WindowManager) getContext()
                .getSystemService(Context.WINDOW_SERVICE);
        Point point = new Point();
        wm.getDefaultDisplay().getSize(point);
        return point.x;
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(measureWidth(widthMeasureSpec),
                measureHeight(heightMeasureSpec));
    }

    /**
     * 总宽度 = 侧边栏宽度 + 主界面宽度 + 分割线宽度
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec){
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if(mode == MeasureSpec.AT_MOST){throw new IllegalStateException("layout_width can not be wrap_content.");
        }
        return (int) (screenWidth + slidingWidth + separator);
    }
    private int measureHeight(int heightMeasureSpec){
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        if(mode == MeasureSpec.AT_MOST){
            throw new IllegalStateException("layout_width can not be wrap_content");
        }
        int height = 0;
        if(mode == MeasureSpec.EXACTLY){
            height = size;
        }
        return height;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //画分割线
        if((flags & FLAG_SEPARATOR) == FLAG_SEPARATOR){
            int left = (int) (slidingWidth + separator / 2);
            canvas.drawLine(left, 0, left, getMeasuredHeight(), paint);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            Log.i(TAG, "X:" + ev.getX());
            if((flags & FLAG_IS_OPEN) == FLAG_IS_OPEN){
                moving = DO_MOVING;
            }else{
                if(ev.getX() > touchWidth)
                    moving = NOT_MOVING;
                else
                    moving = DO_MOVING;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            moving= NOT_MOVING;
            break;
    }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(moving == NOT_MOVING)
            return false;
        int x = (int) event.getX();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                preX = x;
                firstX = x;
                break;case MotionEvent.ACTION_MOVE:
                int dx = x - preX;
                this.scrollBy(-dx, 0);
                preX = x;
                break;
            case MotionEvent.ACTION_UP:
                dx = x - firstX;
                Log.i(TAG, "dx: " + dx);
                int remain = slidingWidth - Math.abs(dx);
//dx 右为正,左为负
                boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN;
                if(dx > 0 && !isOpen) {
                    scroller.startScroll(getScrollX(), 0, -remain, 0);
                    flags = flags | FLAG_IS_OPEN;
                }else if (dx < 0 && isOpen){
                    scroller.startScroll(getScrollX(), 0, remain, 0);
                    flags = flags & ~FLAG_IS_OPEN;
                }else{
//校正(比如向右滑又向左滑)
                    scroller.startScroll(getScrollX(), 0, dx, 0);
                }
                invalidate();
                break;
        }
        return moving == DO_MOVING;
    }
    @Override
    public void computeScroll() {
        if(scroller.computeScrollOffset()){
            this.scrollTo(scroller.getCurrX(), 0);
            postInvalidate();
        }
    }

    /**
     * 打开侧边栏
     */
    public void open(){
        boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN;
        if(!isOpen){
            scroller.startScroll(slidingWidth, 0, -slidingWidth, 0);
            invalidate();
            flags = flags | FLAG_IS_OPEN;
        }
    }
    /**
     * 关闭侧边栏
     */
    public void close(){
        boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN;
        if(isOpen){
            scroller.startScroll(0, 0, slidingWidth, 0);
            invalidate();
            flags = flags & ~FLAG_IS_OPEN;
        }
    }
    /**
     * 打开/关闭侧边栏
     */
    public void toggle(){
        boolean isOpen = (flags & FLAG_IS_OPEN) == FLAG_IS_OPEN;
        if(isOpen)
            close();
        else
            open();
    }
}

上面代码中使用了位运算来保存两个标识数据:打开状态和是否需要绘制分割线。

下来我们定义一个名为 sliding_menu.xml 的布局文件,布局中有一个 LinearLayout 布局和
FrameLayout 布局,分别用于显示侧边栏内容和主界面内容。布局文件的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<com.trkj.lizanhong.chapter9.SliderMenu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:trkj="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sm"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    trkj:separator="0.5dp"
    trkj:sliding_width="200dp"
    trkj:touch_width="50dp">
    <!-- 侧边栏-->
    <LinearLayout
        android:id="@+id/sliding_menu"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="test"
            android:text="测试" />
    </LinearLayout>
    <!-- 主界面 -->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_green_light">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="toggle"
            android:text="显示" />
    </FrameLayout>
</com.trkj.lizanhong.chapter9.SliderMenu>

定义继承自 Activity 名为 MainActivity 类,该类中响应 FrameLayout 布局中的按钮的单击事件,用于显示或隐藏侧边栏。内容如下:

public class MainActivity extends ActionBarActivity {
private SliderMenu sm;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.sliding_menu);
        sm = (SliderMenu) findViewById(R.id.sm);
        this.setTitle("侧边栏");
    }
    public void test(View view){
        Toast.makeText(this, "test", Toast.LENGTH_LONG).sho w();
    }
    public void toggle(View view){
        sm.toggle();
    }
}

继承自HorizontalScrollView 的侧边栏

和上一侧边栏的实现方案相比,继承自 HorizontalScrollView 显然能减少代码量,降低开发难度,但因受制于 HorizontalScrollView 灵活性也随之降低。本章我们将实现一个继承自
HorizontalScrollView 类的侧边栏。

这次实现的侧边栏在功能上有了少许变化:
Ø 在任何地方滑动都可以打开侧边栏;
Ø 滑动距离如果不超过侧边栏一半,则回退;
Ø 不再支持分割线。

HorizontalScrollView 作为水平滚动视图,已经实现了平滑滚动的功能,这可以让我们不再通过 Scroller 实现自动滑动。HorizontalScrollView 定义的平滑滚动方法原型为:

public final void smoothScrollTo(int x, int y)
该方法和 scrollTo()方法不同,支持平滑滚动,而不是直接跳到(x,y)位置。与之对应的还有 public final void smoothScrollBy(int dx, int dy)方法,该方法类似于 scrollBy()方法,但同样支持平滑滚动。

另外,滚动视图有一个规则,子组件只能有一个,所以,我们必须将侧边栏和主界面使用一个水平的 LinarLayout 包裹起来,因此,布局文件的结构与上一个侧边栏也会有所不同。

侧边栏支持指定宽度,在 attrs.xml 中添加如下的配置:

<declare-styleable name="SlidingMenu">
    <!-- 指定侧边栏的宽度 -->
    <attr name="left_padding_width" format="dimension"/>
</declare-styleable>

在 onMeasure()方法中测量组件宽度时,必须指定侧边栏和主界面的宽度,侧边栏宽度由
left_padding_width 属性指定,并应用到组件的 LayoutParams 属性中;主界面的宽度和屏幕的高度相同。HorizontalScrollView 已经为每个组件进行了定位,所以我们也不再在重写 onLayout()方法中为每个子组件定位。

/**
* 指定侧边栏和主界面的宽
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    once = true;
    //水平滚动 View 只能有一个直接子组件
    LinearLayout subView = (LinearLayout) this.getChildAt(0);
    //指定侧边栏和主界面的宽
    //第 0 个子组件就是侧边栏,第 1 个组件就是主界面
    LinearLayout slidingMenu =
    (LinearLayout) subView.getChildAt(0);
    //第 1 个子组件是主界面
    ViewGroup content = (ViewGroup) subView.getChildAt(1);
    //设置侧边的宽度
    slidingMenu.getLayoutParams().width = leftPaddingWidth;
    //设置主界面的宽度
    content.getLayoutParams().width = this.getScreenWidth();
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

而在 onLayout()方法中,我们只需要在运行时将侧边栏隐藏起来。

/**
* 初始状态下在该方法中隐藏侧边栏
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    //下面的代码只调用一次
    if(once){
        //隐藏侧边栏
        this.scrollTo(leftPaddingWidth, 0);
    }
        once = false;
}

HorizontalScrollView 类中早已实现了子组件的平滑滚动,我们要做的就是当手指松开后的惯性滚动。当手指滑动距离超过侧边栏的一半则继续向前滑动,否则回滚到初始状态。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    //当手指松开的时候,决定侧边栏是显示还是隐藏
    //如果侧边栏滚出屏幕的宽度大于侧边栏的一半,则隐藏侧边栏
    //如果侧边栏滚出屏幕的宽度小于等于侧边栏的一半,则显示侧边栏
    if(ev.getAction() == MotionEvent.ACTION_UP){
        int dx = this.getScrollX();
        int halfWidth = this.leftPaddingWidth / 2;
        Log.i("SlidingMenu", "dx:" + this.getScrollX() + " halfWidth:" + halfWidth);
            if(dx < halfWidth){
                //显示侧边栏
                this.smoothScrollTo(0, 0);
                Log.i("SlidingMenu", "显示");
                this.isOpen = true;
            }else{
                //隐藏侧边栏
                this.smoothScrollTo(leftPaddingWidth, 0);
                Log.i("SlidingMenu", "隐藏");
                this.isOpen = false;
            }
        return true;
    }
    return super.onTouchEvent(ev);
}

SlidingMenu 的全部源码如下(省略了前面已列出的代码):


public class SlidingMenu extends HorizontalScrollView {
    private int leftPaddingWidth; //侧边栏的宽度
    private boolean isOpen = false;//侧边栏是否打开
    private boolean once = false;//默认只隐藏一次
    /**
     * 获取屏幕宽度
     * @return
     */
    private int getScreenWidth(){
//请参考上一案例
    }
    /**
     * 指定侧边栏和主界面的宽
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        ……
    }
    /**
     * 默认情况下在该方法中隐藏侧边栏
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ……
    }
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ……
    }
    /**
     * 打开
     */
    public void open(){
        if(!isOpen){
            this.smoothScrollTo(0, 0);
            isOpen = true;
        }
    }
    /**
     * 隐藏
     */
    public void hide(){
        if(isOpen){
                    this.smoothScrollTo(leftPaddingWidth, 0);
            isOpen = false;
        }
    }
    /**
     * 打开隐藏
     */
    public void toggle(){
        if(isOpen){
            hide();
        }else{
            open();
        }
    }
    public SlidingMenu(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    public SlidingMenu(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.SlidingMenu);
        leftPaddingWidth = a.getDimensionPixelSize(
                R.styleable.SlidingMenu_left_padding_width,
                (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 200,
                        context.getResources().getDisplayMetrics()));
        a.recycle();
    }
    public SlidingMenu(Context context) {
        super(context);
    }
}

定义 sliding_menu2.xml 布局文件,仔细比较该布局文件与上一解决方案中布局文件的区别。因为实现不同,所以二者可能会有一些不得已的限制。从 ViewGroup 上继承也许是灵活性最好的。

<?xml version="1.0" encoding="utf-8"?>

<com.trkj.lizanhong.chapter9.SlidingMenu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:trkj="http://schemas.android.com/apk/res-auto"
    android:id="@+id/slidingMenu"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:scrollbars="none"
    trkj:left_padding_width="200dp">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal">
        <!-- 侧边栏 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:background="#FF0000"
            android:orientation="vertical"></LinearLayout>
        <!-- 主界面 -->
        <LinearLayout
            android:layout_width="wrap_content"

            android:layout_height="match_parent"
            android:background="#FFFFFF"
            android:orientation="vertical">

            <Button
                android:id="@+id/btn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="toggle"
                android:text="侧边栏" />
        </LinearLayout>
    </LinearLayout>
</com.trkj.lizanhong.chapter9.SlidingMenu>

运行效果如图所示。
这里写图片描述

谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309

猜你喜欢

转载自blog.csdn.net/u011733020/article/details/81003248