Android系列之 屏幕触控机制(三)

目录

1 Android native层传递点击事件

2 .Window和WindowManagerService的关系

2.1 ViewRootImpl 如何串联Window和WindowManagerService

2.2 ViewRootImpl和Window的关系

2.3 ViewRootImpl和WindowManagerService的关系

3 ViewRootImpl的事件接收及分发

以上参考前面的文章

[4 ViewGroup事件的分发机制](#4 ViewGroup事件的分发机制)

[4.1 activity的视图创建流程](#4.1 activity的视图创建流程)

[4.2 ViewGroup事件的分发机制](#4.2 ViewGroup事件的分发机制)

[4.3 View的事件处理](#4.3 View的事件处理)

[4 .4 ViewGroup事件的分发机制](#4 .4 ViewGroup事件的分发机制)

[5 事件分发具体案例及解决方案](#5 事件分发具体案例及解决方案)


4 ViewGroup事件的分发机制

ViewRootImpl传递事件时我们讲过最终会调用mView.dispatchPointerEvent(event),那这个mView是什么呢,其实他就是DecorView。

因为之前咱们讲过,ViewRootImpl在setView的时候第一个参数view 就是DecorView。

ViewRootImpl将点击事件传递给DecorView后,DecorView回调Activity实现的Window.Callback接口传递到Activity,Activity又通过getWindow传递给DecorView,之后具体分发到下面的ViewGroup。

所以传递的机制是:ViewRootImpl->DecorView->Activity->Window->DecorView-> ViewGroup

图1

4.1 activity的视图创建流程

那么有人就有疑问了,点击事件传递给DecorView后,为什么会调到activity这里呢,这就涉及到android view的创建绘制流程了。

做过android 应用层的小伙伴都知道,只要是android应用,都逃不开activity,咱们就以activity为例进行讲解,先上图 右侧是activity 的整个视图关系

图2

Activity 在调用setContentView()方法设置布局时,其实是调用

getwindow(). setContentView()的方法 ,

java 复制代码
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里的getwindow()就是window,但是window是一个抽象类,phoneWindow 是他的子类,所以window的最终实现类是phoneWindow,所以最终会调到phoneWindow的setContentView()方法中,

java 复制代码
@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
    mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
}

mLayoutInflater.inflate()函数我们就很熟悉了,它会把布局layoutResID填充到根布局mContentParent里面,但是这个mContentParent 是怎么来的呢

我们看installDecor这个方法中,创建了一个DecorView,并在下方初始化了一个mContentParent,

java 复制代码
private void installDecor() {
        //创建了一个DecorView
        mDecor = generateDecor(-1);
        ...
        if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        ...
}

generateLayout()方法中,会将layoutResource加载到DecorView去,并最终返回一个connentParent,它是id 为ID_ANDROID_CONTENT的ViewGroup

java 复制代码
protected ViewGroup generateLayout(DecorView decor) {
    ...
    int layoutResource;
    //会根据一些设置去配置布局
    if (......) {
    ...

    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

    //比如这里我们常设置不需要Title的Activity就会走这里
     requestWindowFeature(Window.FEATURE_NO_TITLE);
     layoutResource = R.layout.screen_title;
    } else {
        // Embedded, so no decoration is needed.
        layoutResource = R.layout.screen_simple;
    }
    mDecor.startChanging();
    //将layoutResource加载到DecorView
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    ...
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ...
    return contentParent;
}

而这个ID_ANDROID_CONTENT经过查看,是在默认布局R.layout.screen_simple中的FrameLayout

到这里我们就可以得到结论: 我们在activity 中方法里常写的setContentView(layoutResID)最终会填充到FrameLayout的布局中去。而这个是放在一个竖直方向的LinearLayout里面。

XML 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

那这整个最外层的layout要放在哪里呢?最终会在mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)方法中通过addView方法填充到DecorView中去。这也侧面说明了,为什么是DecorView先响应的点击事件。

java 复制代码
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    ......
    final View root = inflater.inflate(layoutResource, null);
    ......
    addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    ......
}

图3

由此可以得出一个结论, 在Activity里设置的布局会添加到window上,而在window里布局最终会添加到DecorView,所以它也顺理成章成为了整个View树的顶级View。从这里也可以得出一个结论: Activity并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是Window.

至此,咱们就可以理解为什么点击事件的传递是DecorView -> Activity -> PhoneWindow -> DecorView了,

是为了解耦! ViewRootImpl并不知道有Activity这种东西存在!它只是持有了DecorView。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent();而Activity需要响应dispatchTouchEvent 是因为可以直接在activity层或者dialog 层做直接的一些处理工作,比如说dialog的点击屏幕外自动miss的功能,

那么,既然触摸事件已经到了Activity.dispatchTouchEvent()中了,为什么不直接分发给DecorView,而是要通过PhoneWindow来间接发送呢?因为Activity不知道有DecorView!但是,Activity持有PhoneWindow ,而PhoneWindow当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView。在Android中,Activity并不知道自己的Window中有些什么,这样耦合性就很低了。就是不一定需要activity ,只要有能让window依赖的 也可以响应这个点击事件。

4.2 ViewGroup事件的分发机制

下面从源码角度来看一下具体的分发流程

DecorView收到viewrootimpl的点击事件,通过callback传递到Activity,这里的callback是Activity实现的Window的callback接口。

java 复制代码
DecorView.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

这个是Activity接收DecorView回调的点击事件,

刚开始会执行onUserInteraction() 这个方法,该方法为空方法,我们可以重写这个方法,达到监听整个事件序列的开始。

之后通过getWindow().superDispatchTouchEvent(ev)这个方法可以看出来,这个时候Activity又会将事件交由Window处理。Window它是一个抽象类,它的具体实现是PhoneWindow,也就是说这个时候,Activity将事件交由PhoneWindow中的superDispatchTouchEvent方法。现在跟踪进去看一下这个superDispatchTouchEvent代码。

java 复制代码
Activity.java

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

PhoneWindow中 这里面的mDecor它是一个DecorView,DecorView它是一个Activity的顶级View。是PhoneWindow的一个内部类,继承自FrameLayout

于是在这个时候事件又交由DecorView的superDispatchTouchEvent方法来处理。我们能够很清晰的看到DecorView它调用了父类的dipatchTouchEvent方法。在上面说到DecorView它继承了FrameLayout,而这个FrameLayout又继承自ViewGroup。所以在这个时候事件就开始交给了ViewGroup进行处理了。

java 复制代码
//=============PhoneWindow.java==========
......
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

...... //===========DecorView.java==========
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

....
//========ViewGroup.java=======
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
}

当事件传递至ViewGroup 后, ViewGroup就开始做事件分发,

事件分发机制主要由事件分发 ->事件拦截 ->事件响应三步来进行逻辑控制 分别对应,

dispatchTouchEvent(MotionEvent ev)

onInterceptTouchEvent(MotionEvent ev)

onTouchEvent(MotionEvent ev)

那么这三个方法的功能究竟是什么呢?这里可以看看下面的表格的总结

图4

可以看到dispatchTouchEvent方法是整个事件派发的主要方法,在分发的过程中需要进行事件拦截onInterceptTouchEvent()和事件响应onTouchEvent()。

Activity 和View是没有事件拦截onInterceptTouchEvent()方法的,因为这个onInterceptTouchEvent()是ViewGroup独有的类,Activity因为不是视图类,所以没有这个方法,而View是最终的子类,他没有子View,不需要拦截事件。

onTouchEvent()方法就是最终的事件响应的方法

下面就开始详细看下这个ViewGroup的dispatchTouchEvent方法。由于dispatchTouchEvent代码比较长,在这里就摘取部分代码进行说明。 在dispatchTouchEvent()中,会先对接收的事件进行判断,是ACTION_DOWN事件时,便会清空事件分发的目标和状态。然后执行resetTouchState方法重置触摸状态

// ACTION_DOWN事件,表示这是一个全新的事件序列,会清除所有的touchTarget,重置所有状态

java 复制代码
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

cancelAndClearTouchTargets(ev);方法主要是清空mFirstTouchTarget链表并将mFirstTouchTarget设为空。

java 复制代码
private void cancelAndClearTouchTargets(MotionEvent event) {
    if (mFirstTouchTarget != null) {
        ......
        clearTouchTargets();
        ......
    }
}

resetTouchState();清除之前记录的信息,在这里介绍一下FLAG_DISALLOW_INTERCEPT标记,这是禁止ViewGroup拦截事件的标记,可以通过requestDisallowInterceptTouchEvent方法来设置这个标记,当设置了这个标记以后,ViewGroup便无法拦截除了ACTION_DOWN以外的其它事件。因为在上面代码中可以看出,当事件为ACTION_DOWN时,会重置FLAG_DISALLOW_INTERCEPT标记。

java 复制代码
private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

FLAG_DISALLOW_INTERCEPT是子View通过requestDisallowInterceptTouchEvent(true)来设置的,可以用来做内部拦截操作

下面来看一下dispatchTouchEvent方法中的事件拦截判断,可以看到根据actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null这两个情况进行判断事件是否需要拦截。

java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
        // 判断是否需要拦截.
        final boolean intercepted;

        // down事件或者Target中有子view的非down事件则需要判断是否需要拦截
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // 此标志为子view通过requestDisallowIntercept方法设置
            // 禁止viewGroup拦截事件
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // viewGroup是否调用了此方法拦截点击事件
                intercepted = onInterceptTouchEvent(ev); 
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
               intercepted = true;
        }
        ......
}

