(二十九)DrawerLayout 打造炫酷侧滑菜单

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、DrawerLayout 的 demo

先来看一下 DrawerLayout 的简单 demo。

效果:
这里写图片描述

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="com.xiaoyue.drawerlayout.MainActivity">

    <android.support.v4.widget.DrawerLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:layout_editor_absolutyX="5dp"
        tools:layout_editor_absolutyY="5dp">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/xiaoyue"
            />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorAccent"
            android:layout_gravity="start">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:text="侧滑菜单" />

        </LinearLayout>

    </android.support.v4.widget.DrawerLayout>

</LinearLayout>

主要在布局文件里面引用 DrawerLayout 即可,DrawerLayout 下的子控件,当添加属性android:layout_gravity 的值 为 “start”,即可设置为侧边菜单。

上面虽然在侧边菜单设置宽度为 match_parent,但实际显示效果并没有占满屏幕。这是抽屉里的宽度不能超过320 dp, 所以用户总是可以看到内容视图的一部分。

**注:这边不要在最外层使用 ConstraintLayout。**DrawerLayout 要求宽高必须是 MeasureSpec.EXACTLY。正常情况下 match_parent 和具体值都是 MeasureSpec.EXACTLY,但 ConstraintLayout 的 match_parent,有点不同,不是。

二、DrawerLayout 方法介绍

1. setDrawerLockMode

锁住 DrawerLayout 的指定模式(侧滑界面出现或关闭),不再允许滑动改变。

setDrawerLockMode(@LockMode int lockMode):
传入指定的锁住模式,左右两边都不在允许侧滑。

setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity):
传入指定的锁住模式和哪一边的侧滑进行锁住,对选择的侧滑进行锁住。

setDrawerLockMode(@LockMode int lockMode, View drawerView):
传入指定的锁住模式和要进行锁住的侧滑 view。

2.addDrawerListener

添加滑动的监听:

     drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() {

            /**
             * 监听滑动距离
             * @param drawerView 侧滑菜单
             * @param slideOffset 滑动的百分比(完全打开的时候为 1)
             */
            @Override
            public void onDrawerSlide(View drawerView, float slideOffset) {

            }

            /**
             * 当侧滑打开的时候
             * @param drawerView
             */
            @Override
            public void onDrawerOpened(View drawerView) {

            }

            /**
             * 当侧滑关闭的时候
             * @param drawerView
             */
            @Override
            public void onDrawerClosed(View drawerView) {

            }

            /**
             * 当状态改变的时候(有三种状态)
             * DrawerLayout.STATE_IDLE 静止
             * DrawerLayout.STATE_DRAGGING 准备滑动(手指一点上去就会触发)
             * DrawerLayout.STATE_SETTLING 滑动完成之后
             * @param newState
             */
            @Override
            public void onDrawerStateChanged(int newState) {
            }
        });

比较重要的是 onDrawerSlide,onDrawerStateChanged 的三种状态最好自己日志添加去试一下,不好讲清楚。

三、自定义侧滑菜单

1.效果

这里写图片描述

这边如果说侧滑菜单只使用一个自定义 ViewGroup 进行实现的话,会比较复杂。

1.要重写 onLayout 方法,当然也可以直接继承 LinearLayout 等。
2.ViewGroup 的 onDraw 方法不一定都会被调用。如果重写 dispatchOnDraw 方法,又会导致每个子控件也进行重新绘制。
3.监听事件的处理。

这边采用职责分明的策略,用多个自定义控件组合实现这个效果。

1.实现 MyDrawerLayout 继承 DrawerLayout,用自带的监听处理滑动事件
2.实现 MyDrawerLayoutSlideBar 继承LinearLayout,处理子控件的摆放问题
3.实现 MyDrawerLayoutBgView 继承 View,进行侧滑菜单的背景绘制(蓝色背景部分,绘制区域随手指变化)
4.实现 MyDrawerBgRelativeLayout 继承 RelativeLayout,把 MyDrawerLayoutSlideBar 和 MyDrawerLayoutBgView 组合在一起,形成真正的侧滑菜单

2.布局文件

MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.xiaoyue.drawerlayout.MainActivity">
    <com.xiaoyue.widget.mydrawerlayout.MyDrawerLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--内容  区域-->
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitXY"
            android:src="@drawable/fake"
            />
        <!--侧滑区域  LinearLayout    ColorDrawable -->
        <com.xiaoyue.widget.mydrawerlayout.MyDrawerLayoutSlideBar
            android:layout_width="200dp"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            app:maxTranslationX="66dp"
            android:background="@color/colorPrimaryDark"
            >
            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/circle"
                android:text="朋友圈" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/wallet"
                android:text="钱包" />

            <TextView
                style="@style/MenuText"
                android:drawableLeft="@drawable/coupon"
                android:text="优惠券" />

        </com.xiaoyue.widget.mydrawerlayout.MyDrawerLayoutSlideBar>


    </com.xiaoyue.widget.mydrawerlayout.MyDrawerLayout>

