先上干货,侧滑菜单listview和去除冲突的SwipeRefreshLayout github项目地址: https://github.com/little-tongue/SidesplidListView
前段时间项目需要侧滑菜单的ListView,所以自己重写ListView仿qq的部分效果自定义了一个SideslipListView,在Dome里面运行正常,但是在使用的时候,发现经常侧滑有时候滑一半就失灵了,并且同时触发了SwipeRefreshLayout的下拉刷新CircleImageView的显示。反复试验了几次并配合log,得出问题:当SideslipListView到了顶部且侧滑的时候出现垂直方向滑动,会导致子View的滑动事件失效,SwipeRefreshLayout处理了滑动事件,显示顶部CircleImageView。因为我的SideslipListView是通过对触摸事件做处理实现侧滑的,所以我第一反应就是可能滑动冲突了。
上面这张图相信所有人都烂熟于心了,简单分析可以知道SwipeRefreshLayout可能搞事情的地方是dispatchTouchEvent ,onInterceptTouchEvent。那就去看看源码中是如何实现。
SwipeRefreshLayout继承自ViewGroup,在SwipeRefreshLayout中没有重写dispatchTouchEvent,只重写了 onInterceptTouchEvent,所以只用看在onInterceptTouchEvent中怎么处理的。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
可以看到最终返回的是mIsBeingDragged的值,mIsBeingDragged表示SwipeRefreshLayout是否开始下拉刷新的操作,即SwipeRefreshLayout顶部的CircleImageView是否开始显示。mIsBeingDragged的值是true时,就会导致SwipeRefreshLayout的子View不能接受到相应的事件。
在分Action处理事件之前有一段代码
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
这段代码判断了SwipeRefreshLayout是否可用,SwipeRefreshLayout的子View是不是滑动到了顶部,其中ListView的判断是canChildScrollUp(),这也就是为什么最开始产生的问题中,必须是SideSlipListView滑动到顶部的时候才会产生。
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, -1);
}
return mTarget.canScrollVertically(-1);
}
ACTION_DOWN :mIsBeingDragged的第一次赋值在ACTION_DOWN中赋值为false,ACTION_DOWN中其他的代码都是初始化一些参数,可以略过。
ACTION_MOVE:在ACTION_MOVE中获取了触摸的Y坐标,然后调用了startDrag个ing(y),跟踪过去。
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
这里一目了然,把当前的y坐标和ACTION_DOWN中的起始y坐标求差,当Y轴的移动距离大于系统最小滑动距离的时候,会将mIsBeingDragged从false变成true。所以只要我们重写SwipeRefreshLayout的onInterceptTouchEvent方法,当滑动事件可判断为水平滑动的时候直接返回false,就可以解决SwipeRefreshLayout下子View的水平滑动冲突了。
/**
* 是否让子view处理touch事件
*/
private boolean letChildDealTouchEvent;
private float startX;
private float startY;
private int mTouchSlop;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录手指按下的位置
startY = ev.getY();
startX = ev.getX();
// 初始化标记
letChildDealTouchEvent = false;
break;
case MotionEvent.ACTION_MOVE:
// 如果子view正在拖拽中,那么不拦截它的事件,直接return false;
if (letChildDealTouchEvent) {
return false;
}
// 获取当前手指位置
float endY = ev.getY();
float endX = ev.getX();
float distanceX = Math.abs(endX - startX);
float distanceY = Math.abs(endY - startY);
// 如果X轴位移大于Y轴位移,那么将事件交给子View处理
if (distanceX > mTouchSlop && distanceX > distanceY) {
letChildDealTouchEvent = true;
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 初始化标记
letChildDealTouchEvent = false;
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
总结:SwipeRefreshLayout在子view滑动到顶部的时候,会对垂直方向的滑动时间做判断,当垂直方向向下的滑动距离大于系统最小滑动距离的时候,会拦截子View的Touch事件,开始做下拉刷新处理。重写SwipeRefreshLayout的onInterceptTouchEvent事件,对水平滑动做相应处理,可以避免该问题产生。