mFirstTouchTarget!=null的情况, 当事件没有被拦截时,ViewGroup的子元素成功处理事件后,mFirstTouchTarget会被赋值并且指向其子元素。也就是说这个时候mFirstTouchTarget!=null。可是一旦事件被拦截,mFirstTouchTarget不会被赋值,mFirstTouchTarget也就为null。

而这个事件拦截判断里有一个之前提到的重要方法 onInterceptTouchEvent() ,下面我们来看一下源码

java 复制代码
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

if语句看起来是和鼠标有关系的,貌似和咱们的平时使用没有什么关系,所以除去这个判断语句,其实就是返回true和false的区别,

如果return true, 说明需要拦截此次事件,

return false和返回默认的super.onInterceptTouchEvent是一样的 ,都是不拦截此次事件。

所以如果我们想要在中间某一层拦截点击事件的话,直接重写它的onInterceptTouchEvent()方法,返回true就可以达到拦截点击事件的目的。

下面咱们来看一下dispatchTouchEvent()方法,对于这个方法来说代码虽然比较长,但是这里面的逻辑却不是很复杂。首先获取当前ViewGroup中子View的数量。然后对该ViewGroup中的元素进行逐步遍历。在获取到ViewGroup中的子元素后,判断该元素是否能够接收触摸事件。子元素若是能够接收触摸事件,并且该触摸坐标在子元素的可视范围内的话,便继续向下执行。否则就continue。对于衡量子元素能否接收到触摸事件的标准有两个:子元素是否可以接受触摸事件和点击事件的坐标是否在子元素的区域内。

