View 事件分發(fā)源碼解析

1. 源碼分析目標

view_event_u.png

上一篇文章中對 View 事件分發(fā)的規(guī)律進行了總結(jié),總結(jié)了 View 事件流的分發(fā)規(guī)律以及不同攔截情況下的走向速梗。其中有些總結(jié)我們可能只知道結(jié)論,但是并不知道為什么是那樣的結(jié)論腔寡,比如舷蟀,在父 View 中的 onInterceptTouchEvent 中攔截 move 時翠储,首次 move 事件過來時危号,子 View 的 dispatchTouchEvent 中收到了一個動作 ACTION_CANCEL什猖,而父 View 首次并沒有收到第一次 move 事件票彪,而 Activity 中收到了第一次的 move 事件?不通過源碼我們很難理解為什么是這樣不狮。

下面整理一下問題點降铸,閱讀源碼時我們主要來解決幾個疑問:

  • 事件分發(fā)的 U 型結(jié)構(gòu)是怎么產(chǎn)生的?
  • 子 View 什么情況下會收到 Cancel 事件摇零?
  • 父 View 攔截 Move 事件后推掸,為什么把第一個 Move 事件交給 Activity 了?
  • 為什么 Down 事件至關(guān)重要?
  • 什么是內(nèi)部攔截法谅畅,什么是外部攔截法登渣?
  • 為什么會有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?
  • 為什么會有 TouchTarget 鏈毡泻,什么情況下會有多個 TouchTarget胜茧?

2. Activity 之前的事件分發(fā)

input_progress.png

在底層是通過管道機制來完成事件的監(jiān)聽和下發(fā),首先當產(chǎn)生事件時牙捉,driver 向特定描述符寫入事件后竹揍,會觸發(fā)喚醒 epoll 工作,此時 eventHub 通過 read 方法從描述符中讀取原始事件邪铲,然后通過簡單封裝成 rawEvent 并傳遞給 InputReader芬位。InputReader 中循環(huán)線程會獲取 eventHub 中的事件,然后將事件傳遞到 InputDispater 并最終傳遞到上層带到。上層中有 InputEventReceiver 來接收事件昧碉,它的實現(xiàn)類 WindowInputEventReceiver 負責(zé)接收,WindowInputEventReceiver 是在 ViewRootImpl 中的內(nèi)部類揽惹,ViewRootImpl 我們就不陌生了被饿,雖然不是 View,但是 View 中很多操作源頭開始于此搪搏,如繪制狭握,事件分發(fā)等。

native_event.png

在 ViewRootImpl 中主要將事件按照隊列形式來處理疯溺,依次從隊列中出去事件论颅,每個事件又分為 InputStage 處理,可以理解為階段處理囱嫩。InputStage 主要是用來將事件的處理分成若干個階段(stage)進行恃疯,事件依次經(jīng)過每一個stage,如果該事件沒有被處理(標識為FLAG_FINISHED)墨闲,則該stage就會調(diào)用 onProcess 方法處理今妄,然后調(diào)用 forward 執(zhí)行下一個 stage 的處理;如果該事件被標識為處理則直接調(diào)用 forward鸳碧,執(zhí)行下一個 stage 的處理盾鳞,直到?jīng)]有下一個 stage(也就是最后一個SyntheticInputStage)。這里一共有 7 種 stage瞻离,各個 stage 間串聯(lián)起來雁仲,形成一個鏈表。

其中對于我們關(guān)心的事件屬于 MotionEvent琐脏,相應(yīng)的 InputStage 實現(xiàn)類是 ViewPostImeInputStage,對于手指觸碰的 TouchEvent,在 onProcess 方法中最終調(diào)用 processPointerEvent 處理日裙,processPointerEvent 中又調(diào)用了 mView.dispatchPointerEvent(event)吹艇,mView 是 DecorView,這樣就越來越接近我們的視野范圍昂拂。

final class ViewPostImeInputStage extends InputStage {
    
    ...
    private int processPointerEvent(QueuedInputEvent q) {
        final MotionEvent event = (MotionEvent)q.mEvent;
        mAttachInfo.mUnbufferedDispatchRequested = false;
        mAttachInfo.mHandlingPointerEvent = true;
        boolean handled = mView.dispatchPointerEvent(event);
        ...
        return handled ? FINISH_HANDLED : FORWARD;
    }
}

