<UI>View繪制及事件機(jī)制原理

一逃沿、View繪制流程機(jī)制

1疏哗、View繪制起點(diǎn)

  • performTraversals()方法觸發(fā)了View 的繪制疲迂。

    Activity調(diào)用流程

  • 說(shuō)明:
    在Activity顯示時(shí),WindowManager 將View添加到 DecorView 朱嘴,兩者通過(guò) ViewRoot 連接起來(lái)倾鲫。
    具體實(shí)現(xiàn)類是 ViewRootImpl
    再通過(guò) ViewRootImpl 的一系列處理萍嬉,最終調(diào)用 performTraversals 方法乌昔,在performTraversals 方法中,依次調(diào)用了 performMeasure()壤追,performLayout()磕道,performDraw(),將View 的measure行冰,layout溺蕉,draw` 過(guò)程從頂層View 分發(fā)了下去。開(kāi)始了View的繪制悼做。

2疯特、View繪制流程

  • 繪制過(guò)程分為三步:
    measure(測(cè)量) --> layout(布局) --> draw(繪制),
    draw 流程結(jié)束以后就可以在屏幕上看到view了肛走。
  • 流程圖如下:


    繪制流程
.1漓雅、measure(測(cè)量)
  • 測(cè)量的目的,是為了計(jì)算出View的大小朽色,通過(guò)MeasureSpec來(lái)進(jìn)行計(jì)算的邻吞。
    MeasureSpec是一個(gè) specSizespecMode 信息的32 位int 值,其中高兩位表示 specMode葫男,低30位表示 specSize抱冷。

  • specMode 模式:
    UNSPECIFIED:父容器不對(duì)View有任何限制,要多大有多大腾誉。常用于系統(tǒng)內(nèi)部徘层。
    EXACTLY(精確模式):父視圖為子視圖指定一個(gè)確切的尺寸SpecSize峻呕。對(duì)應(yīng)LyaoutParams中的match_parent或具體數(shù)值。
    AT_MOST(最大[限制]模式):父容器為子視圖指定一個(gè)最大尺寸SpecSize趣效,View的大小不能大于這個(gè)值瘦癌。對(duì)應(yīng)LayoutParams中的wrap_content。

  • 決定View大小的因素:
    因素:widthMeasureSpecheightMeasureSpec
    MeasureSpec 值由 子View的布局參數(shù)LayoutParams父容器的MeasureSpec值 共同決定跷敬。具體規(guī)則見(jiàn)下圖:

    MeasureSpec創(chuàng)建規(guī)則

頂級(jí)View(即DecorView)的測(cè)量在ViewRootImpl的源碼里(getRootMeasureSpec方法):

//desire的這2個(gè)參數(shù)就代表屏幕的寬高讯私,
  childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

  //decorView的measureSpec就是在這里確定的,其實(shí)比普通view的measurespec要簡(jiǎn)單的多
  private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
}
  • 流程:
    performMeasure() 會(huì)調(diào)用 measure()(final方法)西傀,將計(jì)算的MeasureSpec 傳遞給調(diào)用的onMeasure()方法斤寇,此方法會(huì)調(diào)用setMeasuredDimension()來(lái)設(shè)置自身大小拥褂;
    如果是ViewGroup娘锁,先遍歷子View,測(cè)量出子View的MeasureSpec 饺鹃,再調(diào)用measureChild*相關(guān)方法讓子View通過(guò)調(diào)用measure方法來(lái)測(cè)量自己的大心选;再根據(jù)所有子View的大小確定自身的大小悔详,其中的子View會(huì)重復(fù)此類的measure過(guò)程镊屎,如此反復(fù)至完成整個(gè)View樹(shù)的遍歷,確定各個(gè)View自身的大小茄螃。

注意:

  • ViewGroup中沒(méi)有onMeasure方法
  • View會(huì)進(jìn)行多次的測(cè)量缝驳,第一次測(cè)量和最終測(cè)量的實(shí)際寬高不一定相等,在layout流程中可確定View的實(shí)際寬高
  • 獲取measure()后的寬高方法:
  • Activity#onWindowFocusChange()中獲取
  • view.post(Runnable)將獲取的代碼投遞到消息隊(duì)列的尾部
  • ViewTreeObservable方法
.2归苍、layout(布局)
  • 布局目的:確定View的 最終寬高四個(gè)頂點(diǎn)的位置

  • 流程:
    performLayout() 會(huì)調(diào)用頂級(jí)View的layout() 方法用狱,其中調(diào)用 setFrame() 方法來(lái)設(shè)置其四個(gè)頂點(diǎn)(mLeft、mRight拼弃、mTop齿拂、mBottom);
    接著調(diào)用 onLayout() (空方法)肴敛,此方法由具體實(shí)現(xiàn)的View自身重寫,用來(lái)確定自身位置吗购,及循環(huán)其子View來(lái)確定坐標(biāo)位置医男,子View?會(huì)循環(huán)調(diào)用setChildFrame()(就是調(diào)用 View.layout())。

    layout流程

  • layout和onLayout方法有什么區(qū)別捻勉?
    layout是確定本身view的位置镀梭,通過(guò)serFrame方法設(shè)定本身view的四個(gè)頂點(diǎn)的位置。
    onLayout是確定所有子元素的位置踱启。
    View和ViewGroup的 onLayout 方法都是空方法报账。都留給我們自己給子元素布局研底。

.3、draw(繪制)
  • 繪制目的:顯示View
  • 流程:
    performMeasure() 會(huì)調(diào)用 ViewRootImpl的draw方法透罢,再調(diào)用drawSoftWare()方法榜晦,其中會(huì)調(diào)用mView.draw(),是真正繪制步驟的開(kāi)始羽圃。繪制步驟如下:(1乾胶、3、4朽寞、6四步為主要步驟)
  • 1识窿、Draw the background:繪制背景 —— drawBackground(canvas)
    1. If necessary, save the canvas' layers to prepare for fading:保存圖層
    1. Draw view's content:繪制view自身的內(nèi)容 —— onDraw(canvas)
    1. Draw children:繪制子View(分發(fā)) —— dispatchDraw(canvas)
    1. If necessary, draw the fading edges and restore layers:繪制一些圖層
    1. Draw decorations (scrollbars for instance):繪制裝飾(如滾動(dòng)條) —— onDrawForeground(canvas)

注:View中的dispatchDraw(canvas) 是空方法,ViewGroup中的dispatchDraw(canvas)調(diào)用了其drawChild方法

  • 繪制流程圖:


    draw流程
  • 說(shuō)明:

  • setWillNotDraw:用于設(shè)置繪制的標(biāo)志位脑融,view是否需要draw繪制喻频。
    若自定義的View 不需要draw,則可以設(shè)置這個(gè)方法為true肘迎。系統(tǒng)會(huì)根據(jù)此標(biāo)記來(lái)優(yōu)化執(zhí)行速度甥温。
    ViewGroup 一般都默認(rèn)設(shè)置這個(gè)為true,因?yàn)閂iewGroup多數(shù)都是只負(fù)責(zé)布局膜宋,不負(fù)責(zé)draw的窿侈。
    View 的這個(gè)標(biāo)志位默認(rèn)一般都是關(guān)閉的。
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
  • 一般執(zhí)行動(dòng)畫秋茫,會(huì)多次調(diào)用onDraw方法史简,通過(guò)監(jiān)聽(tīng)動(dòng)畫的參數(shù)值變化,不斷 invalidate肛著,不斷重繪圆兵。
    invalidate 是在 主線程 中進(jìn)行調(diào)用,會(huì)引發(fā)onDraw進(jìn)行重繪
    postInvalidate 是在 子線程 中調(diào)用枢贿,最終調(diào)用的仍是invalidate

參考鏈接:
Android View繪制13問(wèn)13答
要點(diǎn)提煉|開(kāi)發(fā)藝術(shù)之View

二殉农、View及ViewGroup的事件機(jī)制

1、相關(guān)概念:

  • MotionEvent事件
  • 事件類型:
    ACTION_DOWN:手指剛接觸屏幕局荚,按下去的那一瞬間產(chǎn)生該事件
    ACTION_MOVE:手指在屏幕上移動(dòng)時(shí)候產(chǎn)生該事件
    ACTION_UP:手指從屏幕上松開(kāi)的瞬間產(chǎn)生該事件
  • 事件序列:
    從ACTION_DOWN開(kāi)始到ACTION_UP結(jié)束我們稱為一個(gè)事件序列
  • TouchSlop
    系統(tǒng)所能識(shí)別的被認(rèn)為是滑動(dòng)的最小距離超凳。
    即當(dāng)手指在屏幕上滑動(dòng)時(shí),如果兩次滑動(dòng)之間的距離小于這個(gè)常量耀态,那么系統(tǒng)就不認(rèn)為你是在進(jìn)行滑動(dòng)操作轮傍。
    該常量和設(shè)備有關(guān),可用它來(lái)判斷用戶的滑動(dòng)是否達(dá)到閾值首装,獲取方法:
   ViewConfiguration.get(getContext()).getScaledTouchSlop()
  • VelocityTracker
    速度追蹤创夜,用于追蹤手指在滑動(dòng)過(guò)程中的速度,包括水平和豎直方向的速度仙逻。

  • GestureDetector
    手勢(shì)檢測(cè)驰吓,用于輔助檢測(cè)用戶的單擊涧尿、滑動(dòng)、長(zhǎng)按檬贰、雙擊等行為姑廉。

2、事件分發(fā)的要點(diǎn)

  • 事件分發(fā)的本質(zhì):
    是對(duì)MotionEvent事件分發(fā)的過(guò)程偎蘸,并將事件消費(fèi)處理庄蹋。

  • 事件分發(fā)的傳遞順序:
    Activity(Window) --> ViewGroup --> View
    即最終調(diào)用的是 View的dispatchTouchEvent(MotionEvent event)方法

  • 事件分發(fā)的重要方法:
    1、dispatchTouchEvent(MotionEvent event):分發(fā)事件迷雪,返回boolean類型限书,表示事件是否消費(fèi)
    2、onInterceptTouchEvent(MotionEvent event):中斷事件(僅ViewGroup有)章咧,返回boolean類型倦西,表示事件是否中斷
    3、onTouchEvent(MotionEvent event):消費(fèi)事件(僅View有)赁严,返回boolean類型扰柠,表示事件是否消費(fèi)
    4、perform*Click():執(zhí)行事件(僅View有)疼约,最終是onClick(View view)卤档,或長(zhǎng)按、雙擊等事件處理程剥。

  • 事件分發(fā)偽代碼:

   // ViewGroup
   public boolean dispatchTouchEvent(MotionEvent ev) {
       // 事件是否被消費(fèi)
       boolean consume = false;
       // 調(diào)用onInterceptTouchEvent判斷是否攔截事件
       if (onInterceptTouchEvent(ev)) {
           // 如果攔截則調(diào)用自身的onTouchEvent方法
           consume = onTouchEvent(ev);
       } else {
           if (targetChild == null) {
              // 沒(méi)有找到目標(biāo)child劝枣,則調(diào)用父容器的分發(fā)方法
              consume = super.dispatchTouchEvent(ev);
           } else {
              // 不攔截調(diào)用子View的dispatchTouchEvent方法
              consume = child.dispatchTouchEvent(ev);
           }
       }
       // 返回值表示事件是否被消費(fèi),true事件終止织鲸,false調(diào)用父View的onTouchEvent方法
       return consume;
   }

   // View
   public boolean dispatchTouchEvent(MotionEvent ev) {
       boolean consume = false;
       // 是否實(shí)現(xiàn)了TouchListener#onTouch方法
       if (onTouchListener != null) {
           // 調(diào)用實(shí)現(xiàn)的onTouchListener#onTouch方法
           consume = onTouchListener.onTouch(ev);
       } else {
           // onTouchEvent()中調(diào)用了perform*Click()等方法
           consume = onTouchEvent(ev);
       }
       //返回值表示事件是否被消費(fèi)舔腾,true事件終止,false調(diào)用父View的onTouchEvent方法
       return consume;
   }

3搂擦、事件分發(fā)的機(jī)制&流程

  • 示意圖


    View及ViewGroup事件流程示意圖
流程詳述:
  • 1稳诚、ViewGroup分發(fā)開(kāi)端:
    Acivity#dispatchTouchEvent(MotionEvent)
    事件最開(kāi)始從Activity開(kāi)始,由Acivity的dispatchTouchEvent方法來(lái)對(duì)事件進(jìn)行分發(fā)瀑踢。
    // Activity源碼:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 事件分發(fā)并返回結(jié)果
        if (getWindow().superDispatchTouchEvent(ev)) {
            //事件被消費(fèi)
            return true;
        }
        // 無(wú)View 消費(fèi)事件扳还,則調(diào)用Activity#onTouchEvent方法
        return onTouchEvent(ev);
    }

PhoneWindow#superDispatchTouchEvent(MotionEvent)
getWindow().superDispatchTouchEvent(ev)Window的抽象方法,具體由PhoneWindow實(shí)現(xiàn)
其內(nèi)部是調(diào)用的頂級(jí)View(DecorView)的superDispatchTouchEvent方法

    // PhoneWindow源碼:
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView.superDispatchTouchEvent(MotionEvent)
頂級(jí)View(DecorView)一般為ViewGroup橱夭,其方法中調(diào)用了ViewGroup#dispatchTouchEvent方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 此處調(diào)用的是ViewGroup的dispatchTouchEvent方法
        return super.dispatchTouchEvent(event);
    }
  • 2普办、ViewGroup事件分發(fā) — onInterceptTouchEvent
    MotionEvent.ACTION_DOWN事件:
    先判斷是否為MotionEvent.ACTION_DOWN事件,是徘钥,則清除 FLAG_DISALLOW_INTERCEPT 設(shè)置并且mFirstTouchTarget 設(shè)置為null,然后根據(jù)條件調(diào)用 onInterceptTouchEvent方法肢娘,來(lái)處理攔截事件呈础。
    如果不是MotionEvent.ACTION_DOWN事件舆驶,且mFirstTouchTarget == null,則直接設(shè)置中斷標(biāo)記為true(intercepted = true)而钞,ViewGroup直接攔截其他事件(如MOVE和UP等)進(jìn)行處理沙廉。
    FLAG_DISALLOW_INTERCEPT標(biāo)志位:
    如果通過(guò)requestDisallowInterceptTouchEvent方法設(shè)置了此標(biāo)志位,則子View可以以此來(lái)干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外臼节,上面的原因)撬陵,而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法。
    // ViewGroup源碼:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
            cancelAndClearTouchTargets(ev);
            //清除FLAG_DISALLOW_INTERCEPT設(shè)置并且mFirstTouchTarget 設(shè)置為null
            resetTouchState();
        }
        // Check for interception.
        final boolean intercepted;//是否攔截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過(guò)
            //requestDisallowInterceptTouchEvent方法進(jìn)行設(shè)置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
        ...
    }
  • 3网缝、ViewGroup事件分發(fā) — 子View遍歷mFirstTouchTarget
    當(dāng)ViewGroup不攔截事件巨税,則遍歷子View,找到事件接收的目標(biāo)View(mFirstTouchTarget)粉臊,條件:

    • View可見(jiàn)且沒(méi)有播放動(dòng)畫:canViewReceivePointerEvents方法
    • 事件的坐標(biāo)落在View的范圍內(nèi):isTransformedTouchPointInView

    當(dāng)mFirstTouchTarget不為null草添,則說(shuō)明已經(jīng)找到過(guò)了目標(biāo)child,則newTouchTarget不為null扼仲,會(huì)跳出循環(huán)远寸。
    但此時(shí)還沒(méi)有將事件分發(fā)給子View,所以newTouchTarget為null屠凶,mFirstTouchTarget也是null驰后。
    如果dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回了true,即子View消費(fèi)了事件矗愧,則會(huì)將mFirstTouchTarget進(jìn)行賦值為該子View灶芝,終止子View的遍歷。此時(shí)贱枣,子View的遍歷完成监署。
    dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法中,如果子View不為null纽哥,則調(diào)用了子View的child.dispatchTouchEvent分發(fā)方法钠乏,進(jìn)行View的分發(fā)。

    // ViewGroup源碼:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final View[] children = mChildren;
        //對(duì)子View進(jìn)行遍歷
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

            //判斷1春塌,View可見(jiàn)并且沒(méi)有播放動(dòng)畫晓避。2,點(diǎn)擊事件的坐標(biāo)落在View的范圍內(nèi)
            //如果上述兩個(gè)條件有一項(xiàng)不滿足則continue繼續(xù)循環(huán)下一個(gè)View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            //如果有子View處理即newTouchTarget 不為null則跳出循環(huán)只壳。
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            //dispatchTransformedTouchEvent第三個(gè)參數(shù)child這里不為null
            //實(shí)際調(diào)用的是child的dispatchTouchEvent方法
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // Child wants to receive touch within its bounds.
                mLastTouchDownTime = ev.getDownTime();
                if (preorderedList != null) {
                    // childIndex points into presorted list, find original index
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                //當(dāng)child處理了點(diǎn)擊事件俏拱,那么會(huì)設(shè)置mFirstTouchTarget 在addTouchTarget被賦值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View處理了事件,然后就跳出了for循環(huán)
                break;
            }
        }
    }

dispatchTransformedTouchEvent方法:

    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }

mFirstTouchTarget的賦值:

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
  • 4吼句、ViewGroup事件分發(fā) — View#dispatchTouchEvent
    若遍歷子View后锅必,ViewGroup沒(méi)有找到事件處理者(① ViewGroup沒(méi)有子View 或 ② 子View處理了事件卻在dispatchTouchEvent方法返回了false),則ViewGroup會(huì)去處理這個(gè)事件。
    dispatchTouchEvent方法返回了false搞隐,mFirstTouchTarget 必然為null驹愚,則再次調(diào)用自身的dispatchTransformedTouchEvent 方法(但傳入的child為null),其內(nèi)部會(huì)調(diào)用super.dispatchTouchEvent(event);方法劣纲,將調(diào)用View#dispatchTouchEvent逢捺,將事件傳給View,至此癞季,ViewGroup的分發(fā)過(guò)程完成劫瞳。
   // Dispatch to touch targets.
   if (mFirstTouchTarget == null) {
       // No touch targets so treat this as an ordinary view.
       handled = dispatchTransformedTouchEvent(ev, canceled, null,
               TouchTarget.ALL_POINTER_IDS);
   }
  • 5、View的事件分發(fā)View#dispatchTouchEvent
    mOnTouchListener.onTouch監(jiān)聽(tīng):
    View中首先判斷是否設(shè)置了OnTouchListener監(jiān)聽(tīng)(開(kāi)發(fā)者自己實(shí)現(xiàn)的)绷柒,若設(shè)置了且onTouch返回true志于,則之后的onTouchEvent方法不會(huì)調(diào)用;若沒(méi)有設(shè)置監(jiān)聽(tīng)或 onTouch 返回 false辉巡,則會(huì)調(diào)用onTouchEvent方法恨憎。
    onTouchEvent方法:
    在此方法中,具體的處理了各個(gè)事件郊楣。
    如果View設(shè)置成了disabled狀態(tài)(即不可用)憔恳,只要 CLICKABLELONG_CLICKABLE 有一個(gè)為true,就一定會(huì)消費(fèi)這個(gè)事件(即onTouchEvent返回true)净蚤,只是它看起來(lái)不可用钥组。只有不可點(diǎn)擊(clickablelongClickable同時(shí)為false),才會(huì)返回false今瀑,即onTouchEvent不消費(fèi)此事件程梦。
    performClick()方法:
    就點(diǎn)擊事件而言,在ACTION_UP 事件的條件下橘荠,會(huì)調(diào)用performClickInternal方法(內(nèi)部實(shí)際是performClick())屿附;在performClick()方法中,如果設(shè)置了OnClickListener哥童,則會(huì)回調(diào)onClick方法挺份。

dispatchTouchEvent(MotionEvent)

     // View源碼:
    //如果窗口沒(méi)有被遮蓋
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        //當(dāng)前監(jiān)聽(tīng)事件
        ListenerInfo li = mListenerInfo;
        //需要特別注意這個(gè)判斷當(dāng)中的li.mOnTouchListener.onTouch(this, event)條件
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //result為false調(diào)用自己的onTouchEvent方法處理
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

onTouchEvent(MotionEvent)

// View源碼:
public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        // 判斷是否不可用,但仍會(huì)消費(fèi)事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 此處重點(diǎn)
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                                if (!post(mPerformClick)) {
                                    // 其內(nèi)部調(diào)用的是performClick方法
                                    performClickInternal();
                                }
                    ...
                    break;
                    ...
            }

            return true;
        }

        return false;
    }

performClick()

   // View源碼:
    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

說(shuō)明: setClickable 失效的原因:
View的setOnClickListener會(huì)默認(rèn)將View的clickable設(shè)置成true贮懈。
View的setOnLongClickListener同樣會(huì)將View的longClickable設(shè)置成true匀泊。

    // View源碼:
    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

4、事件分發(fā)的相關(guān)問(wèn)題:

-- 問(wèn)題:如果一個(gè)事件序列的 ACTION_DOWN 事件被 ViewGroup 攔截朵你,此時(shí)子 View 調(diào)用 requestDisallowInterceptTouchEvent 方法有沒(méi)有用各聘?

子View可以通過(guò) requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外),而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法抡医。

requestDisallowInterceptTouchEvent 中躲因,設(shè)置了 FLAG_DISALLOW_INTERCEPT 標(biāo)志位,表示子View不希望此父級(jí)及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 攔截觸摸事件。

ACTION_DOWN 事件是清除了這個(gè)標(biāo)志位的毛仪,所以搁嗓,requestDisallowInterceptTouchEvent 的設(shè)置對(duì)ACTION_DOWN無(wú)效。

    // ViewGroup源碼箱靴,實(shí)現(xiàn)的是ViewParent的抽象方法
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

    // ViewGroup#dispatchTouchEvent
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            ....
            //清除FLAG_DISALLOW_INTERCEPT設(shè)置并且mFirstTouchTarget 設(shè)置為null
            resetTouchState();
        }
        //是否攔截事件
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過(guò)
            //requestDisallowInterceptTouchEvent方法進(jìn)行設(shè)置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        ...
    }

-- 問(wèn)題:ACTION_DOWN 事件被子 View 消費(fèi)了,那 ViewGroup 能攔截剩下的事件嗎荷愕?如果攔截了剩下事件衡怀,當(dāng)前這個(gè)事件 ViewGroup 能消費(fèi)嗎?子 View 還會(huì)收到事件嗎安疗?

ACTION_DOWN 事件被子 View 消費(fèi)后抛杨,mFirstTouchTarget 則不為null了,就會(huì)直接攔截其他事件荐类,intercepted = true;怖现,見(jiàn)下面的源碼。
設(shè)置了攔截玉罐,就不會(huì)再遍歷子View進(jìn)行事件分發(fā)了屈嗤。則會(huì)取消cancelChild,攔截子View的事件吊输。

   // ViewGroup#dispatchTouchEvent
   if (actionMasked == MotionEvent.ACTION_DOWN
           || mFirstTouchTarget != null) {
       // onInterceptTouchEvent方法的判斷
       ......
   } else {
       // 重點(diǎn)在這里
       // There are no touch targets and this action is not an initial down
       // so this view group continues to intercept touches.
       intercepted = true;    // <---------------------------------------重點(diǎn)
   }

   ....
   // Dispatch to touch targets.
   if (mFirstTouchTarget == null) {
       // 沒(méi)有子 View 消費(fèi)事件饶号,則傳入 null 去分發(fā),最終調(diào)用的是自身的 onTouchEvent 方法季蚂,進(jìn)行處理 touch 事件
       handled = dispatchTransformedTouchEvent(ev, canceled, null,
               TouchTarget.ALL_POINTER_IDS);
   } else {
       // Dispatch to touch targets, excluding the new touch target if we already
       // dispatched to it.  Cancel touch targets if necessary.
       TouchTarget predecessor = null;
       TouchTarget target = mFirstTouchTarget;
       while (target != null) {
           final TouchTarget next = target.next;
           if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
               handled = true;
           } else {
               // 如果 intercepted 就取消 cancelChild茫船,這便是攔截子 View 事件的原理
               final boolean cancelChild = resetCancelNextUpFlag(target.child)
                       || intercepted;   // <---------------------------------------重點(diǎn)
               if (dispatchTransformedTouchEvent(ev, cancelChild,
                       target.child, target.pointerIdBits)) {
                   //內(nèi)部會(huì)比較 pointerIdBits 和當(dāng)前事件的 pointerIdBits,一致才會(huì)處理
                   //這便是 Down 事件處理后后續(xù)事件都交給該 View 處理的原理
                   handled = true;
               }
               if (cancelChild) {
                   if (predecessor == null) {
                       mFirstTouchTarget = next;
                   } else {
                       predecessor.next = next;
                   }
                   target.recycle();
                   // 沒(méi)有next則為null扭屁,就結(jié)束了循環(huán)
                   target = next;
                   continue;
               }
           }
           predecessor = target;
           target = next;
       }
   }
-- 問(wèn)題:當(dāng) View Disable 時(shí)算谈,會(huì)消費(fèi)事件嗎?

會(huì)消費(fèi)事件料滥,只是設(shè)為了不可用然眼,可以看到,在源碼中的注釋為:

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
[一個(gè)可點(diǎn)擊的禁用view幔欧,仍然可以消費(fèi)事件罪治,它只是沒(méi)有響應(yīng)它們(事件)而已。]

    // View#onTouchEvent
   // 判斷是否不可用礁蔗,但仍會(huì)消費(fèi)事件
   if ((viewFlags & ENABLED_MASK) == DISABLED) {
       if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
           setPressed(false);
       }
       mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
       // 此處重點(diǎn)
       // A disabled view that is clickable still consumes the touch
       // events, it just doesn't respond to them.
       return clickable;
   }

參考鏈接:
一文讀懂Android View事件分發(fā)機(jī)制
必問(wèn)的事件分發(fā)觉义,你答得上來(lái)嗎
Android事件分發(fā)機(jī)制詳解:史上最全面、最易懂
要點(diǎn)提煉|開(kāi)發(fā)藝術(shù)之View

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末浴井,一起剝皮案震驚了整個(gè)濱河市晒骇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖洪囤,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徒坡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡府怯,警方通過(guò)查閱死者的電腦和手機(jī)冲簿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門轻绞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)槐雾,“玉大人一屋,你說(shuō)我怎么就攤上這事诽嘉。” “怎么了踏烙?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵浩螺,是天一觀的道長(zhǎng)患蹂。 經(jīng)常有香客問(wèn)我囱挑,道長(zhǎng),這世上最難降的妖魔是什么沼溜? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任平挑,我火速辦了婚禮,結(jié)果婚禮上系草,老公的妹妹穿的比我還像新娘通熄。我一直安慰自己,他們只是感情好找都,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布唇辨。 她就那樣靜靜地躺著,像睡著了一般檐嚣。 火紅的嫁衣襯著肌膚如雪助泽。 梳的紋絲不亂的頭發(fā)上啰扛,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音嗡贺,去河邊找鬼隐解。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诫睬,可吹牛的內(nèi)容都是我干的煞茫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼摄凡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼续徽!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起亲澡,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钦扭,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后床绪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體客情,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年癞己,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了膀斋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痹雅,死狀恐怖仰担,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绩社,我是刑警寧澤摔蓝,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站铃将,受9級(jí)特大地震影響项鬼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜劲阎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一绘盟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悯仙,春花似錦龄毡、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至货岭,卻和暖如春路操,著一層夾襖步出監(jiān)牢的瞬間疾渴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工屯仗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搞坝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓魁袜,卻偏偏與公主長(zhǎng)得像桩撮,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子峰弹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容