从源码解析View的事件分发机制

本文详细探讨了Android中View的事件分发机制,从源码层面解析了Activity的构成,特别是如何通过setContentView()加载布局。接着,重点讨论了View的事件分发,包括点击事件的传递规则,涉及dispatchTouchEvent(), onInterceptTouchEvent()和onTouchEvent()这三个关键方法的作用。文章还介绍了事件由上而下和由下而上的传递过程,帮助理解Android中事件处理的流程。" 84722271,8142414,Linux内核TCP/IP参数优化详解,"['操作系统', '网络', 'shell']
摘要由CSDN通过智能技术生成

Android、View视图与坐标系
View的滑动和属性动画
从源码解析View的事件分发机制
View的工作流程
Android自定义view

View的事件分发机制

  在讲到View的事件分发机制之前要首先了解
一下Activity的组成,然后从源码的角度来分析View的事件分发机制。

源码解析Activity的构成

  点击事件用MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity,所以我们首先要了解一下Activity的构成。当我们写Activity时会调用setContentView()方法来加载布局。现在来看看
setContentView()方法是怎么实现的,代码如下所示:

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

  这里调用了getWindow().setContentView(layoutResID)getWindow()指的是什么呢?接着往下看,getWindow()返回mWindow

    public Window getWindow() {
        return mWindow;
    }

  那这个mWindow又是什么呢?我们继续查看代码,最终在Activityattach()方法中发现了mWindow,代码如下所示:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
    attachBaseContext(context);
    mFragments.attachHost(null /*parent*/);
    mWindow = new PhoneWindow(this);
    ...
}

  而getWindow()又指的是PhoneWindow。所以来看看PhoneWindowsetContentView()方法,代码如下所示:

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();//1
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, 
        layoutResID, getContext());
            transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}

  原来mWindow指的就是PhoneWindowPhoneWindow是继承抽象类Window的,这样就知道了getWindow()得到的是一个PhoneWindow,因为ActivitysetContentView()方法调用的是getWindow().setContentView(layoutResID)

  挑关键的接着看,看看上面代码注释1处installDecor()方法里面做了什么,代码如下所示:

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        mDecor = generateDecor();//1
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        ...
    }
    ...
}    

  在前面的代码中没发现什么,紧接着查看上面代码注释1处的generateDecor方法里做了什么:

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

  这里创建了一个DecorView,这个DecorView就是Activity中的根View。接着查看DecorView的源码,发现DecorViewPhoneWindow类的内部类,并且继承了FrameLayout。我们再回到installDecor()方法中,
查看generateLayout(mDecor)做了什么:

protected ViewGroup generateLayout(DecorView decor) {
    ...
    int layoutResource;
    int features = getLocalFeatures();
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
        layoutResource = R.layout.screen_swipe_dismiss;
        setCloseOnSwipeEnabled(true);
    } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_title_icons;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
        && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
        layoutResource = R.layout.screen_progress;
    } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
        } else {
            layoutResource = R.layout.screen_custom_title;
        }
        removeFeature(FEATURE_ACTION_BAR);
    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
        if (mIsFloating) {
            TypedValue res = new TypedValue();
            getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
            layoutResource = res.resourceId;
        } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
            layoutResource = a.getResourceId(
            R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);
        } else {
            layoutResource = R.layout.screen_title;
        }
    } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        layoutResource = R.layout.screen_simple;
    }
    ...
}

  PhoneWindowgenerateLayout()方法比较长,这里只截取了一小部分关键的代码,其主要内容就是根据不同的情况加载不同的布局给layoutResource。现在查看上面代码注释1处的布局R.layout.screen_title,这个文件在frameworks,它的代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <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:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

  上面的ViewStub是用来显示Actionbar的。下面的两个FrameLayout:一个是title,用来显示标题;另一
个是content,用来显示内容。看到上面的源码,大家就知道了一个Activity包含一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindowDecorView作为整个应用窗口的根 View,而这个DecorView又将屏幕划分为两个区域:一个是 TitleView,另一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的,
image

图5:Activity的构成

源码解析View的事件分发机制

  当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEventView中的层级传递过程就是点击事件分发。在了解了什么是事件分发后,我们还需要了解事件分发的3个重要方法。点击事件有3个重要的方法,它们分别是:

  • dispatchTouchEvent(MotionEvent ev)——用来进行事件的分发。
  • onInterceptTouchEvent(MotionEvent ev)——用来进行事件的拦截,在dispatchTouchEvent()中调用,
    需要注意的是View没有提供该方法。
  • onTouchEvent(MotionEvent ev)——用来处理点击事件,在dispatchTouchEvent()方法中进行调用。

View的事件分发机制

  当点击事件产生后,事件首先会传递给当前的 Activity,这会调用 ActivitydispatchTouchEvent()方法,当然具体的事件处理工作都是交由Activity中的PhoneWindow来完成的,然后PhoneWindow再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给根ViewGroup。所以,我们从ViewGroup
