对于侧滑删除已经是见惯不惯的了,我也一直有写类似QQ那样的侧滑删除控件的想法,虽然研究一段时间的自定义View,然对自定义ViewGroup实战还是较少,并且侧滑删除还要考虑大量的事件分发机制,比如如何处理子控件与父控件之间的滑动冲突以及一系列的down->move..move.. ->up操作等等。童哥刚好写了这么一个侧滑删除,使用起来不但简单方便,更重要的是更加优雅的实现了解耦,也就是说我们不管是使用ListView还是RecyclerView还是其他ViewGroup中的子View都可以使用此方式实现侧滑,具体使用方法是:只需要在XML布局文件中用这个自定义侧滑删除类去包裹我们要执行侧滑删除的item即可(比如我们要对ListView实现侧滑删除功能,我们只需要在定义ListView的item布局文件时,用我们的自定义类作为item的根布局即可)
如果喜欢原文请移驾童哥的博客【Android】毫无耦合性,一个Item根布局搞定 item侧滑删除菜单,像IOS那样简单的使用侧滑删除
既然已经有轮子了,为啥还要再重复一遍呢?原因很简单,因为上面已经说过了,首先对ViewGroup实战偏少,加之对事件分发机制想了解的深入些,刚好童哥的文章中实现的侧滑删除demo中包含了我的知识薄弱点,所以便有了此篇博客,自己跟着敲一遍确实比只看收获的多。
扯了这么多,由于童哥的博客中介绍的很详细了,那么我就把自己在调试中理解的一些知识以及对事件分发机制大致说一下,因为童哥并没有主要介绍事件分发这块
主要解决的问题如下:
1 侧滑拉出菜单。
2 点击除了这个item的其他位置,菜单关闭。并加上了属性动画,菜单关闭有回弹效果。
3 侧滑过程中,不许父控件上下滑动(事件拦截)。
4 多指同时滑动,屏蔽后触摸的几根手指。
5 不会同时展开两个侧滑菜单。
6 侧滑菜单时 拦截了长按事件。
7 侧滑时,拦截了点击事件
8 通过开关 isLeftSwipe支持左滑右滑
9 判断手指起始落点,如果距离属于滑动了,就屏蔽一切点击事件(和QQ交互一样)
主要是自定义ViewGroup的知识点,通过重写onMeasure()方法来告诉父控件需要多大尺寸,在onLayout()方法中确定子View(即:childView)的位置。我们需要来遍历我们的childView
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
//具体的操作逻辑
}
}
下面我们来看下onLayout()方法是如何确定每一个子view的位置的
先把我们的item布局贴出来,方便理解,注意:引用我们自定义ViewGroup包裹布局时要设置android:clickable=”true”这一属性
<?xml version="1.0" encoding="utf-8"?>
<com.example.swipedelete.view.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
>
<!-- 第一个子view,显示ListView数据内容-->
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/listview_iv"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/listview_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是listview的item内容"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
/>
</LinearLayout>
<!-- 下面是侧滑菜单项 即:第2+个子view-->
<Button
android:id="@+id/btn_zd"
android:layout_width="50dp"
android:layout_height="match_parent"
android:background="#d9dee4"
android:text="置顶"
android:textColor="@android:color/white"/>
<Button
android:id="@+id/btn_delete"
android:layout_width="50dp"
android:layout_height="match_parent"
android:background="#F76E6B"
android:text="删除"
android:textColor="@android:color/white"/>
</com.example.swipedelete.view.SwipeMenuLayout>
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//LogUtils.e(TAG, "onLayout() called with: " + "changed = [" + changed + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]");
int childCount = getChildCount();
int left = 0 + getPaddingLeft();
int right = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
if (i == 0) {//第一个子View是内容 宽度设置为全屏
childView.layout(left, getPaddingTop(), left + mMaxWidth, getPaddingTop() + childView.getMeasuredHeight());
left = left + mMaxWidth;
} else {
if (isLeftSwipe) {
childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
left = left + childView.getMeasuredWidth();
} else {
childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight());
right = right - childView.getMeasuredWidth();
}
}
}
}
}
这里需要注意的是,我们侧滑删除根布局下有三部分(一个用来显示ListView数据内容的,一个是置顶,一个是删除)。所以childCount的值为3,因此,需要判断,当 i = 0时,拿到的是第一个子view,即用来显示数据内容的,所以设置它的宽为全屏,所以left = getPaddingLeft(),如果没有设置左内边距则left = 0;当 i 的值不为0时,分为左侧滑菜单和右侧滑菜单,这里只说左侧滑,i 不为0 接着确定第二个子view的位置,由于这三部分是横向排列的,虽然此时侧滑还处于隐藏状态,但是第二个子view的left肯定是自身的宽 + 第一个子view的宽,从而来确定第二个子view的左边距位置。第三个childView则依次累加。
侧滑时,拦截了点击事件
增加一个变量存储scaleTouchSlop,这个值是系统定义的,超过这个值即判断此次动作是在滑动。我们利用这个值判断是否处于侧滑。
我们知道对于ViewGroup中事件分发包括三部分:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent(),也就是常说的:事件分发、事件拦截、事件处理。
下面我们假设一个场景:
当我们点击中间的view内中的某一点时:
1、假设都是返回默认值的情况下,事件分发的顺序是从上往下:Activity的dispatchTouchEvent()—>ViewGroup的dispatchTouchEvent()—>View的dispatchTouchEvent()
2、假设都是返回默认值的情况下,事件处理的顺序是从下往上:View的onTouchEvent()—>ViewGroup的onTouchEvent()—>Activity的onTouchEvent()
3、dispatchTouchEvent()以及onTouchEvent()有一个共同点就是:当返回值为true时,则消费此次事件,不再传递给任何view
4、Activity的dispatchTouchEvent()不管返回true还是false都是消费掉事件,返回super.xxx的时候才会分发给下一级ViewGroup的dispatchTouchEvent(),ViewGroup的dispatchTouchEvent()返回false时,则事件回溯到父类(Activity)的onTouchEvent()处理。
5、ViewGroup的dispatchTouchEvent()返回super.xxx时,事件传递给自己的onInterceptTouchEvent()处理,如果onInterceptTouchEvent()返回true,表示拦截,然后交给自己的onTouchEvent()处理(onTouchEvent()返回true则消费掉事件,谁也接收不到此事件,返回false或者super.xxx时,则交给父类的onTouchEvent()处理。),返回false或者super.xxx时会将事件交给下一级View的dispatchTouchEvent()处理。
6、View的dispatchTouchEvent()接收到事件之后,返回值为false,则回溯给父类(ViewGroup)的onTouchEvent()处理,返回值为super.xxx时,则交给自己的onTouchEvent()处理。onTouchEvent()返回true则消费掉事件,谁也接收不到此事件,返回false或者super.xxx时,则交给父类的onTouchEvent()处理。
上面简单的介绍了down的情况下Activity、ViewGroup、View的事件分发机制。有篇文章通过图文并茂的介绍了事件分发机制,推荐给大家图解 Android 事件分发机制
由效果图可以发现,我们的侧滑菜单在展开和关闭的时候会有回弹的效果,很炫酷,实现方式是通过属性动画实现的,设置了动画的变化率setInterpolator,展开时设置了:mExpandAnim.setInterpolator(new OvershootInterpolator());关于这几个属性动画变化效果简单说下:
AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
LinearInterpolator 以常量速率改变
OvershootInterpolator 向前甩一定值后再回到原来位置
如果你不想设置带回弹效果,你可以不用设置setInterpolator或者你直接设置mExpandAnim.setInterpolator(new LinearInterpolator()); LinearInterpolator():表示匀速