Android 仿QQ侧滑菜单

前言

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

示例图片


集成方式

  • 注入依赖
    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

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



兼容超强的BaseRecyclerViewAdapterHelper

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



方法及属性介绍

nameformat中文解释
isLeftMenuboolean菜单是否在内容左边(如果在左边,则右滑)
enableParentLongClickboolean允许父类长按(此时内容就会被拦截down事件)
expandRatiofloat菜单能够自动打开的阈值
expandDurationinteger菜单展开动画时间
collapseRatiofloat菜单能够自动关闭的阈值
collapseDurationinteger菜单关闭动画时间
collapseInstantvoid不显示动画,关闭菜单
collapseSmoothvoid平滑关闭菜单
expandSmoothvoid平滑打开菜单



THANKS

借鉴SwipeDelMenuLayout

超强的BaseRecyclerViewAdapterHelper


侧滑的雏形

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

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

测绘布局

onLayout

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


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



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


这里写图片描述

下面我们来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轴的距离

scrollBy,x>0

从上图演示可以看出,我们使用的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);
                    }
                }

ACTION_MOVE


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(点击区域分为内容区域及菜单区域)
                        }
                    }

ACTION_UP


子View事件拦截

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

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


总结

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

  • 2
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值