一步步实现ListView的Item侧滑删除菜单效果,仿QQ的聊天页面侧滑删除。(解决ListView与侧滑删除按钮的滑动冲突)

版权声明:本文是博主原创文章,不得转载本文任何内容。原文地址: https://blog.csdn.net/smile_Running/article/details/81916502

                         【声明】:未经许可,禁止转载!

· 介绍

    今天,我来分享我学习过的一个ListView侧滑的Item菜单效果,所谓举一反三,一同百通。在我学会了这个之后,很容易的就实现ListView的上拉加载和下拉刷新的效果,还有我们最常见的侧滑抽屉效果。百变不离其中,只要我们搞懂其中的一个实现方式,那么其他的便信手拈来。

我要实现的是这样一个效果,QQ的联系人侧滑菜单,而在没学过自定义View之前毫无头绪。既然这样,我先来看一下实现后的效果吧。

完成的效果
完成的效果图

· Item布局

    不多比比,本文教程开始。现在呢,我们得有一个这样的思路。所谓ListView中Item布局,我们一般是这个样子的:一个父容器,里面一个TextView,ImageView等等内容。比如:

<listview.example.x.slidelistview.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:orientation="horizontal">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:padding="2dp">

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/item_image"
            android:layout_width="100dp"
            android:layout_height="match_parent"
            android:padding="4dp" />

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

            <TextView
                android:id="@+id/item_name"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="center_vertical"
                android:paddingLeft="8dp"
                android:textColor="#323232"
                android:textSize="25sp" />

            <TextView
                android:id="@+id/item_message"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:gravity="center_vertical"
                android:paddingLeft="8dp"
                android:textColor="#cfcfcf"
                android:textSize="18sp" />
        </LinearLayout>
    </LinearLayout>

    <LinearLayout
        android:layout_width="200dp"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/darker_gray"
            android:gravity="center"
            android:text="置顶"
            android:textColor="@android:color/white"
            android:textSize="22sp" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@android:color/holo_red_light"
            android:gravity="center"
            android:text="删除"
            android:textColor="@android:color/white"
            android:textSize="22sp" />
    </LinearLayout>
</listview.example.x.slidelistview.SlideLayout>

  以上的ListView的Item布局,也就是本次我们使用的。

  既然,你要学习侧滑的实现效果,别告诉我,你还不会用ListView。当然也是有可能的,那么在这里给一篇ListView的使用姿势文章,不会的请点击这里:ListView使用技巧、优化和用法拓展,掌握ListView

· 自定义存放Item父容器

    首先,我们存放Item的父容器,这个父容器是有文章的。怎么说呢,大家可以看到上面布局代码中的父容器,其实这就是我自定义的一个继承FrameLayout的类,当然也可以是RelativeLayout。但是别用LinerLayout,因为我们要对子视图进行排列位置,用LinerLayout反正适得其反。

有了这个前提,我们的思路就是:通过onLayout()、onMesure()方法对子视图的测量宽高以及布置它的位置。我们的继承自FrameLayout的类SlideLayout,其中存放两个父容器。一个是ContentView,一个是MenuView。因为MenuView是从右侧滑进来的,我们应该将它布置到屏幕以外,通过滑动显示和隐藏。

我特地画了一张草图,以便更好的理解吧,也许只有我才能看懂~哈哈哈哈

menuView的逻辑理解图,从隐藏到显示整个过程
​​​​​​

好好理解一下吧,好记性不过烂笔头,在本子上多涂涂画画,或者开启画板自己画一画,才能更好的理解。

那么,我做这些有什么用呢?当然了,请看效果图:

实现了,但是有明显的bug

我们来看看实现的代码类:

/**
 * @Created by xww.
 * @Creation time 2018/8/21.
 */

public class SlideLayout extends FrameLayout {

    private View mContentView;
    private View mMenuView;

    private int mMenuWidth;
    private int mMenuHeight;
    private int mContentWidth;
    private int mContentHeight;

    private Scroller mScroller;

    private float startX;
    private float startY;

