自定义View-侧滑菜单

最近在使用酷我音乐软件时看到它的侧滑菜单,突然想起来当年qq5.0时的侧滑菜单,虽然网上有很多实现方式,但是为了纪念我Q还是自己来码一个纪念我的青春,效果如下图,啥也不说来直接开干吧!
在这里插入图片描述

一、搭建基本框架

  1. 创建类ViewGroup的子类SlideLayout,实现提示的内容如下:

    public class SlideLayout extends ViewGroup {
        public SlideLayout(Context context) {
            this(context, null);
        }
        public SlideLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
        public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
        }
    }
    
  2. 先不管onLayout方法,因为我们还不知道相应View的宽高,无法进行onLayout,所以我们直接进行onMeasure测量,个人觉得在侧滑菜单主功能界面基本上不会使用到margin,所以弃用此功能,因为我们只有两个部分,所以我们需要保证SlideLayout只能有两个子View否则就抛出异常,然后调用方法measure(widthMeasureSpec, heightMeasureSpec)统一一次性测量两个View,代码如下:

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (getChildCount() != 2) {
                throw new IllegalArgumentException("The SlideLayout only have two child view");
            }
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
  3. 测量完后我们就可以进行layout了,因为侧滑菜单在关闭状态下时是隐藏在左边或者右边的,我们就以左边为基准吧,但是获取子View进行layout时,我们无法得知哪个View侧滑菜单,所以我们还需要确定两个View中哪个是侧滑菜单模块, 这里有两个思路,第一个思路是我们可以自定义ViewGroup-SlideMenuLayout来管理侧滑菜单,在SlideLayout中通过instanceof关键字来判断哪个是侧滑菜单模块,第二个思路比较简单,直接自定义属性slideMenuId来映射侧滑菜单,在通过对应ViewId来判断当前的模块是否是我们的侧滑菜单,我选择第二种代码如下:
    1)创建自定义属性slideMenuId如下:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <declare-styleable name="SlideLayout" >
            <!--侧滑菜单模块的ViewId-->
            <attr name="slideMenuId" format="reference" />
        </declare-styleable>
    
    </resources>
    

    2)在构造函数public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr)中获取侧滑菜单的ViewId,若未能获取到侧滑菜单Id则抛出异常

        /**
         * 侧滑菜单Id
         */
        private int mSlideMenuId;
    
        public SlideLayout(Context context) {
            this(context, null);
        }
    
        public SlideLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
    
        public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            //获取自定义属性
            TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout);
            mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1);
            styledAttributes.recycle();
            //判断是否成功获取到侧滑菜单Id
            if (mSlideMenuId == -1) {
                throw new IllegalArgumentException("Can't find SlideMenuId");
            }
        }
    

    3)进行onLayout操作,如果是我们的主功能ViewGroup模块,则填充整个屏幕,若是侧滑菜单则在主功能的左侧也就是贴着屏幕的左边,如下:

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < 2; i++) {
                View childView = getChildAt(i);
                if (childView.getId() == mSlideMenuId) {
                    childView.layout(-childView.getMeasuredWidth(), 0, 0, childView.getMeasuredHeight());
                } else {
                    childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
                }
            }
        }
    
  4. 到此简单的onMeasureonLayout操作就完了,我们来运行测试看一下效果吧。
    1)布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <vip.zhuahilong.jdapplication.widget.SlideLayout 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"
        app:slideMenuId="@id/slideMenuId"
        tools:context=".ui.activity.MainActivity">
    
        <LinearLayout
            android:id="@+id/mainViewLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolBar"
                android:layout_width="match_parent"
                android:background="@color/colorPrimary"
                android:layout_height="?actionBarSize"
                app:contentInsetStart="@dimen/dp0">
    
                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">
    
                    <TextView
                        android:id="@+id/title_tv"
                        android:layout_width="wrap_content"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:layout_centerInParent="true"
                        android:text="@string/app_name"
                        android:textColor="@color/colorAccent"
                        android:textSize="@dimen/sp20" />
    
                </RelativeLayout>
    
            </android.support.v7.widget.Toolbar>
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="主要内容"
                android:textColor="@color/colorPrimary"
                android:textSize="@dimen/sp30" />
    
        </LinearLayout>
    
        <LinearLayout
            android:id="@+id/slideMenuId"
            android:background="@color/colorPrimary"
            android:layout_width="@dimen/dp200"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp200"
                android:background="@drawable/slide_menu_mead_bg">
    
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:src="@mipmap/ic_launcher" />
    
            </RelativeLayout>
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/gray"
                android:orientation="vertical">
    
                <TextView
                    android:id="@+id/slideMenuItem1"
                    android:layout_width="match_parent"
                    android:layout_height="?actionBarSize"
                    android:layout_marginTop="@dimen/dp50"
                    android:gravity="center"
                    android:text="slideMenuItem1"
                    android:textSize="@dimen/sp18" />
    
                <TextView
                    android:id="@+id/slideMenuItem2"
                    android:layout_width="match_parent"
                    android:layout_height="?actionBarSize"
                    android:layout_marginTop="@dimen/dp10"
                    android:gravity="center"
                    android:text="slideMenuItem1"
                    android:textSize="@dimen/sp18" />
    
                <TextView
                    android:id="@+id/slideMenuItem3"
                    android:layout_width="match_parent"
                    android:layout_height="?actionBarSize"
                    android:layout_marginTop="@dimen/dp10"
                    android:gravity="center"
                    android:text="slideMenuItem1"
                    android:textSize="@dimen/sp18" />
    
                <TextView
                    android:id="@+id/slideMenuItem4"
                    android:layout_width="match_parent"
                    android:layout_height="?actionBarSize"
                    android:layout_marginTop="@dimen/dp10"
                    android:gravity="center"
                    android:text="slideMenuItem1"
                    android:textSize="@dimen/sp18" />
    
    
            </LinearLayout>
    
    
        </LinearLayout>
    
    </vip.zhuahilong.jdapplication.widget.SlideLayout>
    
    

    运行效果为:
    在这里插入图片描述
    由于我们还未编码侧滑菜单的打开和关闭,所以下现在无法看到侧滑菜单,下面来编码打开关闭侧滑菜单部分。

