在做demo的时候,发现ListView分发事件到TextView的时候,感觉log打印的和预期的不一致。后来通过分析大概搞懂了。 这里记录一下分析过程。
demo很简单,就是一个listview内部包含TextView,通过上下滑动来分析其事件是如何分发解处理的。
首先要明白一个概念,即View是否可点击,下面给出其定义。
View可点击:
可点击包括很多种情况,只要你给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的。
另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。但是TextView默认是不可点击的。可以通过View的isClickable()查看其是否可点击。
所以假如TextView没有设置监听器,或其clickable属性没有声明为true。 则其默认不可点击,意味着TextView 不会消费事件。其onTouchEvent()返回false。
还是贴下代码吧,为了查看事件分发过程,所以自定义了ListView和TextView,并继承一下事件分发和处理的函数。
public class MyListView extends ListView {
private static final String TAG = MyListView.class.getSimpleName();
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN");
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE");
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "dispatchTouchEvent: ACTION_UP");
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL");
break;
}
default:break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean res = super.onInterceptTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "onInterceptTouchEvent: ACTION_DOWN res=" + res);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "onInterceptTouchEvent: ACTION_MOVE res=" + res);
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "onInterceptTouchEvent: ACTION_UP res=" + res);
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "onInterceptTouchEvent: ACTION_CANCEL res=" + res);
break;
}
default:break;
}
return res;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean res = super.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "onTouchEvent: ACTION_DOWN res=" + res);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "onTouchEvent: ACTION_MOVE res=" + res);
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "onTouchEvent: ACTION_UP res=" + res);
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "onTouchEvent: ACTION_CANCEL res=" + res);
break;
}
default:break;
}
return res;
}
}
public class MyTextView extends AppCompatTextView {
private static final String TAG = MyTextView.class.getSimpleName();
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean res = super.dispatchTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN res=" + res);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE res=" + res);
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "dispatchTouchEvent: ACTION_UP res=" + res);
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL res=" + res);
break;
}
default:break;
}
return res;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean res = super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "onTouchEvent: ACTION_DOWN res=" + res);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "onTouchEvent: ACTION_MOVE res=" + res);
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "onTouchEvent: ACTION_UP res=" + res);
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "onTouchEvent: ACTION_CANCEL res=" + res);
break;
}
default:break;
}
return res;
}
}
一、TextView不可点击的情况
上下滑动listview,log打印结果如下:
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=false
MyTextView: dispatchTouchEvent: ACTION_DOWN res=false
MyListView: onTouchEvent: ACTION_DOWN res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_UP
MyListView: onTouchEvent: ACTION_UP res=true
1.1 down事件的分发和处理
1、down事件经过listview的dispatchTouchEvent()函数,并在函数内部执行onInterceptTouchEvent()函数决定是否拦截down事件。 onInterceptTouchEvent返回false,所以listview没有拦截down事件。然后listview将down事件分发到了其子view,即TextView。
----这导致listview的dispatchTouchEvent()函数内部逻辑的第二步(事件分发)没有找到合适的子view消费事件。
3、这时候listview的dispatchTouchEvent()函数还没有执行完,接着执行listview的dispatchTouchEvent()内部第三步逻辑(事件处理)。由于第二步事件分发时没有合适的子view消费事件,所以mFirstTouchTarget == null, 所以listview自己调用onTouchEvent()尝试消费down事件。如果消费了返回true,否则返回false则交给上一级viewgroup处理。这里listview的onTouchEvent()返回的是true。
上面的三点对应的log为
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=false
MyTextView: dispatchTouchEvent: ACTION_DOWN res=false
MyListView: onTouchEvent: ACTION_DOWN res=true
1.2 非down事件的处理
down事件之后就是move事件了。因为down事件被listview自己消费了。所以同一事件序列的后续事件不再会执行onInterceptTouchEvent()函数,而是直接都被listview自己消费,即直接调用onTouchEvent()。对应log为:
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_UP
MyListView: onTouchEvent: ACTION_UP res=true
二、TextView可点击的情况
下面来研究下TextView可点击的情况。这里我们直接在xml文件中给textview的clickable设置成true。
<com.example.helloworld.views.textviews.MyTextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:clickable="true"
android:text="TextView" />
同样是上下滑动,我们再来看看log输出情况:
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=true
MyTextView: dispatchTouchEvent: ACTION_DOWN res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onInterceptTouchEvent: ACTION_MOVE res=true
MyTextView: onTouchEvent: ACTION_CANCEL res=true
MyTextView: dispatchTouchEvent: ACTION_CANCEL res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: dispatchTouchEvent: ACTION_UP
MyListView: onTouchEvent: ACTION_UP res=true
同样先来分析down事件是如何分发和处理的。
2.1 down事件的分发和处理
1、down事件经过listview的dispatchTouchEvent()函数,并在函数内部执行onInterceptTouchEvent()函数决定是否拦截down事件。 onInterceptTouchEvent返回false,所以listview没有拦截down事件。然后listview将down事件分发到了其子view,即TextView。
----所以listview的dispatchTouchEvent()函数内部逻辑的第二步(事件分发)找到合适的子view消费事件。
3、这时候listview的dispatchTouchEvent()函数还没有执行完,接着执行listview的dispatchTouchEvent()内部第三步逻辑(事件处理)。由于第二步事件分发时子view消费了事件,所以mFirstTouchTarget != null, 所以dispatchTouchEvent()直接返回true。
对应log为:
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=true
MyTextView: dispatchTouchEvent: ACTION_DOWN res=true
2.2 第一个move事件的处理
接着分析move事件及后续事件。我们来看第一个move事件的log输出为:
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onInterceptTouchEvent: ACTION_MOVE res=true
MyTextView: onTouchEvent: ACTION_CANCEL res=true
MyTextView: dispatchTouchEvent: ACTION_CANCEL res=true
可以看到listview拦截了第一个move事件。这需要到listview的onInterceptTouchEvent()查看内部实现逻辑。AbsListView#onInterceptTouchEvent的代码为:
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
int touchMode = mTouchMode;
if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
mMotionCorrection = 0;
return true;
}
final int x = (int) ev.getX();
final int y = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
int motionPosition = findMotionRow(y);
if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
// User clicked on an actual view (and was not stopping a fling).
// Remember where the motion event started
v = getChildAt(motionPosition - mFirstPosition);
mMotionViewOriginalTop = v.getTop();
mMotionX = x;
mMotionY = y;
mMotionPosition = motionPosition;
mTouchMode = TOUCH_MODE_DOWN;
clearScrollingCache();
}
mLastY = Integer.MIN_VALUE;
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
startNestedScroll(SCROLL_AXIS_VERTICAL);
if (touchMode == TOUCH_MODE_FLING) {
return true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
switch (mTouchMode) {
case TOUCH_MODE_DOWN:
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
final int y = (int) ev.getY(pointerIndex);
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
return true;
}
break;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mTouchMode = TOUCH_MODE_REST;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
stopNestedScroll();
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(ev);
break;
}
}
return false;
}
可以看到,在down事件的时候设置了mTouchMode = TOUCH_MODE_DOWN,在move事件时针对TOUCH_MODE_DOWN这种情况startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)返回的为true。所以onInterceptTouchEvent()返回true。表示listview拦截了move事件。
接着执行到的是listview的dispatchTouchEvent()内部第三步(move事件不走第二步事件分发)。因为mFirstTouchTarget != null 且intercepted=true,所以第三步最后调用的是
dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)
其中cancelChild=true,target.child是子view,即TextView。dispatchTransformedTouchEvent函数内部会调用child.dispatchTouchEvent(event),并且event此时被替换成ACTION_CANCEL了。所以子view会接收到ACTION_CANCEL事件,并消费它。
最后listview的dispatchTouchEvent()第三步还把该子view从mFirstTouchTarget指向的链表中回收了,mFirstTouchTarget变成了null。
2.3 后续move事件和其他事件的处理
第一个move事件执行完之后,mFirstTouchTarget == null,所以第二个move事件进入到lstview的dispatchTouchEvent()方法中,第一步条件不满足,直接跳过。第二步事件分发也直接跳过,第三步直接调用dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);调用的是listview自己本身的onTouchEvent()执行事件处理。
所以后续的事件进入到dispatchTouchEvent()方法中就直接跳到第三步执行自己的事件处理函数了。
三、进阶:listview外部套一个ScrollView
既然都分析到这里了,干脆再讲下在listview外部再套一个ScrollView的情况。 这可以实现页面左右、上下滑动。
HorizontalScrollViewEx代码为:
public class HorizontalScrollViewEx extends ViewGroup {
private static final String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
// 分别记录上次滑动的坐标(onInterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.e(TAG, "dispatchTouchEvent: ACTION_DOWN");
break;
}
case MotionEvent.ACTION_MOVE: {
Log.e(TAG, "dispatchTouchEvent: ACTION_MOVE");
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "dispatchTouchEvent: ACTION_UP");
break;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "dispatchTouchEvent: ACTION_CANCEL");
break;
}
default:break;
}
boolean res = super.dispatchTouchEvent(event);
return res;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//默认viewgroup不拦截down事件
intercepted = false;
//当左右滑动动画还没结束时触发down事件,
// 则左右移动动画被取消,viewgroup拦截down事件
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
Log.e(TAG, "onInterceptTouchEvent: DOWN intercepted=" + intercepted);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//x方向偏移量大于y方向偏移量,则viewgroup拦截move事件自己处理
//否则交给子view处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
Log.e(TAG, "onInterceptTouchEvent: MOVE " + "Math.abs(deltaX) = " + Math.abs(deltaX)
+ " Math.abs(deltaY)= " + Math.abs(deltaY) + "intercepted=" + intercepted);
break;
}
case MotionEvent.ACTION_UP: {
//up事件默认不拦截
intercepted = false;
Log.e(TAG, "onInterceptTouchEvent: UP intercepted=" + intercepted);
break;
}
case MotionEvent.ACTION_CANCEL:
Log.e(TAG, "onInterceptTouchEvent: CANCLE intercepted=" + intercepted);
break;
default:
break;
}
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: {
Log.e(TAG, "onTouchEvent: DOWN");
//当左右滑动动画还没结束时触发down事件,
// viewgroup拦截down事件,并取消左右滑动动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.e(TAG, "onTouchEvent: MOVE" + " --- Math.abs(deltaX) = " + Math.abs(deltaX)
+ " Math.abs(deltaY)= " + Math.abs(deltaY));
//-deltaX表示控件内容向右移动deltaX距离
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
Log.e(TAG, "onTouchEvent: UP");
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
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;
}
case MotionEvent.ACTION_CANCEL: {
Log.e(TAG, "onTouchEvent: CANCLE");
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
...
}
3.1 上下滑动
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_DOWN
HorizontalScrollViewEx: onInterceptTouchEvent: DOWN intercepted=false
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=true
MyTextView: dispatchTouchEvent: ACTION_DOWN res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
HorizontalScrollViewEx: onInterceptTouchEvent: MOVE Math.abs(deltaX) = 3 Math.abs(deltaY)= 23 intercepted=false
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onInterceptTouchEvent: ACTION_MOVE res=true
MyTextView: onTouchEvent: ACTION_CANCEL res=true
MyTextView: dispatchTouchEvent: ACTION_CANCEL res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
MyListView: dispatchTouchEvent: ACTION_MOVE
MyListView: onTouchEvent: ACTION_MOVE res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_UP
MyListView: dispatchTouchEvent: ACTION_UP
MyListView: onTouchEvent: ACTION_UP res=true
分析:
down事件最后被MyTextView消费了。第一个move事件被ListView拦截了。后续的事件都交给ListView处理了。
注意一下,在上面log种第一个move事件的左右滑动delta_X=3,delta_Y=23。 这导致了listview在onInterceptTouchEvent中调用了parent.requestDisallowInterceptTouchEvent(true)所以后续的move事件再经过HorizontalScrollViewEx时不再调用onInterceptTouchEvent了。
3.2 左右滑动
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_DOWN
HorizontalScrollViewEx: onInterceptTouchEvent: DOWN intercepted=false
MyListView: dispatchTouchEvent: ACTION_DOWN
MyListView: onInterceptTouchEvent: ACTION_DOWN res=false
MyTextView: onTouchEvent: ACTION_DOWN res=true
MyTextView: dispatchTouchEvent: ACTION_DOWN res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
HorizontalScrollViewEx: onInterceptTouchEvent: MOVE Math.abs(deltaX) = 14 Math.abs(deltaY)= 1intercepted=true
MyListView: dispatchTouchEvent: ACTION_CANCEL
MyListView: onInterceptTouchEvent: ACTION_CANCEL res=false
MyTextView: onTouchEvent: ACTION_CANCEL res=true
MyTextView: dispatchTouchEvent: ACTION_CANCEL res=true
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
HorizontalScrollViewEx: onTouchEvent: MOVE --- Math.abs(deltaX) = 40 Math.abs(deltaY)= 2
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
HorizontalScrollViewEx: onTouchEvent: MOVE --- Math.abs(deltaX) = 39 Math.abs(deltaY)= 1
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_MOVE
HorizontalScrollViewEx: onTouchEvent: MOVE --- Math.abs(deltaX) = 95 Math.abs(deltaY)= 29
HorizontalScrollViewEx: dispatchTouchEvent: ACTION_UP
HorizontalScrollViewEx: onTouchEvent: UP
down事件最后被MyTextView消费了。第一个move事件被HorizontalScrollViewEx拦截了,所以HorizontalScrollViewEx分发了ACTION_CANCLE事件给ListView,ListView又把事件分发给MyTextView消费了。后续的move或其他事件就直接被HorizontalScrollViewEx自己处理了。