在开发过程中遇到ViewPager嵌套ListView或者ScrollView嵌套ListView等情况时,可能会出现滑动冲突问题,这是因为ListView是纵向滑动,而ViewPager是横向滑动。
虽然ListView、ViewPager类内部源码已经做好了相应的处理,使他们能同时使用。但是有时为了满足自己项目的特殊需求,还是需要我们解决滑动冲突。
解决滑动冲突问题有两种方法:内部拦截法和外部拦截法。滑动冲突也存在2种场景: 横竖滑动冲突、同向滑动冲突。同向滑动和横竖滑动思路是一样的,只是用来判断是否拦截的那块逻辑不同而已。
推荐:当子元素占满父元素空间时,使用外部拦截法;当没有占满时使用内部拦截。
1.外部拦截方法
外部拦截法是指所有的触摸事件都会先经过经过父容器的传递,从而父容器在需要此触摸事件的时候就可以拦截此触摸事件,否则就传递给子View。这样就可以解决滑动冲突的问题,这种方法比较符合触摸事件的传递、处理机制。
外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法中根据滑动冲突处理规则做相应的拦截。
外部拦截法的思路是:重写父控件的onInterceptTouchEvent方法,然后根据具体的需求,来决定父控件是否拦截事件。如果拦截返回返回true,不拦截返回false。如果父控件拦截了事件,则在父控件的onTouchEvent进行相应的事件处理。
以一个横向滑动的ViewGroup里面包含了3个竖向滑动的ListView为例:
自定义横向滑动控件——HorizontalEx.java:
public class HorizontalEx extends ViewGroup {
private boolean isFirstTouch = true;
private int childIndex;
private int childCount;
private int lastXIntercept, lastYIntercept, lastX, lastY;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalEx(Context context) {
super(context);
init();
}
public HorizontalEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize( widthMeasureSpec);
int height = MeasureSpec.getSize( heightMeasureSpec);
int widthMode = MeasureSpec.getMode( widthMeasureSpec);
int heightMode = MeasureSpec.getMode( heightMeasureSpec);
childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
width = childCount * getChildAt(0).getMeasuredWidth();
height = getChildAt(0).getMeasuredHeight();
setMeasuredDimension(width, height);
} else if (widthMode == MeasureSpec.AT_MOST) {
width = childCount * getChildAt(0).getMeasuredWidth();
setMeasuredDimension(width, height);
} else {
height = getChildAt(0).getMeasuredHeight();
setMeasuredDimension(width, height);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
child.layout(left + l, t, r + left, b);
left += child.getMeasuredWidth();
}
}
@Override
public boolean onInterceptTouchEvent( MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
/*如果拦截了Down事件,则子类不会拿到这个事件序列*/
case MotionEvent.ACTION_DOWN:
lastXIntercept = x;
lastYIntercept = y;
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
final int deltaX = x - lastXIntercept;
final int deltaY = y - lastYIntercept;
/*根据条件判断是否拦截该事件*/
if (Math.abs(deltaX) > Math.abs(deltaY)){
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
lastXIntercept = x;
lastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
mVelocityTracker.addMovement(event);
ViewConfiguration configuration = ViewConfiguration.get(getContext());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
/*因为这里父控件拿不到Down事件,所以使用一个布尔值, 当事件第一次来到父控件时,对lastX,lastY赋值*/
if (isFirstTouch) {
lastX = x;
lastY = y;
isFirstTouch = false;
}
final int deltaX = x - lastX;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
final int childWidth = getChildAt(0).getWidth();
mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) > configuration.getScaledMinimumFlingVelocity()){
childIndex = xVelocity < 0 ? childIndex + 1 : childIndex - 1;
} else {
childIndex = (scrollX + childWidth / 2) / childWidth;
}
childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));
smoothScrollBy(childIndex * childWidth - scrollX, 0);
mVelocityTracker.clear();
isFirstTouch = true;
break;
}
lastX = x;
lastY = y;
return true;
}
void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVelocityTracker.recycle();
}
}
调用代码:
public void showOutHVData(List<String> data1, List<String> data2, List<String> data3) {
ListView listView1 = new ListView(this);
ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1);
listView1.setAdapter(adapter1);
ListView listView2 = new ListView(this);
ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2);
listView2.setAdapter(adapter2);
ListView listView3 = new ListView(this);
ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data3);
listView3.setAdapter(adapter3);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
mHorizontalEx.addView(listView1, params);
mHorizontalEx.addView(listView2, params);
mHorizontalEx.addView(listView3, params);
}
其实外部拦截的主要思想都在于对onInterceptTouchEvent的重写。
注意:
①重写onInterceptTouchEvent方法时一定不要在ACTION_DOWN 中返回 true,否则会让子VIew没有机会得到事件,因为如果在ACTION_DOWN的时候返回了 true,同一个事件序列ViewGroup的disPatchTouchEvent就不会再调用onInterceptTouchEvent方法了。
②在ACTION_UP中返回false,因为如果父控件拦截了ACTION_UP,那么子View将得不到UP事件,那么将会影响子View的 Onclick方法等。但这对父控件是没有影响的,因为如果是父控件在ACITON_MOVE中就拦截了事件,那么UP事件必定也会交给它处理,因为有那么一条定律叫做:父控件一但拦截了事件,那么同一个事件序列的所有事件都将交给他处理。
③最后就是在 ACTION_MOVE中根据需求决定是否拦截。
2.内部拦截法
内部拦截法是指父容器不拦截任何触摸事件,所有的触摸事件都传递给子元素,如果子元素需要此触摸事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件传递、处理机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来比外部拦截法稍显复杂。这种方法需要重写子元素的dispatchTouchEvent方法和父容器的onInterceptTouchEvent方法。
内部拦截主要依赖于requestDisallowInterceptTouchEvent方法,它用于设置父控件的FLAG_DISALLOW_INTERCEPT标志,这个标志可以决定父控件是否拦截事件,如果设置了这个标志则不拦截;如果没设这个标志,就会调用父控件的onInterceptTouchEvent来询问父控件是否拦截。但这个标志对Down事件无效。
那么如果我们想使用内部拦截法拦截事件,就需要:
①第一步: 要重写父控件的onInterceptTouchEvent方法,在ACTION_DOWN的时候返回false,否则的话子View调用requestDisallowInterceptTouchEvent也无能为力。 然后除ACTON_DOWN以外的其他事件都返回true,这样就把能否拦截事件的权利交给了子View。
②第二步: 在子View的dispatchTouchEvent中来决定是否让父控件拦截事件。 先在ACTION_DOWN的时候使用requestDisallowInterceptTouchEvent(true),否则,下一个事件到来时,就交给父控件了。 然后在ACTION_MOVE的时候根据业务逻辑决定是否调用requestDisallowInterceptTouchEvent(false)来决定父控件是否拦截事件。
上代码:
public class HorizontalEx2 extends ViewGroup {
……
//不拦截Down事件,其他一律拦截
@Override
public boolean onInterceptTouchEvent( MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
……
}
自定义ListView——ListViewEx.java:
public class ListViewEx extends ListView {
private int lastXIntercepted, lastYIntercepted;
private HorizontalEx2 mHorizontalEx2;
……//3个默认构造方法
public void setmHorizontalEx2(HorizontalEx2 mHorizontalEx2) {
this.mHorizontalEx2 = mHorizontalEx2;
}
//使用requestDisallowInterceptTouchEvent();来决定父控件是否对事件进行拦截
@Override
public boolean dispatchTouchEvent( MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mHorizontalEx2. requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
final int deltaX = x-lastYIntercepted;
final int deltaY = y-lastYIntercepted;
if(Math.abs(deltaX)>Math.abs(deltaY)){
mHorizontalEx2. requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
lastXIntercepted = x;
lastYIntercepted = y;
return super.dispatchTouchEvent(ev);
}
}
调用代码:
public void showInnerHVData(List<String> data1, List<String> data2, List<String> data3) {
ListViewEx listView1 = new ListViewEx(this);
ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1);
listView1.setAdapter(adapter1);
listView1. setmHorizontalEx2(mHorizontalEx2);
ListViewEx listView2 = new ListViewEx(this);
ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2);
listView2.setAdapter(adapter2);
listView2. setmHorizontalEx2(mHorizontalEx2);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams. MATCH_PARENT);
mHorizontalEx2.addView(listView1, params);
mHorizontalEx2.addView(listView2, params);
}
内部拦截法关键点:
①父容器onInterceptTouchEvent默认拦截ACTION_MOVE,但是ACTION_DOWN不能拦截。因为一旦拦截ACTION_DOWN,整个事件序列都会被拦截,子View就没机会处理ACTION_MOVE了。
②子View接收到ACTION_DOWN后,调用getParent().requestDisallowInterceptTouchEvent(true)来取消父容器的拦截,告诉父容器接下来的事件序列不要拦截,这样后面的事件子View都能接收到。
③子View接收到ACTION_MOVE后,判断滑动方向,根据方向确定由谁处理事件。如果判断为父容器处理滑动,则调用getParent().requestDisallowInterceptTouchEvent(false)开启父容器拦截,父容器就能处理ACTION_MOVE了;如果判断为子View处理滑动,由于子View接收到ACTION_DOWN时,已经取消了父容器的拦截,子view直接处理滑动即可。
举个例子:
使用内部拦截法,需要修改把父容器拦截方法:
@Override
public boolean onInterceptTouchEvent( MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
同时修改子View的dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//接到ACTION_DOWN开始取消父容器的事件拦截
getParent( ).requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//横向偏移大,判定为横向滑动,交给父容器ViewPager处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
//父容器恢复事件拦截,事件交给父容器处理
getParent( ).requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
3.注意
①down事件只会分发一次,move会分发多次
②内部拦截思路是子View在dispatchTouchEvent方法中通过调用requestDisallowInterceptTouchEvent(true)方法,禁止父View拦截事件。并在合适的场景将其置为fase允许拦截
③外部拦截思路是父View通过重写onInterceptTouchEvent方法,在合适的场景拦截事件
注意,如果是down事件,父亲拦截了这个事件,那么子View 将不在收到后续的事件,因此在解决事件冲突的时候,父亲不能拦截down事件,而是在后续的move 事件在合适的场景做拦截,比如滑动的方向。
当然如果需求是父亲要直接拦截子View的事件,那么也可以事件拦截掉down事件