java 复制代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    // 对遮盖状态进行过滤 两个标志,前者表示当被覆盖时不处理;后者表示当前窗口是否被非全屏窗口覆盖
    if (onFilterTouchEventForSecurity(ev)) {
        拦截事件判断
               ......
        if (!canceled && !intercepted) {// 找消费down事件的子控件  如果没有取消和拦截进入分发
               ......
            if (actionMasked == MotionEvent.ACTION_DOWN // down或pointer_down事件,表示新的手指按下了,需要寻找接收事件的view
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) // ACTION_POINTER_DOWN 多指按下
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// ACTION_HOVER_MOVE 鼠标移动事件 暂时忽略
                              ......
               final int childrenCount = mChildrenCount; // 获取子控件的数量  如果子控件为0 那就不需要遍历,只给ViewGroup自己处理
               if (newTouchTarget == null && childrenCount != 0) {

                       ...... 
                    for (int i = childrenCount - 1; i >= 0; i--) {// 遍历所有子控件 寻找能够接收该事件的View
                        ......
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {//检查该子view是否可以接受触摸事件和是否在点击的范围内
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                              ......                       
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 执行事件派发的关键方法
                              ......
                            newTouchTarget = addTouchTarget(child, idBitsToAssign); // 有子view 消费了点击事件 设置mFirstTouchTarget
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ......
                }
        // 经过了前面的处理,到这里touchTarget依旧为null,说明没有找到处理down事件的子控件 或者down事件被viewGroup本身消费了,所以该事件由viewGroup自己处理
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        ......
    return handled;
}

一旦子View接收到了触摸事件,然后便开始调用dispatchTransformedTouchEvent方法对事件进行分发处理。对于dispatchTransformedTouchEvent方法代码比较多,只关注handled = super.dispatchTouchEvent(transformedEvent);和handled = child.dispatchTouchEvent(transformedEvent);两个方法就可以了。

调用handled = super.dispatchTouchEvent(transformedEvent);说明当前view没有子view对事件进行捕获操作,自己消费事件最终会调用自身的onTouchEvent方法,捕捉触摸事件。

调用handled = child.dispatchTouchEvent(transformedEvent);说明有子view 对事件进行了捕获操作,进入子view 的dispatchTouchEvent方法。

java 复制代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    // 如果是取消事件,那么不需要做其他额外的操作,直接派发事件即可,然后直接返回
    // 因为对于取消事件最重要的内容就是事件本身,无需对事件的内容进行设置
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    ......
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        // child为空 自己消费事件
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
    ......       

    // 调用子view的方法进行分发
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    ......
    return handled;
}

4.3 View的事件处理

点击事件分发到最底层的子view后,最底层的view开始响应点击事件,下面咱们来查看下最底层view的响应事件流程。

先检查是否是可点击区域,并且没有被遮盖,再检测有没有响应onTouch方法,最后会走到onTouchEvent方法进行触摸事件onLongClick() 和onClick()处理。

java 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    // 安全过滤 是否处于可以点区域  没有被遮盖
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) { // 如果用户实现了onTouch方法 这边就会调用
           result = true;
        }

        // 如果用户没有实现onTouch 方法 就会响应onTouchEvent方法
        // onTouchEvent()这个方法是view处理事件的核心,里面包含了点击、双击、长按等逻辑的处理
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ......
    return result;
}
java 复制代码
public boolean onTouchEvent(MotionEvent event) {
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 判断是否是可点击的
    if ((viewFlags & ENABLED_MASK) == DISABLED) {// 一个被禁用的view如果被设置为clickable,那么他仍旧是可以消费事件的
        ......
        return clickable;
    }
               ......
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {// 判断是否是可点击的  或者长按会出现工具提示
        switch (action) {
            case MotionEvent.ACTION_UP:
                    ......
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {// 两个参数分别是:长按事件是否已经响应、是否忽略本次up事件
                        removeLongPressCallback(); // 这是一个单击事件,还没到达长按的时间,移除长按标志
                        if (!post(mPerformClick)) {// 调用点击
                            performClickInternal();
                    ......               

                    break;
            case MotionEvent.ACTION_DOWN:
                  ......
                mHasPerformedLongPress = false; // 标志是否是长按
                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }
                   ......
                boolean isInScrollingContainer = isInScrollingContainer();
                if (isInScrollingContainer) {// 如果在一个可滑动的容器中,那么需要延迟一小会再响应反馈
                  .......
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    setPressed(true, x, y); // 没有在可滑动的容器中,直接响应触摸反馈
                    checkForLongClick(0, x, y);
                }
                break;
            case MotionEvent.ACTION_CANCEL: //取消事件,恢复所有的状态
                ......
                break;
            case MotionEvent.ACTION_MOVE: // 移动事件
                ......               

                break;
        }
        return true;
    }
    return false;
}

View 的点击事件执行顺序是 onTouch() --> onTouchEvent() --> onLongClick() --> onClick()

其中onLongClick()方法最终是在performLongClickInternal()方法中调用的

java 复制代码
private boolean performLongClickInternal(float x, float y) {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

    boolean handled = false;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLongClickListener != null) {
        handled = li.mOnLongClickListener.onLongClick(View.this); // 响应用户的长按点击事件
    }
    ......   
    return handled;
}

