SwipeDeleteMenu:侧滑删除

记得在之前项目中,有一个需求是文章列表侧滑显示删除按钮。
当时为了实现这个功能,又加上时间紧急,无奈引入了第三方库。
代价大不说,自身也感觉这样做项目,技术不会有大的进步。
于是决定自撸一个侧滑删除控件 — SwipeDeleteMenu

先上效果图:
这里写图片描述

如何实现?

乍一看是不是感觉比较复杂比较难?
其实当我们一点一点来拆析的时候,就会发现,也不过如此。
在这里,我分为两大步:

  • 布局操作
  • 逻辑实现

第一步,布局操作,直接上代码:

public class SwipeDeleteMenu extends ViewGroup {

    private Context mContext;

    // 右侧菜单测量宽度
    private int mRightMenuWidthMeasured;
    // 左侧内容区测量宽度
    private int mContentWidthMeasured;
    // 测量高度
    private int mHeightMeasured;


    public SwipeDeleteMenu(Context context) {
        this(context, null, 0);
    }

    public SwipeDeleteMenu(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        this.mContext = context;
    }

    public SwipeDeleteMenu(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        mRightMenuWidthMeasured = 0;
        mContentWidthMeasured = 0;
        mHeightMeasured = 0;

        for (int i = 0; i < this.getChildCount(); i++) {
            View childView = this.getChildAt(i);
            if (childView.getVisibility() != GONE) {
                childView.setClickable(true);
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);

                mHeightMeasured = this.getMeasuredHeight();

                // 内容区
                if (i == 0) {
                    mContentWidthMeasured = childView.getMeasuredWidth();
                } else {
                    // 右侧菜单
                    mRightMenuWidthMeasured += childView.getMeasuredWidth();
                }

            }
        }

        // 测量完成进行保存
        setMeasuredDimension(
                mContentWidthMeasured + getPaddingLeft() + getPaddingRight(),
                mHeightMeasured + getPaddingTop() + getPaddingBottom());

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int rightMenuLeft = 0;

        for (int i = 0; i < this.getChildCount(); i++) {
            View childView = this.getChildAt(i);
            if (childView.getVisibility() != GONE) {
                if (i == 0) {
                    childView.layout(getPaddingLeft(), getPaddingTop(),
                            getPaddingLeft() + childView.getMeasuredWidth(),
                            getPaddingTop() + childView.getMeasuredHeight());
                    rightMenuLeft = getPaddingLeft() + childView.getMeasuredWidth();
                } else {
                    childView.layout(rightMenuLeft, getPaddingTop(),
                            rightMenuLeft + childView.getMeasuredWidth(),
                            getPaddingTop() + childView.getMeasuredHeight());
                    rightMenuLeft += childView.getMeasuredWidth();
                }
            }
        }

    }

}

首先要继承 ViewGroup,并重写构造方法、onMeasure、onLayout。

  • onMeasure
    确定内容区宽度、右侧菜单、整体高度
  • onLayout
    确定内容布局以及右侧菜单的位置显示

注意:默认内容区宽度充满屏幕。

第二步,逻辑实现,相较第一步会比较难,也不是难,只是相对复杂些。
在第一步中,我们只需完成测量布局工作,很简单。
第二步,我们会牵扯到动画、拦截事件等。接下来看具体实现:

  • 手指向左滑动,显示菜单

    向左滑动,那滑动多少才显示?
    那么我们需要顶一个变量,来表示滑动界限 mLimitDistance。

    这个值在 onMeasure 方法中进行赋值。

mLimitDistance = mRightMenuWidth * 5 / 10;

在这里,我给定的是如果滑动距离超过 mRightMenuWidth 的一半儿,
就会显示右侧菜单,反之,隐藏。

那滑动距离又是怎么计算的?

我们重写 dispatchTouchEvent() 方法:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mFirstX = ev.getRawX();
                mLastX = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                float swipeDistance = mLastX - ev.getRawX();
                scrollBy((int) swipeDistance, 0);
                mLastX = ev.getRawX();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (Math.abs(getScrollX()) > mLimitDistance){
                    // TODO showMenu();
                } else {
                    // TODO closeMenu();
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

我们手指在不断左滑的时候,布局内容也会不断的向左移动。
很明显,这里是一个相对运动的过程。
所以我们在通过计算得到 swipeDistance 之后,调用 scrollBy();

接下来解释滑动距离:
通过代码可以看到,滑动距离是通过 getScrollX() 计算得到,
那么可能有朋友会问,
getScrollX() 表示什么?为什么还要用 Math.abs(getScrollX())?

getScrollX() 得到的 mScrollX,表示的是:
View 左边缘与 View 内容左边缘在水平方向的距离。
这么说可能太晦涩,我用几张图表示一下,你们就会明白了:
这里写图片描述
将黑色框向左移动,如下图所示:
这里写图片描述
将黑色框再向右移动,如下图所示:
这里写图片描述

看完图片,应该很好理解了吧?

那么现在判断滑动距离了,如何展示出来呢?
在这里我们用的是属性动画 ValueAnimator。

mShowAnim = ValueAnimator.ofInt(getScrollX(), mRightMenuWidth);
mShowAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // 只需要执行一次,是一个绝对运动,所以调用 scrollTo();
        scrollTo((Integer) animation.getAnimatedValue(), 0);
    }
});
mShowAnim.setInterpolator(new OvershootInterpolator()); 
mShowAnim.setDuration(300).start();