二、实现侧滑菜单功能

经分析可知,我们的菜单主要是通过滑动来打开,再通过滑动或点击部分区域来关闭,因此我们要明确是否发生来滑动事件且要实时更新当前事件类型和菜单的状态。

  1. 定义侧滑菜单状态MENU_STATE_OPEN-打开状态MENU_STATE_CLOSE-关闭状态、MENU_STATE_MASK-侧滑菜单状态标志位及Touch事件EVENT_CLICK-单击事件EVENT_MOVE-滑动事件、EVENT_MASK-Touch事件标志位,定义变量mSlideLayoutFlag来存储这些状态,并在构造方法里初始化mSlideLayoutFlag = (~EVENT_MASK) | (~MENU_STATE_MASK);

    /**
         * TouchEvent标志位
         */
        private int EVENT_MASK = 0x1;
    
        /**
         * TouchClick事件
         */
        private int EVENT_CLICK = 0x0;
    
        /**
         * TouchMove事件
         */
        private int EVENT_MOVE = 0x1;
    
        /**
         * 侧滑菜状态标志位
         */
        private int MENU_STATE_MASK = 0x02;
    
        /**
         * 侧滑菜单 打开状态
         */
        private int MENU_STATE_OPEN = 0x02;
    
        /**
         * 侧滑菜单 关闭状态
         */
        private int MENU_STATE_CLOSE = 0x00;
    
        /**
         * 侧滑菜单状态存储器
         */
        private int mSlideLayoutFlag;
        public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            //略
            mSlideLayoutFlag = ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT;
        }
    
  2. 创建滑动工具类private Scroller mScroller且在构造方法里初始化mScroller = new Scroller(context);

  3. 重写onTouchEvent方法,实现菜单滑动
    1)滑动菜单顾名思义就是滑动打开菜单,且为了更方便的关闭菜单我们采用点击主功能页面区域来关闭当前打开的滑动菜单的,也就说打开菜单我们只是用滑动的方式,关闭菜单我们可以使用滑动和点击部分区域来关闭,因此我们要区分当前的事件到时是点击事件还是滑动事件,所以我们要在ACTION_DOWN中更改状态存储器mSlideLayoutFlag对应事件标志位值位EVENT_CLICK,同理在ACTION_MOVE中更新为EVENT_MOVE,为此还需要创建更新mSlideLayoutFlag的方法updateTargetMaskValue,具体代码如下:

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getActionMasked()) {
                case 0:
                    updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                    break;
                case 2:
                    if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                        updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                    }
                    break;
                case 1:
                case 3:
                    break;
            }
            return true;
        }
    
        /**
         * 更新flag的位值
         *
         * @param mask  目标标记位
         * @param value 最新值
         */
        private void updateTargetMaskValue(int mask, int value) {
            mSlideLayoutFlag = (mSlideLayoutFlag & ~mask) | (mask & value);
        }
    
    

    2)尽管上面已经定义的事件类型,为了更简单直接,我们先处理滑动事件,后面再回过头来处理点击事件,想要能够滑动我们只需要知道滑动的距离及方向即可,因此我们要记录上次TouchEventX坐标,定义变量lastX来存储,且在TouchEventDown事件中进行初始化,然后在ACTION_MOVE事件中计算我们需要滑动距离正负号表示滑动的方向,负号表示内容向右滑动即打开菜单正号表示内容向左滑动即关闭菜单,所以我们计算floatdeltaX=lastX-event.getX()即是我们需要移动的差值且包含来正确的方向,调用方法scrollBy(deltaX,0)即完成来此处滑动,但是我们还需要注意滑动距离的范围问题 ,有可能我们的滑动超过来菜单的宽度,导致我们看到一片空白,所以我们还要对滑动的距离作出限制,因为滑动范围是和侧滑菜单的宽度有关系的,所以在onlayout方法中获取到我们侧滑菜单的宽度用mSlideMenuWidth存储,则滑动的距离mScrollX的范围为[-mSlideMenuWidth,0],我们根据getScrollX+deltaX的值判断是否在这个范围内,若getScrollX()+deltaX<=mSlideMenuWidth则scrollTo(-mSlideMenuWidth,0),若getScrollX()+deltaX>=0scrollTo(0,0),否则为scrollBy(deltaX,0),最后更新lastX的值,代码如下:

        /**
         * last touch-event X
         */
        private float lastX;
    
    
        /**
         * 菜单的宽度
         */
        private int mSlideMenuWidth;
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < 2; i++) {
                View childView = getChildAt(i);
                if (childView.getId() == mSlideMenuId) {
                    mSlideMenuWidth = childView.getMeasuredWidth();
                    childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
                } else {
                    childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
                }
            }
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getActionMasked()) {
                case 0:
                    updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                    lastX = event.getX();
                    break;
                case 2:
                    if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                        updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                    }
                    float deltaX = lastX - event.getX();
                    if (getScrollX() + deltaX <= -mSlideMenuWidth) {
                        scrollTo(-mSlideMenuWidth, 0);
                    } else if (getScrollX() + deltaX >= 0) {
                        scrollTo(0, 0);
                    } else {
                        scrollBy((int) deltaX, 0);
                    }
                    lastX = event.getX();
                    break;
                case 1:
                case 3:
                    break;
            }
            return true;
        }
    
    