DecorView 中沒有重寫 dispatchPointerEvent 方法受神,而是走了父類 View 的 dispatchPointerEvent 方法,根據(jù)事件類型判斷格侯,如果是 TouchEvent鼻听,則回到
DecorView 中的 dispatchTouchEvent。

public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

DecorView 中是調(diào)用 Window.Callback 來處理的联四,在 Activity 啟動過程中會創(chuàng)建 PhoneWindow撑碴,并將自己賦給 PhoneWindow,因為 Activity 實現(xiàn)了 Window.Callback 接口朝墩,所以就會轉(zhuǎn)到 Activity 中的 dispatchTouchEvent 處理醉拓。

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

接著調(diào)用 PhoneWindow 的 superDispatchTouchEvent,又回到 mDecor.superDispatchTouchEvent(event)收苏,輾轉(zhuǎn)反側(cè)還交給 mDecor 來處理,接著就來到我們熟悉的 ViewGroup 的 dispatchTouchEvent 中亿卤。

這里簡單思考一下:

(1)ViewRootImpl 中收到事件后為什么不直接通過 DecorView 直接分發(fā)事件,而是繞『彎路』交給 Activity 處理鹿霸,然后又轉(zhuǎn)到 DecorView排吴?

(2)Activity 中為什么不直接獲取 DecorView,而是通過 Window 獲扰呈蟆钻哩?

首選看下第一個問題,由于 Activity 中也會需要處理事件葛闷,如果在 DecorView 中處理憋槐,那么在 DecorView 中就需要回調(diào) Activity 或者持有 Activity,這樣顯然不是很合理淑趾,耦合嚴重阳仔,此外,如果從 DecorView 開始處理扣泊,那么起點就是 Activity 了近范。Activity 是直接面向開發(fā)者的,起始點應(yīng)該設(shè)在 Activity 中延蟹,Activity 是一個外殼评矩,Activity 依賴 Window, Window 再對 View 管理,也就是 PhoneWindow 持有 DecorView阱飘。

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

再看下第二個問題:第一個問題中已經(jīng)提到了斥杜,Activity 是一個外殼虱颗,Activity 直接依賴 Window,Window 是更高層的抽象蔗喂,View 僅僅是 Window 管理的其中部分忘渔,可以比喻為,Activity 不會親自干這種細活缰儿,細活應(yīng)該交給 Window 管理畦粮。同時也達到解耦的目的。

2. Activity 之中的事件分發(fā)

一般來說乖阵,我們平時開發(fā)更多關(guān)注的是 View 的事件分發(fā)宣赔,很少在 Activity 中處理一些事件。而 Activity 中的事件分發(fā)也相對簡單瞪浸,沒有過多的邏輯儒将。在 Activity 中是事件分發(fā)的起點,內(nèi)部 View 沒有攔截的話默终,最后交給 Activity 的 onTouchEvent 處理椅棺。

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

