Android View 的 Touch 事件傳遞機(jī)制

一踊餐、概述

在 Android UI 開發(fā)中拷呆,經(jīng)常涉及與 touch(觸摸)事件和手勢,最經(jīng)常使用的點(diǎn)擊事件(OnClickListener)也與 touch 事件相關(guān)攒驰。因此蟆湖,理解 touch 事件在 View 層級中的傳遞機(jī)制尤為重要。然而玻粪,onInterceptTouchEvent隅津、onTouchEvent诬垂、onTouchListener 等一系列接口方法很容易讓人混淆。

本文將介紹 touch 事件的一些基礎(chǔ)知識伦仍,并通過分析 Android FrameWork 源碼來深入理解 touch 事件的分發(fā)機(jī)制结窘。

注:

  1. 本文的源碼分析基于 Android API Level 21,并省略掉部分與本文關(guān)系不大的代碼充蓝。
  2. 在代碼中加入了個(gè)人對源碼的理解隧枫,以注釋形式呈現(xiàn)。

二谓苟、基礎(chǔ)知識

首先介紹幾個(gè)相關(guān)的類和方法:

  • MotionEvent 類:
    該類封裝了一個(gè) Touch 事件的相關(guān)參數(shù)官脓,我們通常所說的一個(gè) Touch 事件,就是指一個(gè) MotionEvent 類的實(shí)例涝焙。一個(gè) MotionEvent 可以分為多種類型确买,即 ACTION_DOWN(按下)、ACTION_MOVE(移動)纱皆、ACTION_UP(抬起)和 ACTION_CANCEL(取消)等湾趾。

  • ACTION_DOWN:
    按照常規(guī)的操作順序,通常的 Touch 事件觸發(fā)的流程都是 DOWN → UP派草,或者 DOWN → MOVE → UP搀缠。所以 ACTION_DOWN 事件通常都是一系列連續(xù)操作事件的起點(diǎn),也因此它通常在處理程序中被作為一個(gè)特殊的標(biāo)識近迁。

  • ACTION_MOVE:
    當(dāng)手指按下后在屏幕上移動艺普,就會產(chǎn)生 ACTION_MOVE 事件,并且通常會隨著手指移動而連續(xù)產(chǎn)生很多個(gè)鉴竭。在移動過程中歧譬,可以根據(jù) MotionEvent 類的坐標(biāo)信息,得到手指在屏幕上移動的位置搏存。

  • ACTION_UP:
    UP 是一系列手勢操作的結(jié)束點(diǎn)瑰步,程序會在收到 ACTION_UP 事件時(shí)做一些收尾性的工作,例如恢復(fù) View 的點(diǎn)擊狀態(tài)璧眠,值得一提的是缩焦,View 的 click 事件就是在 ACTION_UP 時(shí)加以判斷滿足其他條件之后被觸發(fā)的。

  • ACTION_CANCEL:
    CANCEL 事件不是由用戶觸發(fā)的责静,而是系統(tǒng)經(jīng)過邏輯判斷后對某個(gè) View 發(fā)送“取消”消息時(shí)產(chǎn)生的袁滥。收到 CANCEL 事件時(shí),View 應(yīng)該負(fù)責(zé)將自己的狀態(tài)恢復(fù)灾螃。

  • 事件分發(fā)方法 public boolean dispatchTouchEvent(MotionEvent ev)
    事件由上一層的 View 傳遞到下一層 View 的過程稱為事件分發(fā)题翻。dispatchTouchEvent 方法負(fù)責(zé)事件分發(fā)。Activity腰鬼、ViewGroup嵌赠、View 類中都定義了該方法靴拱,所以它們都具有事件分發(fā)的能力。
    Activity.dispatchTouchEvent 實(shí)際上是調(diào)用了 DecorViewdispatchTouchEvent 方法猾普,而 DecorView 實(shí)際上是一個(gè) FrameLayout袜炕,因此 Activity 的 dispatchTouchEvent 最終也是調(diào)用到了 ViewGroup 的 dispatchTouchEvent 方法。
    另外初家,由于 View 沒有管理子 View 的能力偎窘,所以 View.dispatchTouchEvent 方法實(shí)際上不是用來向下分發(fā)事件,而是將事件分發(fā)給自己溜在,調(diào)用了自己的事件響應(yīng)方法去響應(yīng)事件陌知。

  • 事件響應(yīng)方法 public boolean onTouchEvent(MotionEvent event)
    該方法負(fù)責(zé)響應(yīng)事件,并且返回一個(gè) boolean 型掖肋,表示是否消費(fèi)掉事件仆葡,返回 true 表示消費(fèi),false 表示不消費(fèi)志笼。Activity沿盅、View、ViewGroup 都有這個(gè)方法纫溃,所以它們都具有事件響應(yīng)的能力腰涧,并且通過返回值來表示事件是否已經(jīng)消費(fèi)。

  • 事件攔截方法 public boolean onInterceptTouchEvent(MotionEvent ev)
    事件在 ViewGroup 的分發(fā)過程中紊浩,ViewGroup 可以決定是否攔截事件而不對子 View 分發(fā)窖铡。該方法的返回值決定是否需要攔截的,返回 true 表示攔截坊谁,false 表示不攔截费彼。該方法只定義在 ViewGroup 類中,所以只有 ViewGroup 有權(quán)攔截事件不對子View 分發(fā)口芍。