运行看一下效果如下图:
在这里插入图片描述
3)从上面来看使用滑动打开关闭菜单的功能基本已经完成,但是若在菜单打开或者关闭一半时,手机离开屏幕,则菜单保持当前状态不变了,体验极差,所以我们还要处理事件ACTION_UP和ACTION_CANCEL来完成接下来未完成的动作,我们根据滑动的距离mScrollXgetScrollX()和菜单宽度的一半来判断当前的目标状态,若getScrollX() < -mSlideMenuWidth / 2 则更新菜单状态为打开,使用工具类mScroller完成当前菜单位置到打开状态下菜单位置的过度过程,若getScrollX() >= -mSlideMenuWidth / 2则更新菜单状态为关闭,使用工具类mScroller完成当前菜单位置到关闭状态下菜单位置的过度过程,调用方法invalidate()开始过渡,其中还需要我们重写方法 computeScroll(),来不停的刷新绘制某一状态下菜单的位置,直到完成整个过程,为止,代码如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case 0:
                updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                lastX = event.getX();
                break;
            case 2:
                if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                    updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                }
                float deltaX = lastX - event.getX();
                if (getScrollX() + deltaX <= -mSlideMenuWidth) {
                    scrollTo(-mSlideMenuWidth, 0);
                } else if (getScrollX() + deltaX >= 0) {
                    scrollTo(0, 0);
                } else {
                    scrollBy((int) deltaX, 0);
                }
                lastX = event.getX();
                break;
            case 1:
            case 3:
                if (getScrollX() < -mSlideMenuWidth / 2) {
                    updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
                    mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
                } else {
                    updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                    mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                }
                invalidate();
                break;
        }
        return true;
    }
    
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