dispatchTouchEvent()方法开始分析,代码如下所示:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//1
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//2
            if (!disallowIntercept) {
               intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);  
            } else {
                intercepted = false;
            }
        } else {
                intercepted = true;
        }
        ...
    return handled;    
}

  这里首先判断事件是否为DOWN事件,如果是,则进行初始化,resetTouchState方法中会把mFirstTouchTarget的值置为null。这里为什么要进行初始化呢?原因就是一个完整的事件序列是以DOWN开始,以UP结束的。所以如果是DOWN事件,那么说明这是一个新的事件序列,故而需要初始化之前的状态。接着往下看,上面代码注释1处的条件如果满足,则执行下面的句子,mFirstTouchTarget 的意义是:当前 ViewGroup是否拦截了事件,如果拦截了,mFirstTouchTarget=null;如果没有拦截并交由子View来处理,则mFirstTouchTarget!=null。假设当前的 ViewGroup 拦截了此事件,mFirstTouchTarget!=null 则为false,如果这时触发ACTION_DOWN事件,则会执行 onInterceptTouchEvent(ev) 方法;如果触发的是ACTION_MOVEACTION_UP事件,则不再执行onInterceptTouchEvent(ev)方法,而是直接设置
intercepted=true,此后的一个事件序列均由这个
ViewGroup
处理。再往下看,上面代码注释2 处又出现了一个FLAG_DISALLOW_INTERCEPT标志位,它主要是禁止ViewGroup 拦截除了DOWN之外的事件,一般通
过子ViewrequestDisallowInterceptTouchEvent来设置。所以总结一下就是,当ViewGroup要拦截事件的时候,那么后续的事件序列都将交给它处理,而不用再调用onInterceptTouchEvent()方法了。所以,onInterceptTouchEvent()方法并不是每次事件都会调用的。接下来查看onInterceptTouchEvent()方法:

    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;
    }

  onInterceptTouchEvent()方法默认返回false,不进行拦截。如果想要让ViewGroup拦截事件,那么应该在自定义的ViewGroup中重写这个方法。接着来看看dispatchTouchEvent()方法剩余的部分源码,如下
所示:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {//1
        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child) ||
        !isTransformedTouchPointInView(x, y, child, null)) {//2
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child,idBitsToAssign)) {//3
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
    ...
}

  在上面代码注释1处我们看到了for循环。首先遍历ViewGroup的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意这个for循环是倒序遍历的,即从最上层的子View开始往内层遍历。接着往下看注释2处的代码,其意思是判断触摸点位置是否在子View的范围内或者子View是否在播放动画。如果均不符合则执行continue语句,表示这个子View不符合条件,开始遍历下一个子View。接下来查看注释3处的dispatchTransformedTouchEvent方法做了什么,代码如下所
示:

    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;
        }
        ...
    }

  如果有子View,则调用子ViewdispatchTouchEventevent)方法。如果ViewGroup没有子View,则调
super.dispatchTouchEventevent)方法。ViewGroup是继承View的,再来查看ViewdispatchTouchEvent(event)

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    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)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
}

  我们看到如果OnTouchListener不为null并且onTouch方法返回true,则表示事件被消费,就不会执行onTouchEvent(event);否则就会执行onTouchEvent(event)。可以看出OnTouchListener中的onTouch()方法优先级要高于onTouchEventevent)方法。下面再来看看onTouchEvent()方法的部分源码:

public boolean onTouchEvent(MotionEvent event) {
    ...
        final int action = event.getAction();

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                ...
            }
            return true;
        }
    return false;
}

  从上面的代码可以看到,只要ViewCLICKABLELONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回true消耗这个事件。CLICKABLELONG_CLICKABLE代表View可以被点击和长按点击,可以通过ViewsetClickable和setLongClickable方法来设置,也可以通过View
setOnClickListentersetOnLongClickListener来设置,它们会自动将View设置为CLICKABLELONG_CLICKABLE。接着在ACTION_UP事件中会调用performClick()方法:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {//1
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

  从上面代码注释 1 处可以看出,如果 View 设置了点击事件 OnClickListener,那么它的onClick()方法
就会被执行。View事件分发机制的源码分析就讲到这里了,接下来介绍点击事件分发的传递规则。

点击事件分发的传递规则

  由前面事件分发机制的源码分析可知点击事件分发的这3个重要方法的关系,用伪代码来简单表示:

    public boolean dispatchTouchEvent(MotionEvent event){
        boolean result = false;
        if (onInterceptHoverEvent(event)){
            result = super.onTouchEvent(event);
        }else {
            result = child.dispatchTouchEvent(event);
        }
        return result;
    }

  onInterceptTouchEvent方法和onTouchEvent方法都在dispatchTouchEvent方法中调用。现在我们根据这段
伪代码来分析一下点击事件分发的传递规则。

  首先讲一下点击事件由上而下的传递规则,当点击事件产生后会由 Activity 来处理,传递给
PhoneWindow,再传递给DecorView,最后传递给顶层的ViewGroup。一般在事件传递中只考虑 ViewGroup
onInterceptTouchEvent 方法,因为一般情况下我们不会重写 dispatchTouchEvent()方法。对于根
ViewGroup,点击事件首先传递给它的dispatchTouchEvent()方法,如果该ViewGrouponInterceptTouchEvent()方法返回true,则表示它要拦截这个事件,这个事件就会交给它的onTouchEvent()方法处理;如果onInterceptTouchEvent()方法返回false,则表示它不拦截这个事件,则这个事件会交给它的子元素的dispatchTouchEvent()来处理,如此反复下去。如果传递给底层的ViewView是没有子View的,就会调用ViewdispatchTouchEvent()方法,一般情况下最终会调用ViewonTouchEvent()方法。

  接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其onTouchEvent()方法返回true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父ViewonTouchEvent()处理;如果父ViewonTouchEvent()仍旧返回false,则继续传递给该父View的父View处理,如此反复下去。

以上内容摘自《Android进阶之光》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值