接著是 View 的事件分發(fā),事件從底層過來之后齐蔽,按照從下到上的順序分發(fā)两疚,可以理解為在 Z 方向上下屏幕下到上完成事件的傳遞。一個完整的事件流由一個 Down 事件含滴、若干個 Move 事件和一個 Up 事件組成诱渤。接下來就看下底層 View 是如果向上分發(fā)事件的。先看下 ViewGroup 的 dispatchTouchEvent 方法谈况。

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...
        boolean handled = false;
        // 安全策略校驗通過
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 1. down 事件時清除狀態(tài)勺美,清除事件傳遞鏈表
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
            
            // 2. 事件攔截檢查
            final boolean intercepted;
            // 在初始 Down 事件時或者已經(jīng)有子 View 攔截事件時,即 mFirstTouchTarget碑韵,下次有事件過來時赡茸,如 Move 和 Up 事件,作為 ViewGroup 需要看下是否攔截祝闻,即看 onInterceptTouchEvent 是否攔截
            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 {
                // 子 View 在不攔截 Move 和 Up 事件時占卧,所以當前 ViewGroup 攔截事件,意味著事件不再向下傳遞
                intercepted = true;
            }
            
            // 檢查是否取消事件
            final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;
            
            3. 在 Down 事件時联喘,ViewGroup 沒有攔截的情況下华蜒,尋找攔截事件的子 View
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                ...
                
                // 在 Down 事件時尋找要攔截的子 View,ACTION_POINTER_DOWN 代表是多指觸碰豁遭,后面會分析
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                 
                    ...

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        ...

                        // 從前到后遍歷子View叭喜,找到需要攔截事件的子 view
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            ...
                            // 檢查子View是否能夠接收事件,并判斷事件是否在 view 范圍內(nèi)
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // 判斷子 View 是否已經(jīng)添加到攔截事件的鏈表上蓖谢,即以 mFirstTouchTarget 為頭的鏈表
                            newTouchTarget = getTouchTarget(child);
                            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);
                            // 向子 View 分發(fā)事件捂蕴,看是否需要攔截
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                // 子 View 要攔截譬涡,即找到了攔截的子View,結(jié)束循環(huán)启绰,因為每個事件只有一個View處理
                                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();
                    }
                }
                
                
            }
            
            // 4 沒有子 view 攔截時昂儒,交給自己處理,即走自己的 onTouchEvent
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 
            // 5 有子 view 攔截時委可,調(diào)用子 View 的 dispatchTouchEvent 方法,向上分發(fā)
            else {
                // 有子 view 攔截腊嗡,遍歷鏈表着倾,將事件傳遞到子 View,這里有兩種情況需要處理
                // (1) 如果 ViewGroup 攔截事件,那么向子 View 下發(fā)取消事件燕少,并跳轉(zhuǎn)鏈表的下一個節(jié)點
                // (2) 如果當前的 TouchTarget 是 newTouchTarget卡者,代表已經(jīng)分發(fā)事件了,即在上面已經(jīng)對子 View 調(diào)用過 dispatchTransformedTouchEvent
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
            
        }
        return handled;
    }
            

ViewGroup 的 dispatchTouchEvent 方法相對較長客们,這里再分步驟詳細說明一下(父View 用 ViewGroup 表示),事件下發(fā)時崇决,從 ViewGroup 到子 View,也就是說Down底挫、Move恒傻、Up 事件流是ViewGroup流向子View的,那么在這個過程中ViewGroup可以通過 onInterceptTouchEvent 攔截事件建邓,攔截后就不再向子 View 傳遞盈厘,當然子 View 也可以設(shè)置,不讓 ViewGroup 攔截官边,通過設(shè)置 requestDisallowInterceptTouchEvent 不讓 ViewGroup 攔截沸手。

(1) down 事件時清除狀態(tài),清除事件傳遞鏈表注簿,也就是說 Down 事件時契吉,重新記錄事件的攔截情況,因為 Down 是事件流的開始

(2)actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null诡渴,在初始 Down 事件時或者已經(jīng)有子 View 攔截事件時捐晶,即 mFirstTouchTarget 不為空,下次有事件過來時玩徊,如 Move 和 Up 事件租悄,作為 ViewGroup 需要看下是否攔截,即看 onInterceptTouchEvent 是否攔截恩袱。

  • 對于 Down 事件時泣棋,如果 ViewGroup 攔截了,可以看 intercepted 使用的位置畔塔,會導(dǎo)致不會有子 View 收到事件潭辈,mFirstTouchTarget 為空鸯屿,意味著事件只會在 dispatchTouchEvent 中處理,并返回上一層的 dispatchTouchEvent把敢,ViewGroup 的 onTouchEvent 也不會被調(diào)用寄摆。
  • 如果不是 Down 事件,那么就會是 Move 或者 Up 事件修赞,此時判斷 mFirstTouchTarget 需要不為空才會有可能將事件向上分發(fā)婶恼,才會判斷 onInterceptTouchEvent,為什么呢柏副?上面分析了勾邦,如果 Down 事件時 ViewGroup 攔截了,mFirstTouchTarget 為空的割择,意味著 ViewGroup 在 Down 事件時不能攔截事件眷篇,子 View 需要攔截事件,這樣后續(xù)子 View 才有可能收到 Move 和 Up 事件荔泳,這塊也說明了 Down 事件對子 View 至關(guān)重要蕉饼,決定是否可以收到后續(xù)事件的前提

