美文网首页Android开发经验谈Android开发最近需要做的
嘿~来说说Android点击事件分发和处理

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

作者: 我叫陆大旭 | 来源:发表于2018-08-20 17:06 被阅读126次
    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();
                }
                // ProgressBar doesn't know to repaint the thumb drawable
                // in its inactive state when the touch stops (because the
                // value has not apparently changed)
                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


    我叫陆大旭。

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

    相关文章

      网友评论

        本文标题:嘿~来说说Android点击事件分发和处理

        本文链接:https://www.haomeiwen.com/subject/onmxbftx.html