小結(jié):上述幾個(gè)方法和類的關(guān)系如下:


Android-View-Touch-image1.png

三箍铲、View 中 Touch 事件的分發(fā)邏輯

先來看 View.dispatchTouchEvent 的源碼:

// View.java

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    // ...
    if (onFilterTouchEventForSecurity(event)) {

        ListenerInfo li = mListenerInfo;

        // 只要該 View 設(shè)置了 onTouchListener,并且該 View 是 enabled阶界,
        // 則調(diào)用 onTouchListener.onTouch
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        // 只有 onClickListener.onTouch 返回 false虹钮,
        // onTouchEvent 才會被調(diào)用,并將其返回值返回
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    // ...

    return result;
}

可以看出膘融,View 的事件分發(fā)過程主要涉及兩個(gè)方法:mOnTouchListener.onTouchonTouchEvent,并且當(dāng) mOnTouchListener 存在時(shí)祭玉,mOnTouchListener.onTouch 調(diào)用的優(yōu)先級比較高氧映。

什么時(shí)候 mOnTouchListener 會存在?通過 View 的源碼可看到 mOnTouchListener 是在 View 的 setOnTouchListener(OnTouchListener l) 方法中被設(shè)置的脱货。所以岛都,當(dāng)我們通過 setOnTouchListener(OnTouchListener l) 方法設(shè)置了 onClickListener律姨,并在 onClickListener.onTouch 方法中返回 true 消費(fèi)了事件之后,onTouchEvent 將不會再被調(diào)用臼疫。

可見择份,mOnTouchListener.onTouch 是由外部 set 到 View 里去的,而 onTouchEvent 只能通過 Override 去重寫自己的邏輯烫堤,且 View 的 onTouchEvent 方法自身已經(jīng)有不少邏輯荣赶。所以 mOnTouchListener.onTouch 適用于添加不太復(fù)雜的 touch 邏輯,并且可以不妨礙 onTouchEvent 的正常調(diào)用鸽斟;而 onTouchEvent 更適用于用 Override 的形式來改變 View 本身 touch 邏輯拔创。

四、ViewGroup 中 Touch 事件的分發(fā)邏輯

雖然 ViewGroup 是 View 的子類富蓄,但是因?yàn)?ViewGroup 涉及對子 View 的處理剩燥,所以其事件分發(fā)邏輯比 View 的分發(fā)邏輯會復(fù)雜許多。ViewGroup 中重載了 dispatchTouchEvent 方法立倍,邏輯也完全與之前不一樣灭红。