ok,运行demo,看下效果:
这里写图片描述

那么,到目前为止,我们实现了左滑删除按钮的显示。但也有一些问题,
当手指不断左滑或者右滑的时候哦, view 也会不断的向左向右拉伸,
这样看起来,交互效果会很不舒服。那么如何来优化呢?

既然是在滑动的时候,产生的问题,那我们就在滑动的时候进行优化。
”哪里不会点哪里“
具体代码实现:

case MotionEvent.ACTION_MOVE:

                float swipeDistance = mLastX - ev.getRawX();       

                scrollBy((int) swipeDistance, 0);

                // 左右越界修正
                if (getScrollX() < 0) {
                    scrollTo(0, 0);
                }
                if (getScrollX() > mRightMenuWidth) {
                    scrollTo(mRightMenuWidth, 0);
                }

                mLastX = ev.getRawX();

                break;

再次运行,查看效果:
这里写图片描述

这次比上次已经流畅很多,而且用户也提升了不少。
但也有新的问题,当删除菜单展示,点击左侧内容区时,删除菜单并未收起隐藏。
那这个功能又如何实现?

首先定义一个变量 isContentDown,来表示手指按下的是否是内容区,默认为 true
我们知道当手指在屏幕进行操作的时候,如何来判断手指是点击内容区还是滑动呢?

这里我们需要定义一个变量,这个变量表示最小滑动距离,
不同的设备可能会有所不同。只有滑动距离超过了这个值,
系统才会认为你在做滑动操作。可以用四个字简单概括:滑动过滤

具体代码:

// 获取方法固定
mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();

dispatchTouchEvent() 方法中:

switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                 isContentDown = true;
                 break;
            case MotionEvent.ACTION_MOVE:
                float swipeDistance = mLastX - ev.getRawX();
                // 判断如果滑动距离大于了 mScaledTouchSlop,则认为用户是在滑动
                if (Math.abs(swipeDistance) > mScaledTouchSlop) {
                    isContentDown = false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                break;
            default:
                break;
        }

这里我们重写 onInterceptTouchEvent() :

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                if (getScrollX() > mScaledTouchSlop) {
                    // 判断点击的是右侧删除按钮还是左侧内容区
                    if (ev.getX() < (getWidth() - getScrollX())) {
                        if (isContentDown) {
                            showMenu(false);
                        }
                        // 事件不再继续向下传递
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

代码很简单也很清晰,不再做其他解释。现在运行,再看效果图:
这里写图片描述

现在大体功能已经实现的差不多了,但是还有一点欠缺。我们在刚开始的时候,定义了变量
mLimitDistance,但现在滑动距离即使大于 mLimitDistance,
右侧菜单也会隐藏,原因在哪儿?
原因在于我们之前添加了这一行代码:

if (Math.abs(swipeDistance) > mScaledTouchSlop) {
        isContentDown = false;
   }

当手指缓慢左右滑动的时候,isContentDown 始终是false。
所以在 onInterceptTouchEvent() 中会执行 showMenu(false)。

那么如何解决呢?解决办法:
在 onInterceptTouchEvent() 中,move 的时候添加:

if (Math.abs(ev.getRawX() - mFirstX) > mScaledTouchSlop){
         return true;
   }

这行代码表示,刚才手指在移动,事件不需要向下面传递。所以 up 中的代码也不会执行。

在实际应用中,我们可能会快速左滑或右滑,作为用户来说,
一般希望右侧菜单也会展示或隐藏,那怎么做呢?

很简单,通过得到手指快速滑动时某一点的速度,然后判断速度是否满足条件,
来进行右侧菜单的展示或者隐藏。
关键代码如下:

mTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                float velocityX = mTracker.getXVelocity(mPointerId);
                if (Math.abs(velocityX) > 1000) {
                    if (velocityX < -1000) {
                        showMenu(true);
                    } else {
                        showMenu(false);
                    }
                } else {
                    if (Math.abs(getScrollX()) > mLimitDistance) {
                        showMenu(true);
                    } else {
                        showMenu(false);
                    }
                }

                releaseTracker();
/**
 * 及时释放
 */
private void releaseTracker() {
    if (mTracker != null) {
        mTracker.clear();
        mTracker.recycle();
        mTracker = null;
    }
}

现在运行,看效果图:
这里写图片描述

又有一个新的问题产生了,想要的效果是一个列表中,
不允许多个删除按钮同时显示,那怎么做?
也很简单,要想解决问题,就要对症下药。
我们在删除按钮显示的时候,就把当前的 SwipeDeleteMenu 存储起来,
隐藏时,就置为空。
然后在down的时候,判断存储的对象是否为空,
如果不为空,就把它的删除按钮隐藏并置空。

关键代码:

if (sMenu != null) {
    if (sMenu != this) {
        sMenu.showMenu(false);
    }
}

好了,关于侧滑删除的所有逻辑都已经编写完了,是不是感觉很简单?
其实就是很简单,只要一点点的拆析出来,就会很好做了。

最后附上源码地址:
SwipeDeleteMenu

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值