(3)在 Down 事件時,ViewGroup 沒有攔截的情況下玛歌,尋找攔截事件的子 View昧港。這一步雖然代碼不少,但是邏輯相對簡單沾鳄,就是遍歷子 View慨飘,向子 View 分發(fā)事件,找到要攔截的那個子View译荞,dispatchTransformedTouchEvent 方法會調(diào)用子 View 的 dispatchTouchEvent 方法瓤的,如果子 View 也是一個 ViewGroup,那么就會進行遞歸調(diào)用吞歼,若果是一個 View,會走 View 的 dispatchTouchEvent 方法圈膏,要么 dispatchTouchEvent 方法返回 true,要么內(nèi)部 onTouchEvent 返回 true,或者 OnTouchListener 返回true篙骡,總之在 Down 時需要攔截到事件稽坤。

(4)在步驟 3 中如果沒有找到攔截事件的子 View,mFirstTouchTarget 為空糯俗,此時事件就交給 ViewGroup 自己來處理尿褪,即通過調(diào)用 dispatchTransformedTouchEvent 方法,里面會調(diào)用 View 的 dispatchTouchEvent 方法得湘。

(5)如果找到需要攔截事件的子 View杖玲,那么 mFirstTouchTarget 就不為空,此時如果 ViewGroup 不攔截事件淘正,會將事件繼續(xù)向子 View 分發(fā)摆马。因為事件流總是由父 View 流向子 View臼闻,所以需要看父 View 是否攔截。分發(fā)時需要判斷 Dwon 事件情況囤采,因為這塊的代碼沒有使用事件類型判斷述呐,如判斷事件是否是 Move 或者 Up,而是使用mFirstTouchTarget不為空判斷的蕉毯,需要排除 Down 事件的情況乓搬,即 alreadyDispatchedToNewTouchTarget && target == newTouchTarget 成立時,因為前面已經(jīng)調(diào)用過子 view 的 dispatchTouchEvent 方法恕刘,避免重復(fù)調(diào)用缤谎。

在 ViewGroup 的 dispatchTouchEvent 中多處調(diào)用了 dispatchTransformedTouchEvent 方法,那就看下 dispatchTransformedTouchEvent 中的主要處理邏輯褐着。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // 1 分發(fā)取消事件
        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;
        }

        // 對事件處理,判斷是否是多指觸摸的事件還是同一根手指事件
        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // 2 事件自己處理托呕,調(diào)用 View dispatchTouchEvent 方法含蓉,自己作為 view 處理事件
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        }
        // 3 向子 view 分發(fā)事件,調(diào)用子 View 的 dispatchTouchEvent 方法
        else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

主要有3個重要的處理:

(1)取消事件處理项郊,可以是自身的取消事件馅扣,也可能是子 View 的取消事件處理,那么都是什么情況才是取消事件呢着降?在 dispatchTouchEvent 中找到調(diào)用 dispatchTransformedTouchEvent 方法中 cancel 參數(shù)是 true 的情況差油,

  • 對于子 View 情況,即 child 參數(shù)不為空任洞,同時 cancel 參數(shù)為 true蓄喇,也就是在上面 dispatchTouchEvent 分析的第 5 步中,mFirstTouchTarget 不為空交掏,也就是在 down 事件時有子 View 攔截妆偏,在 Move 或者 Up 時 ViewGroup 攔截了事件,此時 Move 和 Up 不再分發(fā)給子 View盅弛,會給子 View 傳遞一個 Cancel 事件钱骂,一方面可以讓子 View 相關(guān)狀態(tài)復(fù)位,另一方面告知子 View 的事件流結(jié)束
  • 對于 ViewGroup 自身情況挪鹏,dispatchTransformedTouchEvent 方法傳遞參數(shù) child 為 null见秽,cancel 為 true,也就是 ViewGroup 自己的子 View 沒有攔截事件讨盒,自己作為下層父 View 的子 View解取,攔截了 Down 事件,在 Move 或者 Up 事件被自己的父 View 攔截時催植,cancel 為 true

(2)沒有子 View 攔截事件時肮蛹,即 mFirstTouchTarget 為 null勺择,事件自己處理,走 View 中的 dispatchTouchEvent 方法