ViewGroup.dispatchTouchEvent 的源碼:

// ViewGroup.java

/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ...
    // 該變量記錄事件是否已被處理
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Step1.如果是 DOWN 事件,則清理之前的變量和狀態(tài)
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // Step2.檢查攔截的情況
        final boolean intercepted;
        // 只有滿足以下兩種情況口注,才可能去判斷是否需要攔截比伏,否則都當(dāng)作攔截:
        // 1.如果是 DOWN 事件
        // 2.在之前的 DOWN 事件分發(fā)過程中已經(jīng)找到并記錄下了響應(yīng) touch 事件的目標(biāo) View
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // 如果該 View 被設(shè)置為不允許攔截,則跳過攔截判斷
            // (注:調(diào)用 requestDisallowInterceptTouchEvent 方法可設(shè)置該變量)
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 允許攔截疆导,則調(diào)用 onInterceptTouchEvent 判斷是否需要攔截
                intercepted = onInterceptTouchEvent(ev);
            } else {
                // 否則不允許攔截(注意此時(shí)不會調(diào)用 onInterceptTouchEvent)
                intercepted = false;
            }
        } else {
            // 如果不是 DOWN 事件赁项,且之前沒有找到響應(yīng) touch 事件的目標(biāo) View,
            // 則該 View 繼續(xù)攔截事件
            intercepted = true;
        }

        // 該變量記錄是否需要取消掉這次事件
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;

        // Step3.分發(fā) DOWN 事件或其他初始事件(例如多點(diǎn)觸摸的 DOWN 事件)

        // 如果既不取消澈段,又不攔截
        if (!canceled && !intercepted) {
            // 如果是 DOWN 事件或其他兩種特殊事件(先只看 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) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 遍歷所有子 View
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        // 找到事件的坐標(biāo)(x,y)對應(yīng)的子 View
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }

                        // ...
                        // 調(diào)用 dispatchTransformedTouchEvent 方法將事件分發(fā)給子 View悠菜,
                        // 該方法會調(diào)用子 View 的 dispatchTouchEvent 方法繼續(xù)分發(fā)事件
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 如果該方法返回 true,代表子 View 消費(fèi)了該事件

                            // 記錄接受該事件的子 View,記錄在以 mFirstTouchTarget 開頭的鏈表中勤众,具體看 addTouchTarget 方法的源碼
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);

                            // 標(biāo)記已經(jīng)成功分發(fā)了事件
                            alreadyDispatchedToNewTouchTarget = true;

                            // 退出循環(huán)
                            break;
                        }
                    }
                }
            }
        }

        // 到目前為止谎碍,在 (不攔截 && 不取消 && 是 DOWN 事件) 的前提下,已經(jīng)在子 View 中尋找過一次事件的響應(yīng)者芬骄。
        // 如果有子 View 消費(fèi)了事件,那么事件已經(jīng)通過 dispatchTransformedTouchEvent 方法分發(fā)到了該子 View 中鹦聪,
        // 并且 alreadyDispatchedToNewTouchTarget = true账阻,
        // 并且將響應(yīng)者記錄在局部變量 newTouchTarget 和 成員變量 mFirstTouchTarget 鏈表中。

        // Step4.接下來將事件分發(fā)到 touchTarget 中或分發(fā)到自己身上泽本。

        if (mFirstTouchTarget == null) {
            // mFirstTouchTarget == null 意味著之前的程序沒有找到事件的消費(fèi)者淘太,那么事件將傳遞給自己,
            // 注意:是通過調(diào)用 dispatchTransformedTouchEvent 方法,并將該方法的第3個(gè)參數(shù)設(shè)為 null蒲牧,代表傳遞給自己撇贺。
            // 而該方法中,當(dāng)?shù)?個(gè)參數(shù)為 null 時(shí)冰抢,會調(diào)用了 super.dispatchTouchEvent 方法松嘶,而 ViewGroup 的父類就是 View,所以就是走了 View 的事件分發(fā)流程將事件傳遞給自己挎扰。
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 接下來通過遍歷 mFirstTouchTarget 鏈表翠订,將事件分發(fā)到 touchTarget 中,
            // 注意上面用 newTouchTarget 變量記錄了已被分發(fā)的 View鼓鲁,這里不會重復(fù)分發(fā)蕴轨。
            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;
                    }
                }
                target = next;
            }
        }
    }
    // ...
    return handled;
}

