仿Android QQ左侧滑菜单右侧滑列表菜单——处理HorizontalScrollView和SwipeMenuListView滑动冲突

需求

最近项目需要一个仿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才能接收事件。再次运行,符合预期效果。

至此,已把两个控件的冲突解决了^.^

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值