运行看一下效果,见下图:
在这里插入图片描述
4)由上可见基本的滑动菜单效果就实现了,接下来就是缩放View了,为了更加灵活,在自定义属性中增加主页View菜单View的目标缩放值, 默认值都为0.7f,具体为:

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

    <declare-styleable name="SlideLayout">
        <!--侧滑菜单模块的ViewId-->
        <attr name="slideMenuId" format="reference" />
        <!--侧滑菜单模块的缩放值-->
        <attr name="slideMenuTargetScale" format="float" />
        <!--主页View模块的缩放值-->
        <attr name="contentViewTargetScale" format="float" />
    </declare-styleable>

</resources>

    /**
     * 侧滑菜单的目标缩放值
     */
    private float mSlideMenuTargetScale;

    /**
     * 主功能View的目标缩放值
     */
    private float mContentViewTargetScale;
    
    public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout);
        mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1);
        mSlideMenuTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_slideMenuTargetScale, 0.7f);
        mContentViewTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_contentViewTargetScale, 0.7f);
        styledAttributes.recycle();
        //判断是否成功获取到侧滑菜单Id
        if (mSlideMenuId == -1) {
            throw new IllegalArgumentException("Can't find SlideMenuId");
        }
        //初始化标志位状态
        mSlideLayoutFlag =  ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT;
        //初始化滑动工具类
        mScroller = new Scroller(context);
    }

同时我们定义侧滑菜单View:mSlideMenuView和主页View:mContentViewonLayout中获取到两个模块的View引用,根据我们的滑动距离来动态的缩放View,在菜单打开状态下,mSlideMenuView为原始大小,mContentView缩放到原始的mContentViewTargetScale倍;在菜单关闭状态下,mSlideMenuView为原始大小的mSlideMenuTargetScale倍,mContentView为原始大小,所以我们可以根据滑动的距离和缩放的范围来动态的计算当前对应模块的缩放值,mSlideMenuView缩放值为mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale)mContentView缩放值为1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale)代码如下:

    /**
     * 侧滑菜单View引用
     */
    private View mSlideMenuView;

    /**
     * 主页View引用
     */
    private View mContentView;
    
	@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < 2; i++) {
            View childView = getChildAt(i);
            if (childView.getId() == mSlideMenuId) {
                mSlideMenuView = childView;
                mSlideMenuWidth = childView.getMeasuredWidth();
                childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
            } else {
                childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
                mContentView = childView;
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case 0:
                updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                lastX = event.getX();
                break;
            case 2:
                if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                    updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                }
                float deltaX = lastX - event.getX();
                if (getScrollX() + deltaX <= -mSlideMenuWidth) {
                    scrollTo(-mSlideMenuWidth, 0);
                } else if (getScrollX() + deltaX >= 0) {
                    scrollTo(0, 0);
                } else {
                    scrollBy((int) deltaX, 0);
                }
                float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
                float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
                mSlideMenuView.setScaleX(currentSlideMenuViewScale);
                mSlideMenuView.setScaleY(currentSlideMenuViewScale);
                mContentView.setScaleX(currentContentViewScale);
                mContentView.setScaleY(currentContentViewScale);
                lastX = event.getX();
                break;
            case 1:
            case 3:
                if (getScrollX() < -mSlideMenuWidth / 2) {
                    updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
                    mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
                } else {
                    updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                    mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                }
                invalidate();
                break;
        }
        return true;
    }

运行效果如下:
在这里插入图片描述
这样就基本完成来我们仿QQ5.0的大致效果,但是看起来不是那么优雅,我们修改侧滑菜单的背景为透明,给SlideLayout设置背景图片,布局文件为:

