嘿~来说说Android点击事件分发和处理

先说个小事情

onXXXXXX()方法都是对当前View的某个操作进行实际的处理。比如,onDraw()是对View的实际绘制,onMeasure()是对View进行实际的测量,onLayout()是进行实际的布局,onTouchEvent()是对点击事件进行处理,onInterceptTouchEvent()是对是否拦截事件进行处理。

再说一个小事情

点击事件正常情况下就4个类型,一般处理这4个类型就可以了

  • MotionEvent.ACTION_DOWN 按下
  • MotionEvent.ACTION_UP 抬起
  • MotionEvent.ACTION_MOVE 移动
  • MotionEvent.ACTION_CANCEL 非人为取消,在事件分发过程中产生

现在开始说正事,在我看来点击事件的分发处理其实是两块不同的内容。

点击事件分发主要涉及的函数:
  • Activity.dispatchTouchEvent()
  • ViewGroup.dispatchTouchEvent()
  • View.dispatchTouchEvent()

点击事件分发机制的函数基本上是不会被重写的,因为这个是它内部已经规定好的机制。

点击事件处理主要涉及的函数:
  • View.onTouchEvent()

点击事件处理机制的函数就经常需要被重写。很多自定义ViewGroup或者自定义View的时候会去重写onTouchEvent()方法。

点击事件分发

分发的顺序是Activity->ViewGroup->View,其中ViewGroup中的分发逻辑最为复杂。

Activity事件分发机制

Activity的分发机制相对比较简单

//Activity.class
//
public boolean dispatchTouchEvent(MotionEvent ev) {

    //这里告诉你就是调用了ViewGroup.dispatchTouchEvent()方法,如果想知道为啥下面会稍微的解释一下
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

复制代码
听我解释一:
  • getWindow()获取的是Window对象是一个抽象类,它的实现类是PhoneWindow
  • PhoneWindow实现superDispatchTouchEvent()方法,调用了mDecor.superDispatchTouchEvent()方法。
  • mDecorDecorView的一个实例。
  • DecorView继承自FrameLayout,而FrameLayout继承自ViewGroup
  • 好了,最后就是getWindow().superDispatchTouchEvent(ev)调用的就是ViewGroup.dispatchTouchEvent()方法。
View事件分发机制
//View.class
//
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    ListenerInfo li = mListenerInfo;

    if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    ......
    return result;
}
复制代码
听我解释一:
  • View中的事件分发其实很简单,他的分发就是分发给自己的那些方法去处理消费掉;
  • 第一是给mOnTouchListener.onTouch(),如果没有消费掉就再调用View.onTouchEvent()
  • 反正View的事件分发机制中的核心就是“我要消费掉这个事件”
ViewGroup事件分发机制

先记住几样东西。

第一、变量mFirstTouchTarget是接收点击事件的目标View链表。
//ViewGroup.class
//
mFirstTouchTarget
复制代码
第二、方法cancelAndClearTouchTargets()向取消View上事件,并且清除。
//ViewGroup.class
//
private void cancelAndClearTouchTargets(MotionEvent event) {
    //在mFirstTouchTarget
    if (mFirstTouchTarget != null) {
        ......
        //就是各种操作,取消事件,并且清除
    }
}
复制代码
第三、addTouchTarget()向目标View的链表中增加新的目标View。
//ViewGroup.class
//
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
复制代码
第四、dispatchTransformedTouchEvent()分发并转化点击事件,如果有child参数就分发给儿子,如果没有child参数为null就调用super.dispatchTouchEvent()
//ViewGroup.class
//
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
        ......
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        ......
    return handled;
}
复制代码

好了,记好了这四个东西了吗?如果忘了在回去看一遍,接下来会有用。 整个事件分发最复杂的部分来了,我们先看个大概的过程。