ViewGroup 的 dispatchTouchEvent 邏輯顯然比 View 的邏輯復(fù)雜得多,主要分為以下 4 步:

  • Step1. 如果是 DOWN 事件骇吭,則清理之前的變量和狀態(tài)
  • Step2. 檢查攔截的情況
  • Step3. 分發(fā) DOWN 事件或其他初始事件(例如多點(diǎn)觸摸的 DOWN 事件)
  • Step4. 接下來將事件分發(fā)到 touchTarget 中或分發(fā)到自己身上橙弱。

我們從以下幾點(diǎn)來總結(jié)一下 ViewGroup 的事件分發(fā)邏輯:

  • ViewGroup 在什么情況下可以攔截事件?
    我們知道燥狰,攔截是由 onInterceptTouchEvent 方法的返回值決定的棘脐。假設(shè)該 ViewGroup 沒有被設(shè)置為不允許攔截(即正常情況下),那么對于 DOWN 事件龙致,onInterceptTouchEvent 方法肯定會被調(diào)用蛀缝。另外,如果是 MOVE目代、UP 或其他事件類型屈梁,只要滿足 mFirstTouchTarget != null 時(shí)也會調(diào)用 onInterceptTouchEvent

  • mFirstTouchTarget 變量會在什么時(shí)候被賦值榛了?它的作用是什么在讶?
    mFirstTouchTarget 是用來記錄在 DOWN 事件中消費(fèi)了事件的子 View,它以鏈表的形式存在霜大,通過 next 變量串起來构哺。在 DOWN 事件中,如果通過點(diǎn)擊的坐標(biāo)找到了某個(gè)子 View战坤,且該子 View 消費(fèi)了事件曙强,那么鏈表中就將這個(gè)子 View 記錄了下來。這樣在后續(xù)的 MOVE途茫、UP 事件中碟嘴,能直接根據(jù)這個(gè)鏈表,將事件分發(fā)給目標(biāo)子 View慈省,而無需重復(fù)再遍歷子 View 去尋找事件的消費(fèi)者臀防。

  • onInterceptTouchEvent 方法針對不同類型的事件進(jìn)行攔截眠菇,會有什么影響边败?
    從上面的源碼可知袱衷,如果在 onInterceptTouchEvent 方法中攔截了非 DOWN 的事件,那么只會影響本次事件的分發(fā)流程笑窜,把事件分發(fā)到自己的 onTouchEvent 方法去處理致燥。而如果 onInterceptTouchEvent 方法中攔截的是 DOWN 事件,那么將導(dǎo)致在 dispatch 過程中找不到事件的消費(fèi)者(即 mFirstTouchTarget == null)排截,那么后續(xù)的 MOVE嫌蚤、UP 事件將不會再詢問是否需要攔截,而是直接分發(fā)到自己的 onTouchEvent 方法去處理断傲。

因此脱吱,DOWN 事件在 ViewGroup 的事件攔截、分發(fā)過程中是一個(gè)特殊的角色认罩,對其處理的結(jié)果將直接影響后續(xù)事件的分發(fā)流程箱蝠。

五、Activity 中 Touch 事件的分發(fā)邏輯