<?xml version="1.0" encoding="utf-8"?>
<vip.zhuahilong.jdapplication.widget.SlideLayout 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"
    android:background="@drawable/star_bg"
    app:slideMenuId="@id/slideMenuId"
    tools:context=".ui.activity.MainActivity">

    <LinearLayout
        android:id="@+id/mainViewLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolBar"
            android:layout_width="match_parent"
            android:background="@color/colorPrimary"
            android:layout_height="?actionBarSize"
            app:contentInsetStart="@dimen/dp0">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <TextView
                    android:id="@+id/title_tv"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center"
                    android:layout_centerInParent="true"
                    android:text="@string/app_name"
                    android:textColor="@color/colorAccent"
                    android:textSize="@dimen/sp20" />

            </RelativeLayout>

        </android.support.v7.widget.Toolbar>

        <TextView
            android:id="@+id/content_tv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/gray"
            android:gravity="center"
            android:text="主要内容"
            android:textColor="@color/colorPrimary"
            android:textSize="@dimen/sp30" />


    </LinearLayout>

    <LinearLayout
        android:id="@+id/slideMenuId"
        android:background="@android:color/transparent"
        android:layout_width="@dimen/dp200"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp200">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:src="@mipmap/ic_launcher" />

        </RelativeLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:id="@+id/slideMenuItem1"
                android:layout_width="match_parent"
                android:layout_height="?actionBarSize"
                android:layout_marginTop="@dimen/dp50"
                android:gravity="center"
                android:text="slideMenuItem1"
                android:textSize="@dimen/sp18" />

            <TextView
                android:id="@+id/slideMenuItem2"
                android:layout_width="match_parent"
                android:layout_height="?actionBarSize"
                android:layout_marginTop="@dimen/dp10"
                android:gravity="center"
                android:text="slideMenuItem1"
                android:textSize="@dimen/sp18" />

            <TextView
                android:id="@+id/slideMenuItem3"
                android:layout_width="match_parent"
                android:layout_height="?actionBarSize"
                android:layout_marginTop="@dimen/dp10"
                android:gravity="center"
                android:text="slideMenuItem1"
                android:textSize="@dimen/sp18" />

            <TextView
                android:id="@+id/slideMenuItem4"
                android:layout_width="match_parent"
                android:layout_height="?actionBarSize"
                android:layout_marginTop="@dimen/dp10"
                android:gravity="center"
                android:text="slideMenuItem1"
                android:textSize="@dimen/sp18" />


        </LinearLayout>


    </LinearLayout>

</vip.zhuahilong.jdapplication.widget.SlideLayout>

运行效果如下:
在这里插入图片描述

三、处理单机事件关闭侧滑菜单

上面已经完成了滑动打开关闭侧滑菜单,下面我们来实现单击事件来关闭侧滑菜单,原理比较简单。

