事件拦截机制
定义MyViewGroupA,打印Log
public class MyViewGroupA extends RelativeLayout {
private static final String TAG = MyViewGroupA.class.getSimpleName();
public MyViewGroupA(Context context) {
this(context, null);
}
public MyViewGroupA(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
定义MyViewGroupB同上
public class MyViewGroupB extends RelativeLayout {
private static final String TAG = MyViewGroupB.class.getSimpleName();
public MyViewGroupB(Context context) {
this(context, null);
}
public MyViewGroupB(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroupB(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyViewGroupB(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
}
定义MyView,打印Log
public class MyView extends View {
private static final String TAG = MyView.class.getSimpleName();
public MyView(Context context) {
this(context, null);
}
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: ");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent: ");
return super.dispatchTouchEvent(event);
}
}
布局如下,MyViewGroupA包裹MyViewGroupB再包裹MyView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.demo.demo0.MyViewGroupA
android:layout_width="300dp"
android:layout_height="300dp"
android:background="#FF0000">
<com.demo.demo0.MyViewGroupB
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#00FF00">
<com.demo.demo0.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#000FFF" />
</com.demo.demo0.MyViewGroupB>
</com.demo.demo0.MyViewGroupA>
</LinearLayout>
点击最内层的MyView,打印如下
MyViewGroupA: dispatchTouchEvent:
MyViewGroupA: onInterceptTouchEvent:
MyViewGroupB: dispatchTouchEvent:
MyViewGroupB: onInterceptTouchEvent:
MyView: dispatchTouchEvent:
MyView: onTouchEvent:
MyViewGroupB: onTouchEvent:
MyViewGroupA: onTouchEvent:
可知
- 事件传递顺序是由外向内,先dispatchTouchEvent再onInterceptTouchEvent
- 事件处理顺序由内向外,即onTouchEvent
- 要拦截事件,只需要让返回值为True
可用如下伪代码表示三个方法的关系
若MyViewGroupA的onInterceptTouchEvent()方法返回True,打印如下
MyViewGroupA: dispatchTouchEvent:
MyViewGroupA: onInterceptTouchEvent:
MyViewGroupA: onTouchEvent:
若MyViewGroupB的onInterceptTouchEvent()方法返回True,打印如下
MyViewGroupA: dispatchTouchEvent:
MyViewGroupA: onInterceptTouchEvent:
MyViewGroupB: dispatchTouchEvent:
MyViewGroupB: onInterceptTouchEvent:
MyViewGroupB: onTouchEvent:
MyViewGroupA: onTouchEvent:
若MyView的onTouchEvent()方法返回True,打印如下
MyViewGroupA: dispatchTouchEvent:
MyViewGroupA: onInterceptTouchEvent:
MyViewGroupB: dispatchTouchEvent:
MyViewGroupB: onInterceptTouchEvent:
MyView: dispatchTouchEvent:
MyView: onTouchEvent:
若MyViewGroupB的onTouchEvent()方法返回True,打印如下
MyViewGroupA: dispatchTouchEvent:
MyViewGroupA: onInterceptTouchEvent:
MyViewGroupB: dispatchTouchEvent:
MyViewGroupB: onInterceptTouchEvent:
MyView: dispatchTouchEvent:
MyView: onTouchEvent:
MyViewGroupB: onTouchEvent:
事件拦截机制源码分析
上层事件按照如下顺序进行传递:
- Activity
- Phone Window
- DecorView
- ViewGroup
- View
Acitivity
onUserInteraction()是一个空方法,事件会传递给Window
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Phone Window
Window的唯一实现类为Phone Window,其将事件传递给DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView
调用父类的dispatchTouchEvent,其继承FrameLayout(但未重写方法),故事件将会被传递给ViewGroup
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
ViewGroup
一个按键事件序列以ACTION_DOWN开始,以ACTION_UP结束,接下来逐一介绍各个内容
TouchTarget
按键传递用链表存储,即TouchTarget
- mFirstTouchTarget是头节点
- 相比于普通的链表,其采用了内部复用sRecycleBin
下面代码将TouchTarget相关方法复制出来,并重写了toString()
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private TouchTarget mFirstTouchTarget;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View view1 = new View(this);
View view2 = new View(this);
View view3 = new View(this);
TouchTarget touchTarget1 = addTouchTarget(view1, 1);
TouchTarget touchTarget2 = addTouchTarget(view2, 2);
TouchTarget touchTarget3 = addTouchTarget(view3, 3);
Log.d(TAG, "print touchTarget ------------------------------------------------");
Log.d(TAG, "touchTarget1 = " + touchTarget1);
Log.d(TAG, "touchTarget2 = " + touchTarget2);
Log.d(TAG, "touchTarget3 = " + touchTarget3);
clearTouchTargets();
Log.d(TAG, "clearTouchTargets ------------------------------------------------");
Log.d(TAG, "touchTarget1 = " + touchTarget1);
Log.d(TAG, "touchTarget2 = " + touchTarget2);
Log.d(TAG, "touchTarget3 = " + touchTarget3);
Log.d(TAG, "print touchTarget------------------------------------------------");
TouchTarget touchTarget4 = addTouchTarget(view1, 1);
TouchTarget touchTarget5 = addTouchTarget(view2, 2);
TouchTarget touchTarget6 = addTouchTarget(view3, 3);
Log.d(TAG, "touchTarget4 = " + touchTarget4);
Log.d(TAG, "touchTarget5 = " + touchTarget5);
Log.d(TAG, "touchTarget6 = " + touchTarget6);
}
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;
public static final int ALL_POINTER_IDS = -1; // all ones
// The touched child view.
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
private TouchTarget() {
}
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
if (child == null) {
throw new IllegalArgumentException("child must be non-null");
}
final TouchTarget target;
synchronized (sRecycleLock) {
if (sRecycleBin == null) {
target = new TouchTarget();
} else {
target = sRecycleBin;
sRecycleBin = target.next;
sRecycledCount--;
target.next = null;
}
}
target.child = child;
target.pointerIdBits = pointerIdBits;
return target;
}
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
}
synchronized (sRecycleLock) {
if (sRecycledCount < MAX_RECYCLED) {
next = sRecycleBin;
sRecycleBin = this;
sRecycledCount += 1;
} else {
next = null;
}
child = null;
}
}
@Override
public String toString() {
return "TouchTarget+" + Integer.toHexString(hashCode()) + "{" +
"child=" + child +
", pointerIdBits=" + pointerIdBits +
", next=" + next +
'}';
}
}
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
}
当第一次调用addTouchTarget(),通过移动mFirstTouchTarget链接链表,因为链表是反向链接的,所以遍历子View的时候要从后往前
touchTarget1 = TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}
touchTarget2 = TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}
touchTarget3 = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
mFirstTouchTarget = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
当调用clearTouchTargets()时,通过移动sRecycleBin反向链表
- 数量小于32时,只清空child
- 大于32时,将会断开节点,交由gc回收
touchTarget1 = TouchTarget+5787a73{child=null, pointerIdBits=1, next=TouchTarget+eb57da9{child=null, pointerIdBits=2, next=TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}}}
touchTarget2 = TouchTarget+eb57da9{child=null, pointerIdBits=2, next=TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}}
touchTarget3 = TouchTarget+d64acf{child=null, pointerIdBits=3, next=null}
mFirstTouchTarget = null
当再次调用addTouchTarget()时,将会复用sRecycleBin,如下,打印的TouchTarget456和之前创建的TouchTarget123地址一样
touchTarget4 = TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}
touchTarget5 = TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}
touchTarget6 = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
mFirstTouchTarget = TouchTarget+d64acf{child=android.view.View{1231c5c V.ED..... ........ 0,0-0,0}, pointerIdBits=3, next=TouchTarget+eb57da9{child=android.view.View{9462f2e V.ED..... ........ 0,0-0,0}, pointerIdBits=2, next=TouchTarget+5787a73{child=android.view.View{18d5d30 V.ED..... ........ 0,0-0,0}, pointerIdBits=1, next=null}}}
dispatchTouchEvent
如下是基于API 28的源码,省略部分无关代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
.....
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//每个down到来时都会初始化为最开始的状态,因为按键可能会因为某些原因未通过up或cancel终止
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev); //对所有child发送cancel事件
resetTouchState(); //清除TouchTargets、FLAG_DISALLOW_INTERCEPT(故其不能在down事件中设置)
}
//1.当ViewGroup接收到dwon时一定会调用onInterceptTouchEvent(默认返回false),若不拦截,则down会传给child
//1.1 若child处理down,则mFirstTouchTarget != null
//1.1.1 接下来的事件序列到来时,若child设置了FLAG_DISALLOW_INTERCEPT,则不再询问ViewGroup的onInterceptTouchEvent,即全部交给child处理
//1.1.2 若child未设置了FLAG_DISALLOW_INTERCEPT,则每次都询问ViewGroup的onInterceptTouchEvent
//1.2 若child未处理down,mFirstTouchTarget==null,则ViewGroup自身处理接下来的事件序列,且不用再询问onInterceptTouchEvent()
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) { //对应情况1.1.2
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else { //对应情况1.1.1
intercepted = false;
}
} else { //对应情况1.2
intercepted = true;
}
......
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
......
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
......
//当ViewGroup的onInterceptTouchEvent()不拦截down,则传递给child,询问child是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN
|| ......) {
......
final int actionIndex = ev.getActionIndex();
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//下面是构建ChildList和获取Child
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//下面跳过 无法接收事件 与 不在触摸位置 的子view
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//下面判断是否已经在TouchTarget链表中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent将坐标转换成child中的坐标
//调用child的dispatchTouchEvent()判断是否处理down
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
......
//若child处理,将其添加到TouchTarget链表中,设置标志位
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
......
}
......
}
......
}
}
if (mFirstTouchTarget == null) {
//走到这里说明
//1.当前ViewGroup的onInterceptTouchEvent()拦截所有或接下来的事件序列
//2.ViewGoup没有child
//3.child的onTouchEvent()处理down时返回false,接下来的事件也不会传递给child
//此时,ViewGroup将作为View调用onTouchEvent(),若仍不处理,则往上调用父容器的onTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//下面分发接下来的事件序列
//对应情况1.1.1,down被child处理,且接下来的事件都由child处理
//对应情况1.1.2,down(或+move)被child处理,但接下来的事件序列被ViewGroup的onInterceptTouchEvent()拦截
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//走到这,说明child处理了down,不再重复处理
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//若intercepted=true,cancelChild=true,对应情况1.1.2,遍历child发送cancel,接下来的事件序由ViewGroup处理
//若intercepted=false,cancelChild=false,对应上面情况1.1.1,接下来的事件都由child处理,若child成功处理,则当前dispatchTouchEvent()返回true,若返回false,往上调用父容器的onTouchEvent()
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) { //1.1.2,遍历清空TouchTarget和mFirstTouchTarget,接下来的事件将会走mFirstTouchTarget == null的判断
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//下面收尾,处理up/cancel,恢复初始状态
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| ......) {
resetTouchState();
} ......
}
......
return handled;
}
View
dispatchTouchEvent
如下是基于API 28的源码,省略部分无关代码
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
......
//若有设置OnTouchListener,则优先调用onTouch()
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//否则调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
onTouchEvent
如下是基于API 28的源码,省略部分无关代码
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
......
//当View disable但clickable时,也会消耗事件
return clickable;
}
//下面处理事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
//下面处理点击事件
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
......
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
.....
break;
}
return true;
}
return false;
}
PerformClick会调用performClick(),在这里判断和调用onClick()方法
public boolean performClick() {
......
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
......
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
......
return result;
}
滑动冲突
主要分为
- 外部滑动和内部滑动方向不一致,根据滑动方向判断谁来拦截事件
- 外部滑动和内部滑动方向一致,根据业务逻辑判断谁来拦截事件
- 上面两种情况的嵌套
外部拦截法
事件都先判断父容器是否拦截,然后再判断child,具体做法为
- down返回false,因为返回true会导致接下来的事件都由父容器处理
- move根据情况判断是否拦截
- up返回false,因为返回true,child接收不到up,会导致child的onClick()失效
如下重写父容器的onInterceptTouchEvent()
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (isParentIntercept()) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
private boolean isParentIntercept() {
return false;
}
内部拦截法
父容器不拦截任何事件,如果child处理就消耗事件,否则交给父容器处理,具体做法为
- 父容器拦截除down以外的所有事件,因为拦截down会导致接下来的事件都由父容器处理
- child在down中设置FLAG_DISALLOW_INTERCEPT,在接下来的事件中不再询问父容器的onInterceptTouchEvent()
- 当需要父容器处理时,child在move中取消FLAG_DISALLOW_INTERCEPT,此时事件会被父容器拦截处理
如下重写父容器的onInterceptTouchEvent()
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
}
return true;
}
如下重写child的dispatchTouchEvent()
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
ViewParent parent = getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (isParentIntercept()) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
private boolean isParentIntercept() {
return false;
}
实例
外部拦截法
自定义横向滑动组件HorizontalScrollViewEx,往里面放两个ListView,滑动方向不一致导致冲突
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.demo.demo0.HorizontalScrollViewEx
android:id="@+id/listContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ListView
android:id="@+id/list2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.demo.demo0.HorizontalScrollViewEx>
</LinearLayout>
MainActivity 代码如下
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private String[] data1 = {"A", "B", "C", "D", "E", "F", "G", "H",
"A", "B", "C", "D", "E", "F", "G", "H"};
private String[] data2 = {"1", "2", "3", "4", "5", "6", "7", "8",
"1", "2", "3", "4", "5", "6", "7", "8"};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
ArrayAdapter<String> adapter1 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data1);
ArrayAdapter<String> adapter2 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data2);
((ListView) findViewById(R.id.list1)).setAdapter(adapter1);
((ListView) findViewById(R.id.list2)).setAdapter(adapter2);
}
}
具体实现如下,判断当横向移动距离大于纵向时,父容器才拦截处理
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
private int mLastX;
private int mLastY;
private int mLastXIntercept;
private int mLastYIntercept;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
this(context, null);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = x - mLastYIntercept;
intercepted = Math.abs(deltaX) > Math.abs(deltaY);
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
Log.d(TAG, "onInterceptTouchEvent: intercepted = " + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
measureWidth = child.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, child.getMeasuredHeight());
} else if (heightMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
setMeasuredDimension(widthMeasureSpec, child.getMeasuredHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
measureWidth = child.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childMeasuredWidth = child.getMeasuredWidth();
mChildWidth = childMeasuredWidth;
child.layout(childLeft, 0, childLeft + childMeasuredWidth, child.getMeasuredHeight());
childLeft += childMeasuredWidth;
}
}
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVelocityTracker.recycle();
}
}
内部拦截法
修改HorizontalScrollViewEx,拦截除down外的事件
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
private int mLastX;
private int mLastY;
private int mLastXIntercept;
private int mLastYIntercept;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
this(context, null);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
measureWidth = child.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, child.getMeasuredHeight());
} else if (heightMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
setMeasuredDimension(widthMeasureSpec, child.getMeasuredHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {
View child = getChildAt(0);
measureWidth = child.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {
int childMeasuredWidth = child.getMeasuredWidth();
mChildWidth = childMeasuredWidth;
child.layout(childLeft, 0, childLeft + childMeasuredWidth, child.getMeasuredHeight());
childLeft += childMeasuredWidth;
}
}
}
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVelocityTracker.recycle();
}
}
创建ListViewEx,重写dispatchTouchEvent()
public class ListViewEx extends ListView {
private int mLastX = 0;
private int mLastY = 0;
public ListViewEx(Context context) {
this(context, null);
}
public ListViewEx(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
ViewParent parent = getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
}
布局如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.demo.demo0.HorizontalScrollViewEx
android:id="@+id/listContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.demo.demo0.ListViewEx
android:id="@+id/list1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.demo.demo0.ListViewEx
android:id="@+id/list2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.demo.demo0.HorizontalScrollViewEx>
</LinearLayout>
MainActivity如下
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private String[] data1 = {"A", "B", "C", "D", "E", "F", "G", "H",
"A", "B", "C", "D", "E", "F", "G", "H"};
private String[] data2 = {"1", "2", "3", "4", "5", "6", "7", "8",
"1", "2", "3", "4", "5", "6", "7", "8"};
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
ArrayAdapter<String> adapter1 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data1);
ArrayAdapter<String> adapter2 = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1, data2);
((ListView) findViewById(R.id.list1)).setAdapter(adapter1);
((ListView) findViewById(R.id.list2)).setAdapter(adapter2);
}
}