onClick()方法最终是在performClick()方法中调用的

java 复制代码
public boolean performClick() {
......
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this); // 用户的短按事件
        result = true;
    } else {
        result = false;
    }
......
    return result;
}

4 .4 ViewGroup事件的分发机制

至此,一个正常的点击事件就结束了

最后这里再来总结一下:

触摸事件,从屏幕产生后,经过系统服务的处理,最终会发送到viewRootImpl来进行分发;

viewRootImpl会调用它所管理的view的 dispatchTouchEvent 方法来分发事件,这个view最终指向的就是viewGroup ,viewGroup会为每个触控点尽量寻找感兴趣的子view,最后再自己处理事件。其实就是把事件按照点击谁,谁响应的原则精准的分发给他的子view。

ViewGroup的DispatchTouchEvent收到触摸事件后,会询问onInterceptTouchEvent(注意, onInterceptTouchEvent是ViewGroup类型的View独有的方法)是否需要拦截此次触摸事件?如果拦截返回true,事件将会转到自己的onTouchEvent中处理。如果不拦截,事件将传到它的子View,下一层的ViewGroup的DispatchTouchEvent 。

下一层的ViewGroup的DispatchTouchEvent被调用后,会询问自己的onInterceptTouchEvent是否需要拦截此次触摸事件?如果拦截则返回true,事件将会转到自己的onTouchEvent中处理。如果不拦截,事件将传到它的子View,下一层的ViewGroup的DispatchTouchEvent 。