首先上面已经在触发ACTION_DOWN和ACTION_MOVE时更改来对应的标志位状态,所以在事件ACTION_CANCELACTION_UP中我们就根据(EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK是否位真来判断当前的事件是否是单击事件了,若为单击事件,判断当前的菜单是否为打开状态,若为打开状态呢则判断当前事件的落点是否在菜单范围外,在外则关闭,否则不处理,再者我们还要确定触发关闭菜单的单击区域,也就是我们缩放后可见的mContentView区域,所以我们创建mCloseMenuClickRectF,根据缩放比例来确定其范围,它的left为菜单的宽度加上mContentView宽度缩放差值的一半即mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2,它的topmContentView在高度上缩放差值的一半:(1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2,它的rightmContentView原始的宽度也就是屏幕的rightmContentView.getMeasuredWidth(),它的bottommContentView的原始高度减去缩放差值的一半即:(0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight(),在onLayout中初始化,上述具体代码如下:

 @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
      for (int i = 0; i < 2; i++) {
          View childView = getChildAt(i);
          if (childView.getId() == mSlideMenuId) {
              mSlideMenuView = childView;
              mSlideMenuWidth = childView.getMeasuredWidth();
              childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
          } else {
              childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
              mContentView = childView;
          }
          mCloseMenuClickRectF.set(
                  mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2,
                  (1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2,
                  mContentView.getMeasuredWidth(),
                  (0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight()
          );
      }
  }
	
     @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case 0:
                updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                lastX = event.getX();
                break;
            case 2:
                if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                    updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                }
                float deltaX = lastX - event.getX();
                if (getScrollX() + deltaX <= -mSlideMenuWidth) {
                    scrollTo(-mSlideMenuWidth, 0);
                } else if (getScrollX() + deltaX >= 0) {
                    scrollTo(0, 0);
                } else {
                    scrollBy((int) deltaX, 0);
                }
                float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
                float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
                mSlideMenuView.setScaleX(currentSlideMenuViewScale);
                mSlideMenuView.setScaleY(currentSlideMenuViewScale);
                mContentView.setScaleX(currentContentViewScale);
                mContentView.setScaleY(currentContentViewScale);
                lastX = event.getX();
                break;
            case 1:
            case 3:
                if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                    if (slideMenuIsOpen() && mCloseMenuClickRectF.contains(event.getX(), event.getY())) {
                        updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                        mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                        invalidate();
                    }
                } else {
                    if (getScrollX() < -mSlideMenuWidth / 2) {
                        updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
                        mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
                    } else {
                        updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                        mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                    }
                    invalidate();
                }
                break;
        }
        return true;
    }
    /**
     * 判断菜单是否打开
     *
     * @return
     */
    private boolean slideMenuIsOpen() {
        return (mSlideLayoutFlag & MENU_STATE_MASK) == MENU_STATE_OPEN;
    }

运行效果如下:
在这里插入图片描述

至此我们的仿qq5.0的侧滑菜单效果就完成了,但是也仅仅是完成了部分的效果,其中部分还是要根据实际的功能需求来处理TouchEvent,谢谢大家,如果不对的地方,还请不吝赐教!