//ViewGroup.class
//
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
   
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        
        // 判断是否被截获
        // 主要判断disallowIntercept参数和onInterceptTouchEvent()方法
        // 其中有一个true就是截获事件,具体相关内容
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
          intercepted = true;
        }

         //不被取消,不被截获的时候,就继续向子View分发,看这里只处理ACTION_DOWN事件
        if (!canceled && !intercepted) {
            ......
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    ......
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        ......
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ......
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                           
                            break;
                        }

                       ......
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                ......
            }
        }

        //把事件分发到目标View
        if (mFirstTouchTarget == null) {
        //这里如果没有一个接收Touch事件的View,就自己尝试消化掉
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            //这里就是向目标View分发事件
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                ......
             //根据mFirstTouchTarget链表分发事件
            }
        }
    }

    return handled;
}
复制代码
听我解释一:
  • 先后判断disallowIntercept变量和onInterceptTouchEvent()方法,如果是true就表示被截获,如果是false就表示没被截获;
  • disallowIntercept表示是否禁用拦截功能,默认是false。可以通过requestDisallowInterceptTouchEvent()方法设置为true
  • requestDisallowInterceptTouchEvent()方法是需要用户自己调用的,而且如果设置为true,那么它的父容器都是true
  • onInterceptTouchEvent()方法默认是不截获。
听我解释二:
  • canceledintercepted都为false的时候,就会先向子View分发ACTION_DWON事件;
  • 如果子View中的dispatchTransformedTouchEvent()方法返回true的时候,就会调用addTouchTarget()方法;
  • addTouchTarget ()方法中就是在mFirstTouchTarget中增加接收点击事件的View
听我解释三:
  • 什么叫被截获,你可以理解mFirstTouchTarget==null就是被截获了;
  • 如果被截获,就会调用dispatchTransformedTouchEvent()方法,参数child是null;
  • 回想一下刚刚要记住的方法“如果没有child参数为null就调用super.dispatchTouchEvent()
  • 如果没有被截获,就调用dispatchTransformedTouchEvent()方法,参数child就是子View;
  • 这样就会继续向下分发后续点击事件。

处理机制

真正的事件处理其实只有一个函数就是onTouchEvent() 而且记住只要到DOWN事件的时候返回true,那么接下来的事件都会在这个onTouchEvent()里处理。

为啥是这样,请看ViewGroup的听我解释二听我解释三

public boolean onTouchEvent(MotionEvent event) {
    ......
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                //抬起,执行点击,长按等效果
                ......
                break;
            case MotionEvent.ACTION_DOWN:
                //按下,然后进行点击必要的设置,点击状态设置,长按状态设置
                ......
                break;
            case MotionEvent.ACTION_CANCEL:
                //取消,取消和重置按下时的效果
                ......
                break;
            case MotionEvent.ACTION_MOVE:
                //移动,取消一些效果,比如长按
                break;
        }

        return true;
    }

    return false;
}
复制代码

我们在做自定View的时候如果涉及事件反馈的问题都会重写onEventTouch()