(3)有子 View 攔截事件時伦忠,事件向上層子 View 分發(fā)

從這塊代碼分析省核,可以得出,子 View 收到 Cancel 事件昆码,首先是子 View 能夠攔截到 Down 事件气忠,在后續(xù)的事件被父 View 攔截時會收到一個 Cancel事件。

接著再看下 View 中到底是如何處理事件的

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...

        final int actionMasked = event.getActionMasked();
        // down 事件時停止?jié)L動
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            // 先調(diào)用 mOnTouchListener 處理赋咽,優(yōu)先級比 onTouchEvent 高
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

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

        ...
        
        // UP 旧噪、CANCEL 結(jié)束事件處理,停止滑動
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

View 的 dispatchTouchEvent 事件相對簡單脓匿,主要是對事件的響應(yīng)處理淘钟,即調(diào)用 mOnTouchListener 或者 onTouchEvent 來對事件直接響應(yīng)的可以看到 mOnTouchListener 優(yōu)先級比 onTouchEvent 高,如果 mOnTouchListener 中返回 true陪毡,那么 onTouchEvent 中就不再收到事件處理米母,而 onTouchEvent 還有 mOnClickListener 和 mOnLongClickListener 處理,可以認為優(yōu)先級比 onTouchEvent 低毡琉。

到這里事件流的一個整理流向分析基本已經(jīng)完成铁瞒,那么我們看下對于 Down 事件,是如何完成經(jīng)典的 U 型走向的?

  • 其實所謂的 U 型桅滋,實際上就是分發(fā)函數(shù)遞歸向下層層調(diào)用慧耍,即 父 View1 dispatchTouchEvent --> 父 View2 dispatchTouchEvent ---> 子 View dispatchTouchEvent,能一直向下調(diào)用的前提是丐谋,父 View 中的 onInterceptTouchEvent 沒有攔截芍碧,均是走 dispatchTouchEvent 默認的 super 方法,簡單來說就是走源碼的 dispatchTouchEvent 實現(xiàn),不是我們自己重寫方法的實現(xiàn)
  • 那 U 型結(jié)構(gòu)的另外一邊呢,即回溯過程血巍?為了保證完整的 U 型結(jié)構(gòu),對于子 View 仍舊走 dispatchTouchEvent 的默認實現(xiàn)践美,最終調(diào)用默認的 onTouchEvent,這樣對于上層子 View dispatchTouchEvent 返回值是 false 的找岖,對于父 View 中 mFirstTouchTarget 是 null陨倡,這樣父 View 調(diào)用 dispatchTransformedTouchEvent 傳遞的 child 也為 null,所以也會走view dispatchTouchEvent 方法许布,然后走 onTouchEvent 方法兴革,然后重復(fù)這個過程返回值層層向上回溯,最終會回到 Activity 的 dispatchTouchEvent 方法中,最后走 Activiy 的 onTouchEvent 方法杂曲。

對于一個 Down 事件庶艾,如果我們不做任何操作,Down 事件就會完成這樣一個 U 型結(jié)構(gòu)的流向擎勘。

在文章開頭的一個問題:ViewGroup 攔截 Move 事件后咱揍,為什么把第一個 Move 事件交給 Activity 了?這個問題是有背景的棚饵,是在上一篇文章中的例子煤裙,ViewGroup 是 Activity 中布局的根布局,ViewGroup 的子 View 攔截 Down 事件噪漾,在 Move 事件過來時硼砰,被 ViewGroup 攔截,此時第一個 Move 事件傳到了 Activity 中

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    // 攔截 Move 事件欣硼,intercepted = true
    ... 
    
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                 // 攔截 Move 事件后向子 View 發(fā)動一個 Cancel 事件
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                ...
            }
            predecessor = target;
            target = next;
        }
         return handled;
    }
}