四、代码

  1. SlideLayout自定义属性

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <declare-styleable name="SlideLayout">
            <!--侧滑菜单模块的ViewId-->
            <attr name="slideMenuId" format="reference" />
            <!--侧滑菜单模块的缩放值-->
            <attr name="slideMenuTargetScale" format="float" />
            <!--主页View模块的缩放值-->
            <attr name="contentViewTargetScale" format="float" />
        </declare-styleable>
    
    </resources>
    
  2. SlideLayout代码

    public class SlideLayout extends ViewGroup {
    
        /**
         * TouchEvent标志位
         */
        private int EVENT_MASK = 0x1;
    
        /**
         * TouchClick事件
         */
        private int EVENT_CLICK = 0x0;
    
        /**
         * TouchMove事件
         */
        private int EVENT_MOVE = 0x1;
    
        /**
         * 侧滑菜状态标志位
         */
        private int MENU_STATE_MASK = 0x02;
    
        /**
         * 侧滑菜单 打开状态
         */
        private int MENU_STATE_OPEN = 0x02;
    
        /**
         * 侧滑菜单 关闭状态
         */
        private int MENU_STATE_CLOSE = 0x00;
    
        /**
         * 侧滑菜单状态存储器
         */
        private int mSlideLayoutFlag;
    
        /**
         * 侧滑菜单Id
         */
        private int mSlideMenuId;
    
        /**
         * 侧滑菜单的目标缩放值
         */
        private float mSlideMenuTargetScale;
    
        /**
         * 主功能View的目标缩放值
         */
        private float mContentViewTargetScale;
    
        /**
         * 滑动工具类
         */
        private Scroller mScroller;
    
        /**
         * last touch-event X
         */
        private float lastX;
    
        /**
         * 菜单的宽度
         */
        private int mSlideMenuWidth;
    
        /**
         * 侧滑菜单View引用
         */
        private View mSlideMenuView;
    
        /**
         * 主页View引用
         */
        private View mContentView;
        /**
         * 触犯关闭菜单的单击区域
         */
        private RectF mCloseMenuClickRectF;
    
        public SlideLayout(Context context) {
            this(context, null);
        }
    
        public SlideLayout(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
    
        public SlideLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            //获取自定义属性
            TypedArray styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.SlideLayout);
            mSlideMenuId = styledAttributes.getResourceId(R.styleable.SlideLayout_slideMenuId, -1);
            mSlideMenuTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_slideMenuTargetScale, 0.7f);
            mContentViewTargetScale = styledAttributes.getFloat(R.styleable.SlideLayout_contentViewTargetScale, 0.7f);
            styledAttributes.recycle();
            //判断是否成功获取到侧滑菜单Id
            if (mSlideMenuId == -1) {
                throw new IllegalArgumentException("Can't find SlideMenuId");
            }
            //初始化标志位状态
            mSlideLayoutFlag =  ITEM_MENU_CLOSE | DIS_INTERCEPT_EVENT;
            //初始化滑动工具类
            mScroller = new Scroller(context);
    
            mCloseMenuClickRectF = new RectF();
        }
    
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getActionMasked()) {
                case 0:
                    updateTargetMaskValue(EVENT_MASK, EVENT_CLICK);
                    lastX = event.getX();
                    break;
                case 2:
                    if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                        updateTargetMaskValue(EVENT_MASK, EVENT_MOVE);
                    }
                    float deltaX = lastX - event.getX();
                    if (getScrollX() + deltaX <= -mSlideMenuWidth) {
                        scrollTo(-mSlideMenuWidth, 0);
                    } else if (getScrollX() + deltaX >= 0) {
                        scrollTo(0, 0);
                    } else {
                        scrollBy((int) deltaX, 0);
                    }
                    float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
                    float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
                    mSlideMenuView.setScaleX(currentSlideMenuViewScale);
                    mSlideMenuView.setScaleY(currentSlideMenuViewScale);
                    mContentView.setScaleX(currentContentViewScale);
                    mContentView.setScaleY(currentContentViewScale);
                    lastX = event.getX();
                    break;
                case 1:
                case 3:
                    if ((EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK) {
                        if (slideMenuIsOpen() && mCloseMenuClickRectF.contains(event.getX(), event.getY())) {
                            updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                            mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                            invalidate();
                        }
                    } else {
                        if (getScrollX() < -mSlideMenuWidth / 2) {
                            updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_OPEN);
                            mScroller.startScroll(getScrollX(), getScrollY(), -mSlideMenuWidth - getScrollX(), 0);
                        } else {
                            updateTargetMaskValue(MENU_STATE_MASK, MENU_STATE_CLOSE);
                            mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
                        }
                        invalidate();
                    }
                    break;
            }
            return true;
        }
    
        @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                invalidate();
                float currentSlideMenuViewScale = mSlideMenuTargetScale + Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mSlideMenuTargetScale);
                float currentContentViewScale = 1 - Math.abs(getScrollX() * 1.f) / mSlideMenuWidth * (1 - mContentViewTargetScale);
                mSlideMenuView.setScaleX(currentSlideMenuViewScale);
                mSlideMenuView.setScaleY(currentSlideMenuViewScale);
                mContentView.setScaleX(currentContentViewScale);
                mContentView.setScaleY(currentContentViewScale);
            }
        }
    
        /**
         * 判断菜单是否打开
         *
         * @return
         */
        private boolean slideMenuIsOpen() {
            return (mSlideLayoutFlag & MENU_STATE_MASK) == MENU_STATE_OPEN;
        }
    
        /**
         * 更新flag的位值
         *
         * @param mask  目标标记位
         * @param value 最新值
         */
        private void updateTargetMaskValue(int mask, int value) {
            mSlideLayoutFlag = (mSlideLayoutFlag & ~mask) | (mask & value);
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < 2; i++) {
                View childView = getChildAt(i);
                if (childView.getId() == mSlideMenuId) {
                    mSlideMenuView = childView;
                    mSlideMenuWidth = childView.getMeasuredWidth();
                    childView.layout(-mSlideMenuWidth, 0, 0, childView.getMeasuredHeight());
                } else {
                    childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
                    mContentView = childView;
                }
                mCloseMenuClickRectF.set(
                        mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2,
                        (1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2,
                        mContentView.getMeasuredWidth(),
                        (0.5f + mContentViewTargetScale / 2) * mContentView.getMeasuredHeight()
                );
            }
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (getChildCount() != 2) {
                throw new IllegalArgumentException("The SlideLayout only have two child view");
            }
            measureChildren(widthMeasureSpec, heightMeasureSpec);
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值