Android 仿QQ侧滑菜单

    • 前言
    • 集成方式
      • 兼容超强的BaseRecyclerViewAdapterHelper
      • 方法及属性介绍
    • THANKS
    • 侧滑的雏形
      • 测绘布局
        • onLayout
        • onMeasure
      • MotionEvent事件处理View事件知识
        • ACTION_DOWN
        • ACTION_MOVE
        • ACTION_UP
      • 子View事件拦截
    • 总结

前言

继上一篇 Android View的事件分发机制和滑动冲突解决的理论知识铺垫,我们也来撸起袖子仿QQ侧滑造个轮子。
欢迎到Github star

Android 仿QQ侧滑菜单_第1张图片


集成方式

  • 注入依赖
    Step 1. Add the JitPack repository to your build file
    Step 2. Add the dependency
    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
    dependencies {
       compile 'com.github.qdxxxx:SwipeMenuContainer:v1.0.3'
    }


Layout

    "match_parent"
        android:layout_height="70dp"
        qdx:isLeftMenu="false"     //false(右侧是菜单),true(左侧是菜单)    
       ...
        />



兼容超强的BaseRecyclerViewAdapterHelper

演示apk下载地址 : https://fir.im/6q2m
这里写图片描述



方法及属性介绍

name format 中文解释
isLeftMenu boolean 菜单是否在内容左边(如果在左边,则右滑)
enableParentLongClick boolean 允许父类长按(此时内容就会被拦截down事件)
expandRatio float 菜单能够自动打开的阈值
expandDuration integer 菜单展开动画时间
collapseRatio float 菜单能够自动关闭的阈值
collapseDuration integer 菜单关闭动画时间
collapseInstant void 不显示动画,关闭菜单
collapseSmooth void 平滑关闭菜单
expandSmooth void 平滑打开菜单



THANKS

借鉴SwipeDelMenuLayout

超强的BaseRecyclerViewAdapterHelper


侧滑的雏形

首先我们来分析一下侧滑菜单的整个流程

  1. 测绘布局(onMeasure,onLayout),确定菜单布局的摆放位置
  2. 事件分发之ACTION_DOWN : 设定菜单开合状态
  3. 事件分发之ACTION_MOVE : 判断是否拦截父View/菜单View的事件,菜单展开/闭合越界处理
  4. 事件分发之ACTION_UP : 根据滑动展开/闭合阈值以及滑动瞬间速度设定菜单开合状态
  5. 事件拦截之onInterceptTouchEvent : 拦截子View的事件

测绘布局

onLayout

首先onLayout有两个操作思路,第一是代码家的SwipeLayout ,如下图所示


Android 仿QQ侧滑菜单_第2张图片
通过xml布局tag,然后代码判断菜单layout居content(内容)的上下左右,再进行操作。此构造的有点是能够同时上/下/左/右滑出菜单布局,缺点就是需要比较大量的逻辑判断,包括布局测绘和手势滑动。另外ps : 代码家的控件虽然做的早又好,但是issues量多,而且有些特殊机型会存在闪退问题…所以选择第三方控件需谨慎。



我们采用SwipeDelMenuLayout的设计方式
通过布局中isLeftMenu来设置当前菜单布局的摆放位置(只能为左或者是右),然后菜单的每一项便会以LinearLayouthorizontal方式排列。此方法的有点是上手快,逻辑判断少,缺点就是只能允许一侧为菜单布局。


Android 仿QQ侧滑菜单_第3张图片

下面我们来layout菜单布局,如上图所示,如果是左侧为菜单,我们只需依次layout出“置顶”,“点赞”,“收藏”的位置。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = l;
        int right = r;
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE)
                continue;
            if (i == 0) {
                childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
            } else {
                if (isLeftMenu) {//菜单在左边
                    childView.layout(left - childView.getMeasuredWidth(), getPaddingTop(), left, getPaddingTop() + childView.getMeasuredHeight());
                    left = left - childView.getMeasuredWidth();
                } else {//菜单在右边
                    childView.layout(right, getPaddingTop(), right + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
                    right = right + childView.getMeasuredWidth();
                }
            }
        }
    }

onMeasure

我们只需Measure布局上所有的子View即可

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            childView.setClickable(true);//设置子View可以点击
            if (childView.getVisibility() == GONE)
                continue;

            measureChild(childView, widthMeasureSpec, heightMeasureSpec);//measure所有的子View
        }
    }


MotionEvent事件处理(View事件知识)

ACTION_DOWN

只负责记录手指触摸的坐标,并且将所有的开关标记(是否打开菜单,是否为单击等)重置。

                mPointGapF.x = ev.getX();////用来设置滑动的x轴方向距离,即展开/关闭menu

                mPointDownF.x = ev.getX();//记录手指第一次点击down的x轴位置
                mPointDownF.y = ev.getY();//记录手指第一次点击down的y轴位置

ACTION_MOVE

move事件也只用来滑动展开/关闭菜单布局,另外还需处理父类的拦截事件。