了解完 View 和 ViewGroup 的事件分發(fā)邏輯后垦垂,再來看 Activity 的分發(fā)邏輯就簡單多了宦搬。

Activity.dispatchTouchEvent 的源碼:

// Activity.java

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ...
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

非常簡單,先嘗試調(diào)用 window.superDispatchTouchEvent 方法劫拗,改方法返回 false 時(shí)才調(diào)用 onTouchEvent 方法间校。而 window.superDispatchTouchEvent 方法,實(shí)際上是調(diào)用了 Window 的 DecorView 的 dispatchTouchEvent 方法页慷,由于 DecorView 是 FrameLayout 的子類憔足,當(dāng)然也就是一個(gè) ViewGroup,所以歸根到底 Activity.dispatchTouchEvent 方法最終也是調(diào)用了 ViewGroup.dispatchTouchEvent 方法酒繁。

至此為止滓彰,我們將 View、ViewGroup欲逃、Activity 的事件分發(fā)流程都了解完了找蜜。可以想象稳析,當(dāng)用戶觸發(fā)了一個(gè)觸摸事件洗做,Android 系統(tǒng)會將其傳遞到當(dāng)前觸摸的 Activity.dispatchTouchEvent 方法中,接著彰居,就由 Activity诚纸、ViewGroup、View 的 dispatchTouchEvent 方法不斷遞歸調(diào)用陈惰,把事件傳遞給某個(gè)目標(biāo) View畦徘,然后再逐層返回。

六、例子

最后井辆,我們再通過一個(gè)例子來回顧一下整個(gè)分發(fā)過程关筒。

假設(shè)有一個(gè) Activity,他的界面內(nèi)容是一個(gè) ViewGroup杯缺,ViewGroup 內(nèi)還有一個(gè) Button蒸播。當(dāng)點(diǎn)擊 Button 的位置時(shí),會產(chǎn)生一連串事件萍肆,像 DOWN → UP 或者 DOWN → MOVE → MOVE → UP袍榆,這些事件分發(fā)過程的時(shí)序圖如下:


Android-View-Touch-image2.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市塘揣,隨后出現(xiàn)的幾起案子包雀,更是在濱河造成了極大的恐慌,老刑警劉巖亲铡,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件才写,死亡現(xiàn)場離奇詭異,居然都是意外死亡奴愉,警方通過查閱死者的電腦和手機(jī)琅摩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锭硼,“玉大人房资,你說我怎么就攤上這事√赐罚” “怎么了轰异?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長暑始。 經(jīng)常有香客問我搭独,道長,這世上最難降的妖魔是什么廊镜? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任牙肝,我火速辦了婚禮,結(jié)果婚禮上嗤朴,老公的妹妹穿的比我還像新娘配椭。我一直安慰自己,他們只是感情好雹姊,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布股缸。 她就那樣靜靜地躺著,像睡著了一般吱雏。 火紅的嫁衣襯著肌膚如雪敦姻。 梳的紋絲不亂的頭發(fā)上瘾境,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音镰惦,去河邊找鬼迷守。 笑死,一個(gè)胖子當(dāng)著我的面吹牛陨献,可吹牛的內(nèi)容都是我干的盒犹。 我是一名探鬼主播懂更,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼眨业,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沮协?” 一聲冷哼從身側(cè)響起龄捡,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慷暂,沒想到半個(gè)月后聘殖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡行瑞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年奸腺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片血久。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡突照,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出氧吐,到底是詐尸還是另有隱情讹蘑,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布筑舅,位于F島的核電站座慰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翠拣。R本人自食惡果不足惜版仔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望误墓。 院中可真熱鬧蛮粮,春花似錦、人聲如沸优烧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽畦娄。三九已至又沾,卻和暖如春弊仪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背杖刷。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工励饵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滑燃。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓役听,卻偏偏與公主長得像,于是被迫代替她去往敵國和親表窘。 傳聞我的和親對象是個(gè)殘疾皇子典予,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

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