在做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自己处理了。