我們跟蹤一下事件流题翰,ViewGroup 攔截事件后,向子 View 傳遞一個 Cancel 事件诈胜,即調(diào)用 dispatchTransformedTouchEvent 方法遍愿,然后調(diào)用子 View 的 dispatchTouchEvent 方法,子 View 中 cancel 事件一般返回 false耘斩,那么 handled 值也為 false,所以回到 Activity 中的 dispatchTouchEvent 方法時桅咆,會走 onTouchEvent(ev) 方法所以第一個的 Move 事件就流到了 Activity 中括授。對于后面的一系列 Move 事件,只會在 ViewGroup 中被消費岩饼,從上面的代碼中看出荚虚,首次攔截后 mFirstTouchTarget 會被清除掉,所以后面會走 mFirstTouchTarget == null 分支籍茧,即 ViewGroup 自己消費 Move 事件版述,handled 為 true,返回 Activity 中時寞冯,也會直接返回 true渴析,不再走 onTouchEvent(ev)。

下面看下 TouchTarget吮龄,為什么會有 TouchTarget 鏈俭茧,什么情況下會有多個 TouchTarget?

TouchTarget 是用來表示 View 對多個捕獲事件的描述漓帚,簡單來說母债,主要是能夠處理多指觸摸的情況,多只觸摸情況下,多個手指可能觸摸一個 View毡们,也可能是多個 View迅皇,多指觸摸一個 View 時,通過 TouchTarget 記錄該 View 的多個 pointerIdBits衙熔,多指觸摸多個 View 時登颓,通過多個 TouchTarget 組成的鏈表來記錄。鏈表的創(chuàng)建主要是在 Down 事件時通過 addTouchTarget(child, idBitsToAssign) 添加到鏈表上青责,鏈表頭是 mFirstTouchTarget挺据。對于 Down 事件,第一個 Down 事件是 ACTION_DOWN脖隶,后面其他手指的 Down 事件是 ACTION_POINTER_DOWN扁耐,不同 Down 事件都有 index 和 id,對于 ACTION_DOWN 的index始終是 0产阱。所以多指觸摸情況下會有 ACTION_DOWN 和 多個 ACTION_POINTER_DOWN 事件婉称,多個 ACTION_POINTER_DOWN 事件如果是作用在 ViewGroup 中不同子 View 上就會有多個 TouchTarget 形成一個鏈表結(jié)構(gòu),而在 Move 事件時是沒有 ACTION_POINTER_MOVE 的构蹬,只有 ACTION_MOVE王暗。

  if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        final int actionIndex = ev.getActionIndex(); // always 0 for down
        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                : TouchTarget.ALL_POINTER_IDS;
  }

什么是內(nèi)部攔截法,什么是外部攔截法庄敛?內(nèi)部和外部是以子 View 來說的俗壹,如果在子 View 內(nèi)部處理滑動沖突,就是內(nèi)部攔截法藻烤,如果在 ViewGroup 中處理事件攔截绷雏,就是外部攔截法,這里舉一個例子怖亭,父 View 需要攔截左右方向的 Move 事件涎显,子 View 需要攔截垂直方向的 Move 事件。

外部攔截法:


父 View
private float startX;
private float startY;
    
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float delX = Math.abs(ev.getX() - startX);
            float delY = Math.abs(ev.getY() - startY);
            if (delY < delX) {
                return true;
            }
            break;
        default:
            break;
    }
    return super.onInterceptTouchEvent(ev);
}


子 View

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
         return true;
        case MotionEvent.ACTION_MOVE:
        // do some thing
           return true;
        default:
            break;
    }
    return super.onTouchEvent(ev);
}

內(nèi)部攔截法

父 View
    
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            return true;
        default:
            break;
    }
    return super.onInterceptTouchEvent(ev);
}


子 View

private float startX;
private float startY;

@Override
public boolean onTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
         getParent(). requestDisallowInterceptTouchEvent(true);
         return true;
        case MotionEvent.ACTION_MOVE:
            float delX = Math.abs(ev.getX() - startX);
            float delY = Math.abs(ev.getY() - startY);
            if (delY < delX) {
                getParent(). requestDisallowInterceptTouchEvent(false);
                return true;
            }
            break;
        default:
            break;
    }
    return super.onTouchEvent(ev);
}

以上就是內(nèi)部攔截和外部攔截法的簡單實例兴猩,外部攔截法期吓,就是外部ViewGroup通過 onInterceptTouchEvent 來決定是否攔截事件,攔截后倾芝,事件不再向子 View 分發(fā)讨勤。內(nèi)部攔截法是子 View 通過 getParent(). requestDisallowInterceptTouchEvent(false、true);決定是否讓 ViewGroup 攔截蛀醉,簡單來說就是看將判斷邏輯放在哪悬襟。

