Android源碼分析之Touch事件分發(fā)機制

研究了View的繪制流程,接下來不得不研究下View的Touch事件機制,只有掌握了這兩方面的知識育瓜,才能對View有全面的認識,Touch事件的入口在Activity中栽烂,代碼如下:

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

我們可以看到躏仇,它首先是由Window來處理的,如果Window不處理腺办,就會交給Activity#onTouchEvent來完成焰手,那么什么情況下會交給Activity來處理呢?我們看下這個方法怀喉,代碼如下:

/**
* Called when a touch screen event was not handled by any of the views
* under it.  This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

注釋里說明了书妻,這種情況是點擊的區(qū)域不在Window的范圍內(nèi),并且會判斷是否需要關閉Activity躬拢。

接下來繼續(xù)看Window是如何處理Touch事件的躲履,這里的Window是PhoneWindow见间,代碼如下:

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

Touch事件最終交給了DecorView來完成,而DecorView又交給了父類ViewGroup來完成工猜,所以真正的入口在ViewGroup中米诉,代碼如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Handle an initial down.
        // 丟棄之前無用的TouchEvent
        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);
            resetTouchState();
        }

        // Check for interception.
        final boolean intercepted;
        // 如果是DOWN事件,或者有目標Target篷帅,是否攔截通過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 {
            // 直接攔截
            // There are no touch targets and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        // 如果沒有攔截史侣,就進行分配
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            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;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    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);

                        ...
                        // 判斷當前的child在不在之前接收事件的view列表里
                        newTouchTarget = getTouchTarget(child);
                        // 找到了接收事件的view,這里將跳出for循環(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;
                        }

                        // 這里就是說當前的child不在之前的列表里
                        resetCancelNextUpFlag(child);
                        // 如果這個child要攔截事件魏身,才會走下邊的if
                        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();
                            // 把這個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();
                }

                // 這里意思是惊橱,沒有找到接收此事件的子View,那就讓最后添加進列表的view來處理
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // TouchTarget這個鏈表是空的箭昵,也就是沒有child處理
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 對所有的在TouchTarget鏈表中的child view李皇,一一進行分配
            // 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) {
                    // 如果已經(jīng)由一個原先不在鏈表中的子view處理,就是handled完成了
                    handled = true;
                } else {
                    循環(huán)查找能夠dispatch的view
                    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;
}

以上流程相對比較復雜宙枷,總結(jié)下來就是掉房,先根據(jù)onInterceptTouchEvent的結(jié)果判斷當前的ViewGroup是否攔截,如果攔截了慰丛,就從維護的TouchTarget鏈表中尋找可以消費事件的child卓囚,如果不攔截,就遍歷所有的子view诅病,直到找到消費事件的子view哪亿,并添加到TouchTarget鏈表中。接下來我們看下dispatchTransformedTouchEvent的實現(xiàn)贤笆,代碼如下:

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

    ...

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } 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;
}

這里如果child不存在蝇棉,就交給父級來處理,否則就交給child來處理芥永,但是它們調(diào)用的是同一個方法篡殷,這個方法屬于View,它的實現(xiàn)如下:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...

    boolean result = false;

    ...

    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        //noinspection SimplifiableIfStatement
        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;
        }
    }

    ...

    return result;
}

可以看到埋涧,如果我們設置了OnTouchListener板辽,就會調(diào)用它的onTouch方法,否則就會由View的onTouchEvent方法來完成棘催。這個方法的實現(xiàn)如下:

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    // 禁用之后的表現(xiàn)
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    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) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        // The button is being released before we actually
                        // showed it as pressed.  Make it show the pressed
                        // state now (before scheduling the click) to ensure
                        // the user sees it.
                        setPressed(true, x, y);
                    }
                    // 如果不是長按事件劲弦,就是單擊事件
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        removeLongPressCallback();

                        // Only perform take click actions if we were in the pressed state
                        if (!focusTaken) {
                            // Use a Runnable and post this rather than calling
                            // performClick directly. This lets other visual state
                            // of the view update before click actions start.
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                    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:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

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

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    // 檢查是否長按事件
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

這里可以看出醇坝,在ACTION_DOWN事件時會先檢查是否是長按邑跪,然后在ACTION_UP事件時,如果不是長按再確認是點擊事件,那么長按事件是何時觸發(fā)的呢画畅,我們看以下代碼:

private void checkForLongClick(int delayOffset, float x, float y) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
        mHasPerformedLongPress = false;

        ...
        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}

private final class CheckForLongPress implements Runnable {
    ...

    @Override
    public void run() {
        if (isPressed() && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
    }
}

這里是發(fā)送了一個Delayed消息到Handler中贸毕,待取得Message之后就會調(diào)用performLongClick方法,這里也可以看到夜赵,長按的默認觸發(fā)時間是通過ViewConfiguration#getLongPressTimeout方法獲取的,值是500毫秒乡革。也就是說寇僧,只要長按,不需要ACTION_UP就能觸發(fā)事件沸版,這和我們使用手機時的體驗一致嘁傀。

這里我們看到了一堆函數(shù),這些函數(shù)統(tǒng)統(tǒng)都有相同的返回值:一個boolean變量视粮。但是這個變量在不同的函數(shù)中代表的意義不同细办,產(chǎn)生的結(jié)果也不一樣。接下來蕾殴,我們分兩個部分笑撞,依次分析它們的區(qū)別,這兩個部分分別是關于DOWN事件和關于其他事件(如MOVE钓觉、UP)茴肥。

DOWN事件的傳遞

onInterceptTouchEvent

我們要首先分析這個方法,是因為它是ViewGroup特有的荡灾,對View而言并不需要瓤狐。這個方法只有在接收到的事件是DOWN,或著已經(jīng)維護有TouchTarget鏈表時才會觸發(fā)批幌,通俗說就是要么接收到了DOWN础锐,要么它有子View消費了DOWN。這時荧缘,ViewGroup才有決定是否要攔截此事件的選擇皆警,一旦攔截,就意味著之前不在TouchTarget鏈表里的子View不會再接收到任何事件截粗。

return true:攔截消息耀怜,不在TouchTarget鏈表里的子View不會接收到任何事件
return false:不攔截,將消息分發(fā)給子View來處理

dispatchTouchEvent

dispatch是分發(fā)的意思桐愉,在ViewGroup和View兩個類中的表現(xiàn)并不完全一致财破。對于ViewGroup,它的dispatch決定是自己消費還是交由子View消費从诲,而對于View而言則是自己消費左痢。而相同點在于,只要dispatch最終返回了false,也就是沒有消費俊性,就要交給上一層級來完成略步,最終如果到Activity還沒被消費,事件將被丟棄定页。

不過這里有一點需要注意的是趟薄,如果將事件分發(fā)給一個最初不在TouchTarget鏈表中的子View時,那么這個子View對DOWN事件的處理將決定它能否處理其他事件典徊。在上方分析ViewGroup#dispatchTouchEvent時杭煎,我們看到只有子View的dispatchTouchEvent返回為true時,才會把它加入到TouchTarget鏈表里卒落。

return true:表示告訴上一級羡铲,已消費
return false:表示沒有辦法處理消息,交由上一級來完成

onTouchEvent

這里是處理事件的具體地方了儡毕,設置OnTouchListener的道理也是一樣的也切,正如上面說到的,這里處理了DOWN事件后如果返回了false腰湾,就意味著它無法再處理UP雷恃、MOVE等事件。而返回false表示事件在此處并未終止费坊,還要把事件繼續(xù)傳遞下去褂萧,直到在某個View返回true為止,或者因沒有任何View消費而丟棄葵萎。

return true:消費事件导犹,終止事件的傳輸,并可以處理接下來的MOVE羡忘、UP等事件
return false:不消費事件谎痢,事件將繼續(xù)傳輸

MOVE、UP事件的傳遞

這些事件的傳輸和DOWN類似卷雕,但是最主要在于它受DOWN事件的影響节猿,如果DOWN事件返回false,也就是稱沒有消費漫雕,那么這個View就不能收到這些事件滨嘱。

Touch事件的機制,在遇見實際的問題時浸间,情況還是比較復雜的太雨,但是它的根本原理就是源碼里向我們展示的這一部分,以后分析具體的View時魁蒜,可能還會有更多的理解囊扳,這個知識需要不斷去體會吩翻,去嘗試,才能了然于心锥咸。

上一篇:Android源碼分析之Activity啟動與View繪制流程(二)

下一篇:Android源碼分析之Handler

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狭瞎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搏予,更是在濱河造成了極大的恐慌熊锭,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雪侥,死亡現(xiàn)場離奇詭異碗殷,居然都是意外死亡,警方通過查閱死者的電腦和手機校镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捺典,“玉大人鸟廓,你說我怎么就攤上這事〗蠹海” “怎么了引谜?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長擎浴。 經(jīng)常有香客問我员咽,道長,這世上最難降的妖魔是什么贮预? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任贝室,我火速辦了婚禮,結(jié)果婚禮上仿吞,老公的妹妹穿的比我還像新娘滑频。我一直安慰自己,他們只是感情好唤冈,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布峡迷。 她就那樣靜靜地躺著,像睡著了一般你虹。 火紅的嫁衣襯著肌膚如雪绘搞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天傅物,我揣著相機與錄音夯辖,去河邊找鬼。 笑死董饰,一個胖子當著我的面吹牛楼雹,可吹牛的內(nèi)容都是我干的模孩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼贮缅,長吁一口氣:“原來是場噩夢啊……” “哼榨咐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谴供,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤块茁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后桂肌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體数焊,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年崎场,在試婚紗的時候發(fā)現(xiàn)自己被綠了佩耳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡谭跨,死狀恐怖干厚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情螃宙,我是刑警寧澤蛮瞄,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站谆扎,受9級特大地震影響挂捅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜堂湖,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一闲先、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧无蜂,春花似錦饵蒂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泻肯,卻和暖如春渊迁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背灶挟。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工琉朽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人稚铣。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓箱叁,卻偏偏與公主長得像墅垮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子耕漱,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

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