处理父类拦截事件
mScaleTouchSlop是触发移动事件的最小距离,通过ViewConfiguration.get(context).getScaledTouchSlop()获取。
这里的拦截分两种情况

  1. 刚开始滑动,由于滑动距离小,此时我们需判断用户手指滑动的角度,如果水平方向滑动角度小于30°,则我们可以认为是水平左右滑动。
  2. 如果已经开始滑动,且布局滑动的距离大于mScaleTouchSlop,那么证明已经通过了上述条件,则不允许父类拦截事件。
                float gapX = mPointDownF.x - ev.getX();
                float gapY = mPointDownF.y - ev.getY();

                if (Math.abs(gapX) < mScaleTouchSlop && Math.abs(gapX) > Math.abs(gapY) * 2f) {
                    isInterceptParent = true;
                } else if (Math.abs(gapX) > mScaleTouchSlop || Math.abs(getScrollX()) > mScaleTouchSlop) {
                    isInterceptParent = true;
                }

                if (!isInterceptParent) {
                    break;
                }
                getParent().requestDisallowInterceptTouchEvent(true);



处理完了事件拦截,我们就可以随心所欲的展开/关闭菜单布局。

首先介绍一下ScrollTo和ScrollBy :
scrollTo():表示的是移动到哪个坐标点,坐标点的位置就会移动到屏幕原点的位置
scrollBy():表示的是移动的增量dx和dy
getScrollX() : 表示scroll移动x轴的距离

Android 仿QQ侧滑菜单_第4张图片

从上图演示可以看出,我们使用的scrollTo()或者是scrollBy()都是基于手机屏幕幕布移动的,也就是说如果菜单布局在内容的右侧,想要滑出“删除”布局,那么scrollBy x轴的值 dx>0。所以我们scroll的对象并不是我们的真正布局对象!而是手机用来展示这个布局的“幕布”。

下面代码我们来展开菜单布局,并且处理滑动越界情况

                scrollBy((int) (mPointGapF.x - ev.getX()), 0);//滑动布局
                mPointGapF.x = ev.getX();
                if (isLeftMenu) {//如果左边是菜单,允许向右滑动
                    if (-getScrollX() < 0) {//菜单布局向右滑动getScrollX()是<0的
                        scrollTo(0, 0);
                    }
                    if (getScrollX() <= -mWidthofMenu) {//mWidthofMenu菜单布局的总宽度
                        scrollTo(-mWidthofMenu, 0);//处理越界情况
                    }

                } else {//右边是菜单,向左滑动
                    if (getScrollX() < 0) {//向左滑动getScrollX()是>0的
                        scrollTo(0, 0);
                    }
                    if (getScrollX() >= mWidthofMenu) {
                        scrollTo(mWidthofMenu, 0);
                    }
                }

Android 仿QQ侧滑菜单_第5张图片


ACTION_UP

为了完善侧滑效果,我们在ACTION_UP事件处理展开/闭合动画效果,我们依据两个点进行判断

  1. 手指UP时候的瞬间速度
  2. 已经展开/闭合菜单的阈值,也就是说如果菜单的是闭合的时候,如果滑动展开了总菜单宽度宽度的30%(可以改变这个值),就显示展开动画。

我们引入VelocityTracker来计算x轴方向用户手指的滑动即时速度

        VelocityTracker mVelocityTracker = VelocityTracker.obtain();
        mVelocityTracker.addMovement(ev);
        mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
        final float velocityX = mVelocityTracker.getXVelocity(mPointerId);//如果有多指触碰,计算第一根手指触碰的速度

我们先分析右侧是菜单(手指向左滑动展开菜单)的情况,mExpandLimit为展开的阈值,mCollapseLimit 为闭合菜单的阈值。(都为菜单宽度的30%)

                    if (!isExpand) { //如果还没展开
                        if (getScrollX() > mExpandLimit || (velocityX < -1000 && isInterceptParent)) {
                            expandSmooth();
                        } else {
                            collapseSmooth();
                        }
                    } else {//已经展开的
                        if (getScrollX() < mCollapseLimit || (velocityX > 1000 && isInterceptParent)) {
                            collapseSmooth();
                        } else if (!isClickEvent) {//如果是滑动的
                            expandSmooth();
                        } else if (ev.getX() < getWidth() - getScrollX()) {
                            collapseSmooth();//点击内容部分区域View(点击区域分为内容区域及菜单区域)
                        }
                    }

Android 仿QQ侧滑菜单_第6张图片


子View事件拦截

最后我们只需要处理是否拦截子view(包括内容布局,菜单布局),须重写onInterceptTouchEvent方法,拦截的判定条件如下

  1. 如果已经有Menu菜单打开,拦截。
  2. 如果Menu菜单打开,点击到内部布局(如上图的“我是内容1”布局),拦截。如果点击到的是菜单布局,则不拦截,点击事件交给子View自行处理。
  3. 另外搭配BaseAdapter的拖拽排序时,子View的事件一律拦截,包括触碰内容布局


总结

所谓工欲善其事,必先利其器,总体分析下来仿QQ侧滑菜单的难度并不是很大,关键在于是否掌握了 Android View的事件分发机制和滑动冲突解决。
学习成长的道路亦如此,在前行的道路上披荆斩棘的同时,也应该让“斧子”更加锋利的助我们前行。
欢迎到Github star

你可能感兴趣的:(《自定义view系列》)