那么從設(shè)計角度看為什么有 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent?首先看 onInterceptTouchEvent 方法拯刁,有這樣一個場景脊岳,ViewGroup 是能夠上下滑動的,內(nèi)部子 View 是可以點擊的,假設(shè)沒有 onInterceptTouchEvent 方法割捅,那么手指在 子 View 上垂直方向移動時奶躯,應(yīng)該能夠上下滑動,才符合操作習(xí)慣亿驾,但是事件默認都是先給到子 View嘹黔,Move 達到子 View 時再傳給 父 View 也不是不行,但是較麻煩莫瞬,最直接的方式還是在 ViewGroup 向子 view 分發(fā)時直接將 Move 攔截儡蔓,這樣邏輯更加清晰。

requestDisallowInterceptTouchEvent 雖然看上去可有可無疼邀,但是有些場景還是需要有它來處理喂江,比如 ViewPager 中多個 Page,每個 Page 中有 ScrollView 可以上下滑動旁振,開始階段一直上下滑動获询,手指不抬起來,變成水平滑動拐袜,此時應(yīng)該不觸發(fā) ViewPager 切換更為合理吉嚣,但是沒有 requestDisallowInterceptTouchEvent 處理一下,ViewPager 會在水平滑動時攔截 Move 事件蹬铺,導(dǎo)致 ScrollView 對事件處理中途被改變了尝哆。所以這就是源碼中設(shè)置 onInterceptTouchEvent 和 requestDisallowInterceptTouchEvent 這兩個鉤子方法的意義。

3. View 中 onTouchEvent 分析

上面分析了 View 中 dispatchTouchEvent 方法甜攀,內(nèi)部優(yōu)先級是 mOnTouchListener > onTouchEvent, mOnTouchListener 沒什么可說的较解,是由用戶自定義重寫的接口,接下來看下 onTouchEvent 方法赴邻。

    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;

        // 是否view設(shè)置了DISABLED狀態(tài),如果是不再走后面的邏輯啡捶,但是如果 clickable姥敛,仍舊可以消費事件,只是不響應(yīng)點擊事件
        if ((viewFlags & ENABLED_MASK) == DISABLED
                && (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        // 如果設(shè)置了 View 的事件代理瞎暑,則由代理完成 onTouchEvent彤敛,常用來擴大 View 的點擊熱區(qū)
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        // 如果可點擊,根據(jù)事件來處理邏輯
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    // 如果不可點擊則移除 tap 檢測和長按檢測
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    // 是否是按下狀態(tài)
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 獲取焦點
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        // 設(shè)置按下狀態(tài)了赌,一般是背景色半透明墨榄,讓用戶感知到是按下狀態(tài)
                        if (prepressed) {
                            setPressed(true, x, y);
                        }

                        // 在 UP 事件狀態(tài),如果沒有執(zhí)行長按情況下勿她,才有可能執(zhí)行點擊事件
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // 移除長按事件的延遲處理
                            removeLongPressCallback();

                            // 注意這里是 !focusTaken袄秩,不是沒有焦點才執(zhí)行點擊事件,而是本身已經(jīng)有了焦點才執(zhí)行動作,也就是不需要重新獲取焦點之剧,如在有鍵盤情況下郭卫,點擊 // View 是不響應(yīng)事件的,而是先關(guān)閉鍵盤背稼,這種就屬于先要獲取焦點贰军。
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }

                        // 延時取消按下狀態(tài)
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;
                    // 不可點擊時,檢查是否可以長按蟹肘,因為有可能是 (mViewFlags & TOOLTIP) == TOOLTIP) 狀態(tài)
                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // 使用 CheckForTap 做一個延遲檢測词疼,避免處于可滑動的父 View 中時,和滑動事件沖突
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // 觸發(fā)長按檢測帘腹,長按長按時間 400ms 響應(yīng)長按事件
                        setPressed(true, x, y);
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    ...
                    
                    // 移出 View 時贰盗,取消 tap 檢測和長按檢測,并取下按下狀態(tài)
                    if (!pointInView(x, y, touchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }

                    ...

                    break;
            }

            return true;
        }

        return false;
    }

