天天看点

自定义View-侧滑菜单

自定义View-侧滑菜单

      • 一、搭建基本框架
      • 二、实现侧滑菜单功能
      • 三、处理单机事件关闭侧滑菜单
      • 四、代码
最近在使用酷我音乐软件时看到它的侧滑菜单,突然想起来当年qq5.0时的侧滑菜单,虽然网上有很多实现方式,但是为了纪念我Q还是自己来码一个纪念我的青春,效果如下图,啥也不说来直接开干吧!
自定义View-侧滑菜单

一、搭建基本框架

  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. 到此简单的

    onMeasure

    onLayout

    操作就完了,我们来运行测试看一下效果吧。

    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>
    
               
    运行效果为:
    自定义View-侧滑菜单
    由于我们还未编码侧滑菜单的打开和关闭,所以下现在无法看到侧滑菜单,下面来编码打开关闭侧滑菜单部分。

二、实现侧滑菜单功能

经分析可知,我们的菜单主要是通过滑动来打开,再通过滑动或点击部分区域来关闭,因此我们要明确是否发生来滑动事件且要实时更新当前事件类型和菜单的状态。
  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)尽管上面已经定义的事件类型,为了更简单直接,我们先处理滑动事件,后面再回过头来处理点击事件,想要能够滑动我们只需要知道滑动的距离及方向即可,因此我们要记录上次

    TouchEvent

    X

    坐标,定义变量

    lastX

    来存储,且在

    TouchEvent

    Down

    事件中进行初始化,然后在

    ACTION_MOVE

    事件中计算我们需要滑动距离,

    正负号

    表示滑动的方向,

    负号

    表示内容向右滑动即打开菜单正号表示内容向左滑动即关闭菜单,所以我们计算

    floatdeltaX=lastX-event.getX()

    即是我们需要移动的差值且包含来正确的方向,调用方法

    scrollBy(deltaX,0)

    即完成来此处滑动,但是我们还需要注意滑动距离的范围问题 ,有可能我们的滑动超过来菜单的宽度,导致我们看到一片空白,所以我们还要对滑动的距离作出限制,因为滑动范围是和侧滑菜单的宽度有关系的,所以在

    onlayout

    方法中获取到我们侧滑菜单的宽度用

    mSlideMenuWidth

    存储,则滑动的距离

    mScrollX

    的范围为

    [-mSlideMenuWidth,0]

    ,我们根据

    getScrollX+deltaX

    的值判断是否在这个范围内,若

    getScrollX()+deltaX<=mSlideMenuWidth

    则s

    crollTo(-mSlideMenuWidth,0)

    ,若

    getScrollX()+deltaX>=0

    scrollTo(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;
        }
    
               

运行看一下效果如下图:

自定义View-侧滑菜单

3)从上面来看使用滑动打开关闭菜单的功能基本已经完成,但是若在菜单打开或者关闭一半时,手机离开屏幕,则菜单保持当前状态不变了,体验极差,所以我们还要处理事件

ACTION_UP和ACTION_CANCEL

来完成接下来未完成的动作,我们根据滑动的距离

mScrollX

getScrollX(

)和菜单宽度的一半来判断当前的目标状态,若

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();
        }
    }
           

运行看一下效果,见下图:

自定义View-侧滑菜单

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:

mContentView

onLayout

中获取到两个模块的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;
    }
           

运行效果如下:

自定义View-侧滑菜单

这样就基本完成来我们仿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>
           

运行效果如下:

自定义View-侧滑菜单

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

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

首先上面已经在触发

ACTION_DOWN和ACTION_MOVE

时更改来对应的标志位状态,所以在事件

ACTION_CANCEL

ACTION_UP

中我们就根据

(EVENT_MASK & mSlideLayoutFlag) == EVENT_CLICK

是否位真来判断当前的事件是否是单击事件了,若为单击事件,判断当前的菜单是否为打开状态,若为打开状态呢则判断当前事件的落点是否在菜单范围外,在外则关闭,否则不处理,再者我们还要确定触发关闭菜单的单击区域,也就是我们缩放后可见的

mContentView

区域,所以我们创建

mCloseMenuClickRectF

,根据缩放比例来确定其范围,它的

left

为菜单的宽度加上

mContentView

宽度缩放差值的一半即

mSlideMenuWidth + (1 - mContentViewTargetScale) * mContentView.getMeasuredWidth() / 2

,它的

top

mContentView

在高度上缩放差值的一半:

(1 - mContentViewTargetScale) * mContentView.getMeasuredHeight() / 2

,它的

right

mContentView

原始的宽度也就是屏幕的

right

mContentView.getMeasuredWidth()

,它的

bottom

mContentView

的原始高度减去缩放差值的一半即:

(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;
    }

           

运行效果如下:

自定义View-侧滑菜单
至此我们的仿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);
        }
    }