需求
最近项目需要一个仿QQ的界面,向右滑可以拉出菜单,向左滑则拉出列表菜单。
初步实现
这两个侧滑控件网上都有了,这里我用的是:
- 侧滑菜单XCSlideMenu(以下简称Slide),详见http://www.w2bc.com/Article/13213
- 列表侧滑SwipeMenuListView(以下简称Swipe),详见https://github.com/baoyongzhang/SwipeMenuListView
问题分析
如果单独使用这两个控件的话是没问题的,但是如果放到一起,在屏幕左右滑动时,Swipe无法正确响应。
因为Slide实则是个HorizontalScrollView,两控件的滑动响应都是水平方向的,这是Android典型的事件冲突。这样的话,事件应该进行分发,根据场景分配给相应的处理者。
这里只分析下思路,对事件传递不熟悉的童鞋可以学习http://www.cnblogs.com/jqyp/archive/2012/04/25/2469758.html。
处理
分析完问题的原因,可以想到一个处理办法:用一个变量来标志当前的状态。
由于Slide是父容器,所以我把标记放那里了,同时声明几种状态:
/** 普通状态 */
public static final int STATUS_NORMAL = 0;
/** 左边侧滑菜单操作中 */
public static final int STATUS_SLIDE = 1;
/** 列表侧滑菜单操作中 */
public static final int STATUS_SWIPE = 2;
/** 列表侧滑菜单已关上但手指还未离开 */
public static final int STATUS_SWIPE_STICK = 4;
/** 当前界面的状态 */
public static int sStatus = STATUS_NORMAL;
1)界面的初始状态是STATUS_NORMAL,右滑则打开Slide,左滑则拉开Swpie。
那么先在父容器里拦截右滑,改变状态标记,在Slide里加上:
private float x1;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
float x2 = ev.getX();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
x1 = x2;
return super.onInterceptTouchEvent(ev);
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
// 只有普通状态时右滑时才拦截事件
if (x2 > x1 && isMove(x1, x2) && sStatus == STATUS_NORMAL) {
isIntercept = true;
sStatus = sStatus | STATUS_SLIDE;
}
}
x1 = x2;
return isIntercept;
}
x1用于记录按下时的x坐标,isMove方法用于判断手指的移动距离是不是大于某个临界值,true的话才算真正滑动了手指。isSlideOut后面会说。
运行下,发现有点成效了,原本不正常的Swipe可以拉开。然后Slide也能拉开,但是收不回去。
2)接下来处理状态STATUS_SLIDE。
Slide控件里有个isSlideOut成员变量,是标志菜单是否打开状态。可以借助这个变量,每当其改变值时,跟上我们的逻辑:
isSlideOut = true;
sStatus = sStatus | STATUS_SLIDE;
isSlideOut = false;
sStatus = sStatus & ~STATUS_SLIDE;
继续完善Slide的onInterceptTouchEvent方法,加入了左滑的处理:
else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
// 只有普通状态时右滑&侧滑菜单打开时左滑才拦截事件
if (x2 > x1 && isMove(x1, x2) && sStatus == STATUS_NORMAL) {
isIntercept = true;
sStatus = sStatus | STATUS_SLIDE;
} else if (x2 < x1 && isMove(x1, x2) && isSlideOut) {
isIntercept = true;
}
}
同时我们想要实现跟QQ一样的,点击右边缩小的Swipe界面能关闭Slide,那么就不是MOVE事件了,修改onInterceptTouchEvent:
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
x1 = x2;
// 只有侧滑菜单打开时点击右侧才拦截事件
if (x2 > mMenuWidth && isSlideOut) {
isIntercept = true;
} else {
return super.onInterceptTouchEvent(ev);
}
}
也需要更新onTouchEvent方法:
float x2 = ev.getX();
switch (action) {
case MotionEvent.ACTION_UP:
...
} else if (!isMove(x1, x2) && x2 > mMenuWidth && isSlideOut) { // 侧滑菜单打开时,点击右侧将其关闭
slideInMenu();
} else{
...
}
运行下,Slide打开状态的各种事件也正确处理了。
3)再看下STATUS_SWIPE状态。
Swipe为我们提供了一个接口OnSwipeListener,包含了onSwipeStart和onSwipeEnd方法。故名思议,就是拉开或关闭Swipe的菜单时会触发的回调。那么我们只要在使用了Swipe控件的地方实现接口,跟上我们的逻辑即可:
mListView.setOnSwipeListener(new OnSwipeListener() {
@Override
public void onSwipeStart(int position) {
XCSlideMenu.sStatus = XCSlideMenu.STATUS_SWIPE;
}
@Override
public void onSwipeEnd(int position) {
if (position < 0) {
XCSlideMenu.sStatus = XCSlideMenu.STATUS_NORMAL;
}
}
});
运行下,在Swipe某一项上左滑右滑等操作,结果是符合我们的预期的。
但是只拉开Swipe菜单,然后单击外部空白处,Swipe菜单是收回去了,但是Slide又拉不出来了。
只能回Swipe的源码查看,发现点击空白是不会回调onSwipeEnd的(坑啊……)。而且,处理点击空白这个行为时,在ACTION_DOWN方法里调用了ACTION_CANCEL,强行结束事件。这样会出现什么问题呢?我们先试一下。
为OnSwipeListener加上新方法:
void onSwipeCancel(int position);
在Swipe的onTouchEvent的ACTION_DOWN里加上:
if (mTouchView != null && mTouchView.isOpen()) {
...
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeCancel(oldPos);
}
return true;
}
再完善回调:
mListView.setOnSwipeListener(new OnSwipeListener() {
...
@Override
public void onSwipeCancel(int position) {
XCSlideMenu.sStatus = XCSlideMenu.STATUS_NORMAL;
}
});
运行后拉开Swipe菜单,然后单击空白,再右滑,Slide可以拉出。
如果拉开Swipe菜单后按住空白处再右滑,发现Swipe是收回去了,但是Slide也跟着出来了,从用户体验角度来看,这不符合我们的预期。于是要加多一个状态STATUS_SWIPE_STICK。
4)新状态STATUS_SWIPE_STICK,指的是在已展开菜单的Swipe的空白处按下,但手指不离开屏幕。此时的焦点应该还是Swipe,所以要改下mListView的回调:
public void onSwipeCancel(int position) {
XCSlideMenu.sStatus = XCSlideMenu.STATUS_SWIPE_STICK;
}
再为Slide的onInterceptTouchEvent添加:
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
if (sStatus == STATUS_SWIPE_STICK) {
sStatus = STATUS_NORMAL;
}
}
这意味着按住空白并左右滑动,事件还是交由Swipe处理,只有手指抬起,才把状态恢复到普通,Slide才能接收事件。再次运行,符合预期效果。
至此,已把两个控件的冲突解决了^.^