onTouchEvent 方法代碼雖然很多,但主要就是處理點擊和長按事件,在不同事件下對這兩種事件進行響應(yīng)處理竹椒。對于單擊事件相對明確童太,單擊事件是在 Up 時才響應(yīng)的,長按是有個時長胸完,超過 400ms 時才響應(yīng)书释,否則會取消響應(yīng),以及一些按壓狀態(tài)處理赊窥。

(1)在 Up 事件時爆惧,如果不可點擊則移除 tap 檢測和長按檢測,其中 tap 檢測是為了防止長按事件和屬于滑動容器中時和滑動事件互斥锨能,tap 檢測延遲的時長是 100ms扯再,超過 100ms 則說明是長按時間,再延遲 400-100 = 300ms 后執(zhí)行長按事件址遇。

(2)down 事件時主要是觸發(fā)長按事件的延遲檢測熄阻,其中也有上面提到的 tap 檢測,也就是說長按是由時間來決定的倔约,點擊事件是由 UP 事件決定的秃殉。

(3)move 事件時檢查觸摸位置是否還在需要響應(yīng)事件的 View 范圍內(nèi),不在的話浸剩,就需要取消 tap 檢測和長按檢測

(4)取消事件不用多說钾军,同樣會取消 tap 檢測和長按檢測,以及按壓狀態(tài)等

4 總結(jié)

以上就是對 View 分發(fā)事件源碼的一個解析绢要,主要針對開篇的幾個問題吏恭,在分析過程中也做出了回答。本質(zhì)上 View 的事件在一個時刻只由一個 View 來處理重罪,所以就會有攔截過程樱哼,事件由底層向上分發(fā)哀九,中間設(shè)置了一些鉤子函數(shù),onInterceptTouchEvent 和 onTouchEvent,給開發(fā)者定制的機會唇礁。當然還有更加復(fù)雜的場景勾栗,比如嵌套滑動的場景,使用 NestedScrollingChild 和 NestedScrollingParent以及后面更新的 2 和 3 代接口盏筐,主要來更好的解決嵌套滑動場景围俘,簡單來說就是使得 Move 事件能夠在一個事件流過程中,能夠被不同的 View 消費琢融,消費多少滑動距離界牡,都能夠做到精細控制,后面再針對嵌套滑動情形進行分析漾抬。

參考

Android Input輸入事件處理流程分享

ViewGroup事件分發(fā)總結(jié)-TouchTarget

Android 多點觸控詳解

【透鏡系列】看穿 > 觸摸事件分發(fā) >

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宿亡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子纳令,更是在濱河造成了極大的恐慌挽荠,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件平绩,死亡現(xiàn)場離奇詭異圈匆,居然都是意外死亡,警方通過查閱死者的電腦和手機捏雌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門跃赚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人性湿,你說我怎么就攤上這事纬傲。” “怎么了肤频?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵叹括,是天一觀的道長。 經(jīng)常有香客問我宵荒,道長领猾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任骇扇,我火速辦了婚禮,結(jié)果婚禮上面粮,老公的妹妹穿的比我還像新娘少孝。我一直安慰自己,他們只是感情好熬苍,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布稍走。 她就那樣靜靜地躺著袁翁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪婿脸。 梳的紋絲不亂的頭發(fā)上粱胜,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音狐树,去河邊找鬼焙压。 笑死,一個胖子當著我的面吹牛抑钟,可吹牛的內(nèi)容都是我干的涯曲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼在塔,長吁一口氣:“原來是場噩夢啊……” “哼幻件!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蛔溃,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绰沥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贺待,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徽曲,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年狠持,在試婚紗的時候發(fā)現(xiàn)自己被綠了疟位。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡喘垂,死狀恐怖甜刻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情正勒,我是刑警寧澤得院,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站章贞,受9級特大地震影響祥绞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鸭限,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一蜕径、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧败京,春花似錦兜喻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帕识。三九已至,卻和暖如春遂铡,著一層夾襖步出監(jiān)牢的瞬間肮疗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工扒接, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伪货,地道東北人。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓珠增,卻偏偏與公主長得像超歌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒂教,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

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