</RelativeLayout>

修改完成的自定义控件的使用尽量保证与原先的使用风格一样,这边在布局文件中就像是使用 DrawerLayout 一样。

3.自定义的 DrawerLayout

MyDrawerLayout:

public class MyDrawerLayout extends DrawerLayout implements DrawerLayout.DrawerListener{

    //侧滑菜单
    private MyDrawerLayoutSlideBar mSlideBar;
    //用来装载 MyDrawerLayoutSlideBar
    private MyDrawerBgRelativeLayout mBgRelativeLayout;
    //内容
    private View mContenView;

    //侧滑菜单的滑动百分比
    private float mSlideOffset;
    //当前手指触摸的 Y 坐标
    private float mTouchY;

    public MyDrawerLayout(Context context) {
        super(context);
    }

    public MyDrawerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        init();
    }

    private void init() {

        //循环子控件,获取内容的 View 和侧滑菜单
        for(int i = 0; i < getChildCount(); i ++) {
            View childView = getChildAt(i);

            if (childView instanceof MyDrawerLayoutSlideBar) {
                mSlideBar = (MyDrawerLayoutSlideBar) childView;
            } else {
                mContenView = childView;
            }
        }

        //偷梁换柱
        //为 MyDrawerLayoutSlideBar 添加一层 MyDrawerBgRelativeLayout包装

        //先移除 MyDrawerLayoutSlideBar
        removeView( mSlideBar);
        //把 MyDrawerLayoutSlideBar 添加到 MyDrawerBgRelativeLayout 下
        mBgRelativeLayout = new MyDrawerBgRelativeLayout(mSlideBar);
        addView(mBgRelativeLayout);

        addDrawerListener(this);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //不能在 OnTouch 中获取 Y 的坐标,避免事件被子控件消费的时候,获取不到
        mTouchY = ev.getY();

        if (ev.getAction() == MotionEvent.ACTION_UP) {
            //手指松开的时候,侧滑菜单关闭
            closeDrawers();
            mSlideBar.onMotionUp(mTouchY);
            return super.dispatchTouchEvent(ev);
        }

        if (mSlideOffset == 1 ) {
            mBgRelativeLayout.setTouchY(mTouchY, mSlideOffset);
        }

        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {
        mSlideOffset = slideOffset;
        //传递触摸 Y 坐标,触发背景动画
        mBgRelativeLayout.setTouchY(mTouchY, slideOffset);

        //设置内容区域向右偏移为侧滑菜单的一半,形成视觉差
        float contentViewOffset = drawerView.getWidth() * slideOffset / 2;
        mContenView.setTranslationX(contentViewOffset);
    }

    @Override
    public void onDrawerOpened(View drawerView) {

    }

    @Override
    public void onDrawerClosed(View drawerView) {

    }

    @Override
    public void onDrawerStateChanged(int newState) {

    }
}

MyDrawerLayout 主要做三件事:一是对布局文件中的侧滑菜单 MyDrawerLayoutSlideBar 进行了一次包装,偷偷把 MyDrawerLayoutSlideBar 换成 MyDrawerBgRelativeLayout,并添加 MyDrawerLayoutBgView,这也是核心的一个实现思路。二是对侧滑菜单的滑动进行监听,实现对内容区域的同步滑动。三是监听手指的动作,每一次滑动进行背景的变化,以及每次手指离开屏幕的时候,关闭侧滑菜单。

4.侧滑菜单

MyDrawerLayoutSlideBar:

public class MyDrawerLayoutSlideBar extends LinearLayout {

    //子控件的最大偏移量
    private float maxTranslationX;

    public MyDrawerLayoutSlideBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {

        //设置摆放方向为竖直方向
        setOrientation(VERTICAL);

        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SideBar);
            maxTranslationX = typedArray.getDimension(R.styleable.SideBar_maxTranslationX, 0);
            typedArray.recycle();
        }
    }

    /**
     * 对子控件进行相应的偏移
     * @param y 当前触摸点 Y 坐标
     * @param slideOffset 侧滑栏菜单滑出的百分比
     */
    public void setTouchY(float y, float slideOffset) {

        //遍历全部子控件  给每一个子控件进行偏移
        for (int i=0;i<getChildCount();i++) {
            View chlid= getChildAt(i);

            //偏移方法
            apply(getParent(), chlid, y, slideOffset);
        }
    }

    /**
     * 对子控件进行偏移
     * @param parent 父控件
     * @param childView 要偏移的子控件
     * @param y 偏移最大的 Y 坐标
     * @param slideOffset 偏移比例
     */
    private void apply(ViewParent parent, View childView, float y, float slideOffset) {

        //计算子控件的中点 Y 坐标
        int centerY = (childView.getTop() + childView.getBottom()) / 2;
        //计算子控件中点与手指触摸的 Y 方向距离
        float distanceY = Math.abs(y - centerY);

        //计算子控件偏移距离
        float scale = distanceY / getHeight() * 3;  //3   放大系数
        float translationX = maxTranslationX * (1f - scale);

        childView.setTranslationX(translationX);

    }

    /**
     * 手指松开的时候处理
     * @param y 触摸点 Y 坐标
     */
    public void onMotionUp(float y) {
        View childView;
        for (int i=0; i<getChildCount(); i++) {
            childView = getChildAt(i);

            childView.setPressed(false);
            //要判断  y坐落在哪一个子控件    松手的那一刻  进行回调  跳转其他页面
            boolean isHover = y > childView.getTop() && y < childView.getBottom();
            if (isHover) {
                childView.performClick();

                //回调操作,可以采用监听,这边使用吐司只是为了验证回调到了
                Toast toast = Toast.makeText(getContext(), ((TextView) childView).getText(), Toast.LENGTH_SHORT);
                toast.show();

                break;
            }
        }
    }
}

MyDrawerLayoutSlideBar 是被引用的布局文件里,从表面看这个是侧滑菜单,实际是上只是管理侧滑菜单的每一个子控件。当手指触摸屏幕进行滑动时候,背景变化,每个子控件做出相应的位移(位移大小没有具体参考值,需要对实际情况进行调整)。另外是在手指松开的时候,会去判断调用哪一个子控件的触摸方法(只根据 Y 进行判断,没有进行 X 的处理)。

5.背景绘制

MyDrawerLayoutBgView:

public class MyDrawerLayoutBgView extends View {

    //画笔
    private Paint mPaint;
    //路径
    private Path mPath;

    private Drawable mDrawable;

    public MyDrawerLayoutBgView(Context context) {
        this(context, null);
    }

    public MyDrawerLayoutBgView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化参数
     */
    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPath = new Path();
    }

    /**
     * 设置当前触摸点的 Y 坐标,从而改变背景波浪
     * @param y 当前触摸点 Y 坐标
     * @param slideOffset 侧滑栏菜单滑出的百分比
     */
    public void setTouchY(float y, float slideOffset) {
        //重置路径
        mPath.reset();

        //获取侧滑菜单滑出来的宽度
        float width = getWidth() * slideOffset;
        float height = getHeight();
        //计算贝塞尔曲线 Y 方向超出去的距离(8 效果可能会好一些)
        float offsetY = height / 8;
        //计算被赛尔曲线 X 方向偏移的距离(也是为了效果好一些)
        float offsetX = width / 2;

        mPath.lineTo(offsetX, - offsetY);
        mPath.quadTo( width * 3 / 2, y, offsetX , height + offsetY);
        mPath.lineTo(0,height);
        mPath.close();

        invalidate();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mDrawable == null) {
            //绘制背景为颜色的
            canvas.drawPath(mPath, mPaint);
        } else {
            //绘制背景为图片的,根据 Path 对 图片进行截取
            mDrawable.setBounds(0, 0, getWidth(), getHeight());
            canvas.clipPath(mPath);
            mDrawable.draw(canvas);
        }
    }

    /**
     * 设置背景
     * (供 MyDrawerBgRelativeLayout 传递 MyDrawerLayoutSlideBar 的 background )
     * @param drawable
     */
    public void setDrawable(Drawable drawable) {
        if (drawable instanceof ColorDrawable) {
            //支持背景为颜色
            mPaint.setColor(((ColorDrawable) drawable).getColor());
        }else {
            //支持背景为图片
            mDrawable = drawable;
        }
    }

}

MyDrawerLayoutBgView 对贝塞尔曲线(蓝色背景)进行确认,背景是在 MyDrawerBgRelativeLayout包装 MyDrawerLayoutSlideBar 的时候,把 MyDrawerLayoutSlideBar 的背景传递进来(目前支取支持颜色和背景图片,其他未尝试)。

四、附

代码链接:http://download.csdn.net/download/qq_18983205/10112208

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值