    public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mContentView = getChildAt(0);
        mMenuView = getChildAt(1);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mContentWidth = getMeasuredWidth();
        mContentHeight = getMeasuredHeight();
        mMenuWidth = mMenuView.getMeasuredWidth();
        mMenuHeight = mMenuView.getMeasuredHeight();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        //将menu布局到右侧不可见(屏幕外)
        mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                final float dx = (int) (x - startX);
                final float dy = (int) (startY - y);

                int disX = (int) (getScrollX() - dx);
                if (disX <= 0) {
                    disX = 0;
                } else if (disX >= mMenuWidth) {
                    disX = mMenuWidth;
                }

                scrollTo(disX, getScrollY());

                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_UP:
                if (getScrollX() < mMenuWidth / 2) {
                    closeMenu();
                } else {
                    openMenu();
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    public final void openMenu() {
        mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
        invalidate();
    }

    public final void closeMenu() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
        invalidate();
    }
}

· Bug分析

    以上只是简单的实现了而且,但是明显有很多bug,比如每个都可以滑出来、滑到一半卡主、与ListView滑动冲突了等等,下面我们一点一点来解决它,为了更好的体验。

·一、解决滑动冲突

            大家可能有疑惑,这个滑动冲突是怎么产生的呢?下面,我们来看一下滑动冲突的结果,你就会明白它产生的大致原因了。

滑动冲突了

    分析一下,我们SlideLayout有左右滑动的动作,ListView有上下滑动的动作。当我在左右滑动的时候不松开而进行上下滑动,那么滑动事件便由SlideLayout传递到了ListView的滑动。可想而知,SlideLayout的滑动事件被父视图给夺走了,所以SlideLayout就犯毛病了,停止了它的滑动行为。

    既然是ListView夺走了touch事件,从ListView角度上来说,我(ListView)拦截了SlideLayout的touch事件;从SlideLayout的角度上来说,父(ListView)把我的touch事件给夺走了。

    那么有两种相对的角度,就有两种解决的方法。第一种是继承ListView,设置不拦截;第二种是剥夺ListView对touch的处理权。我就用第二种来实现,既然你可以夺走我的,那么我就可以拿回来。所谓相生相克,一物降一物。

我们思路应该这样,比如手指在左右滑动的距离大于上下滑动的距离,那么判定是SlideLayout的滑动事件,在这种情况下才去剥夺ListView的事件处理器。

看一下我们实现的代码:

在onTouchEvent();的ACTION_MOVE事件中修改代码,添加如下部分

            case MotionEvent.ACTION_MOVE:
                final float dx = (int) (x - startX);
                final float dy = (int) (startY - y);

                int disX = (int) (getScrollX() - dx);
                if (disX <= 0) {
                    disX = 0;
                } else if (disX >= mMenuWidth) {
                    disX = mMenuWidth;
                }

                scrollTo(disX, getScrollY());

                final float moveX = Math.abs(x - downX);
                final float moveY = Math.abs(y - downY);
                if (moveX > moveY && moveX > 10f) {
                    //剥夺ListView对touch事件的处理权
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

                startX = x;
                startY = y;
                break;

通过以上的修改,我们达到了目的,看看效果吧。

解决滑动冲突bug

二、解决Item点击事件的冲突

          我们的Item内可能是有点击事件的,所以呢,又掉入了一个坑。为什么这样说呢?看下面的图你就明白一切了。

我给名字设置了点击事件,发生了什么?

版权所有:_Xu2WeI
在姓名区域无法滑动,其他区域可以滑动

分析一下,产生这种异常的原因。为什么在姓名那里却无法移动呢?因为什么呢?那么,首先看的是onClickListener到底做了什么事情,可以看看郭霖大神的文章Android事件分发机制完全解析,带你从源码的角度彻底理解(上)。那我们就知道了onClickListener其实把touch事件给消费了,这就导致了SlideLayout始终处理不了touch事件,因为被Item中的子元素TextView给消费了。

如果不太懂,你也许可以先看这样的一个简单例子:理解View的事件分发、拦截和消费,处理事件冲突的必备技能

    既然,TextView是它(SlideLayout)的儿子,作为父亲,那肯定要管管啊,不能让儿子乱来吧。那么,下面我们就管一管,怎么管呢?当然是拦截了,父亲把touch事件给拦截下来。但是,总不能所有情况都拦截吧,那儿子不就废了吗?所以呢,我们给定这样一个条件,就是当手指确实在左右滑动,而非单纯的点击时,这时父亲才拦截儿子的touch事件。也许有点难以想象,不急,我们看一下代码就简单多了。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX =  x;
                downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                final float moveX = Math.abs(x - downX);
                if (moveX > 10f) {
                    //对儿子touch事件进行拦截
                    intercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return intercept;
    }

通过这样,我们的儿子就听话多了,那么来看看是这样的一种效果。

解决点击事件与滑动的bug

三、限制只能有一个menu被打开

          这一路,我的bug层出不穷,解决了这个又出了新的bug,真的是一步一个坑。当然,这是幸运的事情,当你爬完了这些坑之后,给你带来的确实另一番天地。

不禁扯了一下,言归正传,我们看看出现的Bug情况吧。

多个Item可以被拉出

我们来分析一下出现这种情况的根本原因,那就是每个Item都是ListView中独立的一个个体,我们的Count数多少也就是Item数的多少,所以我们要对每个Item的侧滑Menu进行监听,我们自定义一个接口用于保存每一个Item的状态,有两种:Open和Close。但我们还需要一个点击的监听,用于判断是否与当前Item一致。

这部分就比较简单了,也好理解,简单的看一下代码吧。

首先,在SlideLayout中定义一个监听接口

    private onSlideChangeListenr onSlideChangeListenr;

    public interface onSlideChangeListenr {
        void onMenuOpen(SlideLayout slideLayout);

        void onMenuClose(SlideLayout slideLayout);

        void onClick(SlideLayout slideLayout);
    }

    public void setOnSlideChangeListenr(SlideLayout.onSlideChangeListenr onSlideChangeListenr) {
        this.onSlideChangeListenr = onSlideChangeListenr;
    }

其次,在三个地方分别设置监听

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = x;
                downY = y;
                if (onSlideChangeListenr != null) {
                    onSlideChangeListenr.onClick(this);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                final float moveX = Math.abs(x - downX);
                if (moveX > 10f) {
                    //对儿子touch事件进行拦截
                    intercept = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return intercept;
    }

    public final void openMenu() {
        mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
        invalidate();
        if (onSlideChangeListenr != null) {
            onSlideChangeListenr.onMenuOpen(this);
        }
    }

    public final void closeMenu() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
        invalidate();
        if (onSlideChangeListenr != null) {
            onSlideChangeListenr.onMenuClose(this);
        }
    }

最后,在我们适配器中设置监听状态的改变做出相应的处理。

        mSlideLayout = (SlideLayout) convertView;
        mSlideLayout.setOnSlideChangeListenr(new SlideLayout.onSlideChangeListenr() {
            @Override
            public void onMenuOpen(SlideLayout slideLayout) {
                mSlideLayout = slideLayout;
            }

            @Override
            public void onMenuClose(SlideLayout slideLayout) {
                if (mSlideLayout != null) {
                    mSlideLayout = null;
                }
            }

            @Override
            public void onClick(SlideLayout slideLayout) {
                if (mSlideLayout != null ) {
                    mSlideLayout.closeMenu();
                }
            }
        });

接下来,我们运行一下项目,成功的解决了问题。

到此,一个仿QQ侧滑菜单的效果完全的实现了,是不是很赞呢?哈哈哈哈。

你以为就结束了吗?开玩笑。。。哈哈哈哈哈哈哈!

博文续篇:

ListView添加下拉刷新、上拉加载,其实很简单

©版权所有:https://blog.csdn.net/smile_Running/article/details/81916502

猜你喜欢

转载自blog.csdn.net/smile_Running/article/details/81916502