在项目中,如果要用到滑动控件嵌套滑动控件,总会让人很心塞。因为很可能会出现冲突的问题。这里举个例子,利用事件分发机制,处理侧滑菜单控件和列表中的侧滑删除控件间的冲突。
分析
提到侧滑删除,一个经典的例子就是QQ了。QQ的首页是一个大的侧滑菜单控件,嵌套一个列表,列表里面再嵌套侧滑删除的控件。我们就仿照这个样式,看看能不能做一个和它类似的效果。
这里关注的重点是在滑动手势的处理上,简单分析一下需要做什么处理:
(下面把侧滑菜单控件称作菜单控件,列表侧滑删除控件称作删除控件。)
在首页上下滑动时,滚动列表。
菜单控件关闭的情况下,如果列表里面没有展开的删除项,则手指向右滑动是滑动菜单控件,向左滑动是滑动删除控件。
如果列表里面有展开的删除控件,则菜单控件和列表项都不可滑动。除了删除按键,点击其他区域,都是将展开项关闭。
当手指滑动删除控件时,手指滑动到屏幕的任意区域都可以滑动展开项。
菜单控件打开的情况下,点击右边主页区域,将菜单控件关闭。
有点复杂的感觉啊,我们一个个来解决。
我自定义了上面说到的三个控件,根据嵌套关系,从大到小分别是:
- 菜单控件SwipeMenuLayout
- 列表控件MyRecyclerView
- 删除控件SwipeDeleteLayout
其中,SwipeMenuLayout和SwipeDeleteLayout都是继承自FrameLayout,用ViewDragHelper实现滑动效果。MyRecyclerView则继承自RecyclerView。
我们知道事件分发和三个方法有关:
- 负责分发的dispatchTouchEvent
- 负责拦截的onInterceptTouchEvent
- 负责消费的onTouchEvent
简单概括一下这个机制就是:分发从父到子,消费从子到父。
一般我们不对分发做特殊处理,下面按执行顺序看看三个控件的onInterceptTouchEvent和onTouchEvent方法是怎么写的。
onInterceptTouchEvent
onInterceptTouchEvent方法的返回值决定是否拦截事件。
菜单控件
这部分要稍微啰嗦一点。我们先看看菜单关闭的情况,这时如果手指向右滑且没有展开的删除控件,我们就可以把事件拦截了,所以onInterceptTouchEvent可以写成这样:
if (mState == State.CLOSE) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mDownX = ev.getRawX();
mDownY = ev.getRawY();
}
break;
case MotionEvent.ACTION_MOVE: {
float deltaX = ev.getRawX() - mDownX;
float deltaY = ev.getRawY() - mDownY;
//向右滑动且列表没有展开项且横向滑动距离比竖向滑动距离大,则拦截
if (deltaX > 0 &&
MainAdapter.mOpenItems.size() == 0 &&
Math.abs(deltaY / deltaX) < 1) {
return true;
}
}
break;
}
}
mState代表当前侧滑控件的状态,MainAdapter.mOpenItems保存的是当前打开的删除控件。我使用Math.abs(deltaY / deltaX)是否小于1来判断手指的滑动方向。
这里还有两种不拦截的情况,向左滑动或者有展开项的话,都是和侧滑菜单没关系的,滑动事件里面再加入以下代码:
//如果是向左滑,且竖直滑动距离大于横向滑动距离,不拦截
//MainPage打开的item个数大于0,不拦截
if ((deltaX < 0 && Math.abs(deltaY / deltaX) > 1) ||
MainAdapter.mOpenItems.size() > 0) {
return false;
}
接下来是菜单打开的情况。这时候当手指点击了右侧的主页面区域是需要拦截并且将菜单关闭。如果手指向右滑动则不需要拦截:
if (mState == State.OPEN) {
//完全展开时并且点到主页面,拦截并关闭菜单
if (mMainContent.getLeft() <= mRange && ev.getRawX() > mRange) {
return true;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是向右滑,不拦截
float deltaX = ev.getRawX() - mDownX;
if (deltaX > 0) {
return false;
}
break;
}
}
mRange是侧滑出来的菜单宽度,关闭菜单的操作可以放在ViewDragHelper的Callback房里里处理。
除了上面这些情况,默认情况下是否拦截交给ViewDragHelper处理就好了,调用它的shouldInterceptTouchEvent方法。
完整代码如下:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mState == State.CLOSE) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mDownX = ev.getRawX();
mDownY = ev.getRawY();
}
break;
case MotionEvent.ACTION_MOVE: {
float deltaX = ev.getRawX() - mDownX;
float deltaY = ev.getRawY() - mDownY;
//向右滑动且列表没有展开项且横向滑动距离比竖向滑动距离大,则拦截
if (deltaX > 0 &&
MainAdapter.mOpenItems.size() == 0 &&
Math.abs(deltaY / deltaX) < 1) {
return true;
}
//如果是向左滑,且竖直滑动距离大于横向滑动距离,不拦截
//MainPage打开的item个数大于0,不拦截
if ((deltaX < 0 && Math.abs(deltaY / deltaX) > 1) ||
MainAdapter.mOpenItems.size() > 0) {
return false;
}
}
break;
}
} else if (mState == State.OPEN) {
//完全展开时并且点到主页面,拦截并关闭菜单
if (mMainContent.getLeft() <= mRange && ev.getRawX() > mRange) {
return true;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = ev.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是向右滑,不拦截
float deltaX = ev.getRawX() - mDownX;
if (deltaX > 0) {
return false;
}
break;
}
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
列表控件
列表里面其实只做了一个处理,就是判断上下滑动的时候就把事件拦截了:
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = e.getRawX();
mDownY = e.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//竖向滑动时拦截事件
float deltaX = e.getRawX() - mDownX;
float deltaY = e.getRawY() - mDownY;
if (deltaY != 0.0 &&
Math.abs(deltaX / deltaY) < 1) {
return true;
}
break;
}
return super.onInterceptTouchEvent(e);
}
删除控件
这里什么都不用做,交给ViewDragHelper就好了:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mDragHelper.shouldInterceptTouchEvent(ev);
}
onTouchEvent
onTouchEvent方法的返回值决定是否消费事件。
删除控件
删除控件的onTouchEvent又有几个地方要做特殊处理的。当有展开的删除项时,点击别的删除项时就将展开的关闭。这样就可以了:
//存在已展开的控件且当前控件为关闭状态,则将所有展开控件关闭
if (MainAdapter.mOpenItems.size() > 0 && mState == State.CLOSE) {
return false;
}
这里我没有消费事件,也没有进行关闭的操作,因为我把关闭的操作交给父控件去处理了,否则会有卡顿的现象(QQ就有这个问题)。
如果点击的是展开的删除项左边区域,这个又比较特殊了。因为手指按下之后,有可能是滑动,也可能是点击。滑动的话是滑动删除项,点击则是将删除项关闭。所以我们要判断一下用户是否有滑动的操作:
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
float deltaX = event.getRawX() - mDownX;
if (Math.abs(deltaX) > 50) {
isDrag = true;
}
break;
case MotionEvent.ACTION_UP:
if (!isDrag &&
event.getRawX() <= mWidth - mBackWidth) {
close();
return true;
}
isDrag = false;
break;
}
当活动距离大于50时,我就把它当做是一个滑动操作,这时候把滑动交给ViewDragHelper处理,否则就将当前控件关闭。
最后还有一个,当我滑动删除控件时,如果手指滑到了别的地方,滑动的依然是当前这个删除控件。换一个说法,其实就是一旦滑动了,父控件就不能再拦截我的滑动事件了。其实ViewGroup里面有一个requestDisallowInterceptTouchEvent方法,传true的时候,相当于通知它的所有父控件不要再拦截了。所以可以这样来处理:
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_CANCEL:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
break;
}
完整代码如下:
public boolean onTouchEvent(MotionEvent event) {
//存在已展开的控件且当前控件为关闭状态,则将所有展开控件关闭
if (MainAdapter.mOpenItems.size() > 0 && mState == State.CLOSE) {
MainAdapter.closeAll();
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
requestDisallowInterceptTouchEvent(true);
float deltaX = event.getRawX() - mDownX;
if (Math.abs(deltaX) > 50) {
isDrag = true;
}
break;
case MotionEvent.ACTION_CANCEL:
requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_UP:
requestDisallowInterceptTouchEvent(false);
if (!isDrag &&
event.getRawX() <= mWidth - mBackWidth) {
//展开状态下,点击左侧部分将其关闭
close();
return true;
}
isDrag = false;
break;
}
mDragHelper.processTouchEvent(event);
return true;
}
列表控件
当有展开删除项且点击了别的删除项的时候,把关闭的操作继续往父控件抛就好了:
public boolean onTouchEvent(MotionEvent e) {
return MainAdapter.mOpenItems.size() == 0 && super.onTouchEvent(e);
}
菜单控件
在这里处理一下上面说的那种情况:
public boolean onTouchEvent(MotionEvent event) {
if (MainAdapter.mOpenItems.size() > 0) {
MainAdapter.closeAll();
return true;
}
mDragHelper.processTouchEvent(event);
return true;
}
效果
扯了这么多,看下效果吧:
搞半天其实也就这样而已。
小结
这篇有点啰嗦啊,里面涉及到的细节比较多。最后可能还会存在一些问题,这里主要是提供利用事件分发机制,处理手势冲突的思路。
写这个的时候发现QQ也有一些小问题,比如QQ在删除控件展开的情况下,按住删除控件左边区域下滑后,再左右滑,会出现列表跳动的问题。
大家可以点下面去看源码。就到这吧,妥妥的。