事件一层一层传递,如果中间的viewGroup都没有拦截的话,最后触摸事件会传递到最终要响应的View的 dispatchTouchEvent 方法中。由于view没有拦截器,事件将会直接由DispatchTouchEvent分发到onTouchEvent中。

最终要响应的view首先会调用onTouchListener,查看用户有没有响应onTouch方法,如果重写了onTouch方法并且返回true,此次触摸事件的传递到此结束,不会在往下传了。如果返回false, 就会去调用view的onTouchEvent()方法处理此次事件,View的onTouchEvent的默认实现中的主要任务就是辨别单击与长按事件,并回调onClickListener与onLongClickListener。如果最终view处理了,就会返回true,此次触摸事件的传递到此结束。如果onTouchEvent()不处理点击事件返回false,那么事件将回传到它的父级的onTouchEvent中。

事实上,不管哪个层级的onTouchEvent ,如果处理事件返回true,一次触摸事件就结束了。如果不处理事件,返回false,就会传送到上一级的onTouchEvent中。最终,如果DecorView的onTouchEvent也不打算处理事件,那么事件将会被发送到Activity的onTouchEvent中处理

图5

5 事件分发具体案例及解决方案

现在给大家举一个因为事件分发造成的滑动冲突案例,以及解决办法。

示例外层为一个ScrollView,内层为TextView+ListView+TextView,这两个TextView分别为"Tittle"和"Bottom",显示在ListView的顶部和底部,添加它们是为了方便观察ScrollView的滑动效果。最终的布局效果如下所示:

在没有解决冲突前,如果滑动中间的ListView部分,会出现ListView中的列表内容不会滑动,而是整个ScrollView滑动的现象。

因为ScrollView默认在move手势的时候 重写了onInterceptTouchEvent方法,拦截了滑动事件,而我们希望看到的是,如果ListView滑到顶部时,而且手势继续下滑时,整个页面下滑,即ScrollView滑动;如果ListView滑到底部了,而且手势继续上滑时,希望整个页面上滑,即也是ScrollView向上滑动。

我们可以用重写外层ScrollView的onInterceptTouchEvent()方法在一定条件下拦截事件来实现这个功能。

图6

如图,只需自定义一个Scrollview,继承系统的ScrollView,之后重写ScrollView的onInterceptTouchEvent()方法,在ACTION_MOVE的时候,判断那个控件响应事件即可。

在这里,首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。

其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。

主要的判断逻辑在move事件内。

java 复制代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    super.onInterceptTouchEvent(ev);
    boolean intercept = false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            intercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            listView = (ListView) ((ViewGroup)getChildAt(0)).getChildAt(1);
            //ListView滑动到顶部,且继续下滑,让scrollView拦截事件
            if (listView.getFirstVisiblePosition() == 0 && (ev.getY() - mLastY) > 0) {
                //scrollView拦截事件
                intercept = true;
            }
            //listView滑动到底部,如果继续上滑,就让scrollView拦截事件
            else if (listView.getLastVisiblePosition() ==listView.getCount() - 1 && (ev.getY() - mLastY) < 0) {
                //scrollView拦截事件
                intercept = true;
            } else {
                //不允许scrollView拦截事件
                intercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercept = false;
            break;
        default:
            break;
    }
    mLastY = ev.getY();
    Log.e("状态", intercept + "");
    return intercept;
}
相关推荐
恋猫de小郭1 天前
你是不是觉得 R8 很讨厌,但 Android 为什么选择 R8 ?也许你对 R8 还不够了解
android·前端·flutter
城东米粉儿1 天前
Android Glide 笔记
android
城东米粉儿1 天前
Android TheRouter 笔记
android
城东米粉儿1 天前
Android AIDL 笔记
android
城东米粉儿1 天前
Android 进程间传递大数据 笔记
android
城东米粉儿1 天前
Android KMP 笔记
android
冬奇Lab1 天前
WMS核心机制:窗口管理与层级控制深度解析
android·源码阅读
松仔log1 天前
JetPack——Paging
android·rxjava
城东米粉儿1 天前
Android Kotlin DSL 笔记
android