看两个例子感受一下
第一个是SeekBar对触摸事件的处理,来看一下:
//AbsSeekBar.class
//
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!mIsUserSeekable || !isEnabled()) {
        return false;
    }

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (isInScrollingContainer()) {
                mTouchDownX = event.getX();
            } else {
                startDrag(event);
            }
            break;

        case MotionEvent.ACTION_MOVE:
            if (mIsDragging) {
                trackTouchEvent(event);
            } else {
                final float x = event.getX();
                if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
                    startDrag(event);
                }
            }
            break;

        case MotionEvent.ACTION_UP:
            if (mIsDragging) {
                trackTouchEvent(event);
                onStopTrackingTouch();
                setPressed(false);
            } else {
                // Touch up when we never crossed the touch slop threshold should
                // be interpreted as a tap-seek to that location.
                onStartTrackingTouch();
                trackTouchEvent(event);
                onStopTrackingTouch();
            }
            
            invalidate();
            break;

        case MotionEvent.ACTION_CANCEL:
            if (mIsDragging) {
                onStopTrackingTouch();
                setPressed(false);
            }
            invalidate(); // see above explanation
            break;
    }
    return true;
}
复制代码
第二个是ScrollView对触摸事件的处理,来看一下:
//ScrollView.class
//
@Override
public boolean onTouchEvent(MotionEvent ev) {
    initVelocityTrackerIfNotExists();

    MotionEvent vtev = MotionEvent.obtain(ev);

    final int actionMasked = ev.getActionMasked();

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mNestedYOffset = 0;
    }
    vtev.offsetLocation(0, mNestedYOffset);

    switch (actionMasked) {
        case MotionEvent.ACTION_DOWN: {
            if (getChildCount() == 0) {
                return false;
            }
            if ((mIsBeingDragged = !mScroller.isFinished())) {
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }

            /*
             * If being flinged and user touches, stop the fling. isFinished
             * will be false if being flinged.
             */
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                if (mFlingStrictSpan != null) {
                    mFlingStrictSpan.finish();
                    mFlingStrictSpan = null;
                }
            }

            // Remember where the motion event started
            mLastMotionY = (int) ev.getY();
            mActivePointerId = ev.getPointerId(0);
            startNestedScroll(SCROLL_AXIS_VERTICAL);
            break;
        }
        case MotionEvent.ACTION_MOVE:
            final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
            if (activePointerIndex == -1) {
                Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                break;
            }

            final int y = (int) ev.getY(activePointerIndex);
            int deltaY = mLastMotionY - y;
            if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                deltaY -= mScrollConsumed[1];
                vtev.offsetLocation(0, mScrollOffset[1]);
                mNestedYOffset += mScrollOffset[1];
            }
            if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                final ViewParent parent = getParent();
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
                mIsBeingDragged = true;
                if (deltaY > 0) {
                    deltaY -= mTouchSlop;
                } else {
                    deltaY += mTouchSlop;
                }
            }
            if (mIsBeingDragged) {
                // Scroll to follow the motion event
                mLastMotionY = y - mScrollOffset[1];

                final int oldY = mScrollY;
                final int range = getScrollRange();
                final int overscrollMode = getOverScrollMode();
                boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                // Calling overScrollBy will call onOverScrolled, which
                // calls onScrollChanged if applicable.
                if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
                        && !hasNestedScrollingParent()) {
                    // Break our velocity if we hit a scroll barrier.
                    mVelocityTracker.clear();
                }

                final int scrolledDeltaY = mScrollY - oldY;
                final int unconsumedY = deltaY - scrolledDeltaY;
                if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                    mLastMotionY -= mScrollOffset[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                } else if (canOverscroll) {
                    final int pulledToY = oldY + deltaY;
                    if (pulledToY < 0) {
                        mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                                ev.getX(activePointerIndex) / getWidth());
                        if (!mEdgeGlowBottom.isFinished()) {
                            mEdgeGlowBottom.onRelease();
                        }
                    } else if (pulledToY > range) {
                        mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                                1.f - ev.getX(activePointerIndex) / getWidth());
                        if (!mEdgeGlowTop.isFinished()) {
                            mEdgeGlowTop.onRelease();
                        }
                    }
                    if (mEdgeGlowTop != null
                            && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
                        postInvalidateOnAnimation();
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);

                if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                    flingWithNestedDispatch(-initialVelocity);
                } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
                        getScrollRange())) {
                    postInvalidateOnAnimation();
                }

                mActivePointerId = INVALID_POINTER;
                endDrag();
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            if (mIsBeingDragged && getChildCount() > 0) {
                if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
                    postInvalidateOnAnimation();
                }
                mActivePointerId = INVALID_POINTER;
                endDrag();
            }
            break;
        case MotionEvent.ACTION_POINTER_DOWN: {
            final int index = ev.getActionIndex();
            mLastMotionY = (int) ev.getY(index);
            mActivePointerId = ev.getPointerId(index);
            break;
        }
        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
            break;
    }

    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(vtev);
    }
    vtev.recycle();
    return true;
}
复制代码

总结一下

  • 事件分发和事件处理是两回事,可以分开了讨论;
  • 事件分发主要是理解里面的分发逻辑,而事件处理主要是针对自己的需求进行重写onTouchEvent()方法;
  • 分发中最难的是ViewGroup中的分发,记住里面的mFirstTouchTarget参数的处理。

其他还不错的文章

Android事件分发机制详解:史上最全面、最易懂

HenCoder 3-1 触摸反馈,以及 HenCoder Plus



我叫陆大旭。

一个懂点心理学的无聊程序员大叔。
看完文章无论有没有收获,记得打赏、关注和点赞!

转载于:https://juejin.im/post/5b7b880de51d4538e331897b

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值