TouchEvent事件分發機制全解析

網上介紹TouchEvent分發機制的文章很多,可能有的同學看了還是不明白
這里我會結合源碼、畫圖、簡化代碼結構圖、三個人買手機的類比等多個角度全面解釋
其中用三個人買手機的例子做的類比,可以讓你更具象化的直接理解整個流程

開始介紹事件分發機制之前,先簡單介紹下這個TouchEvent是什么

安卓手機的交互,主要就是手指在屏幕上的戳戳滑滑點點
而我們的這些操作其實主要是由三種基本動作組成的:

  • 按下down
  • 移動move
  • 抬起up

安卓中把這個基礎動作叫做TouchEvent

比如
一次點擊就是按下、抬起組成的
一次長按就是按下、等待、抬起組成
一次滑動操作則是,按下、移動、抬起組成

其實除此之外還有多點觸碰,光標操作等動作,這里暫時用不到,不討論

安卓里經常會有多個控件重疊,即ViewGroup包含View的情況
這個時候點擊到子View時,其實也是同時點到ViewGroup這個父控件的,那是把這個點擊事件分給Parent呢還是Child呢?
這里我們就要了解下安卓中的TouchEvent事件分發機制啦

TouchEvent的分發傳遞主要涉及到三個核心方法

  • dispatchTouchEvent 分發Touch事件
  • onInterceptTouchEvent 攔截Touch事件
  • onTouchEvent 處理Touch事件

其中
onInterceptTouch是ViewGroup的方法。View中則沒有該方法
dispatchTouchEvent在View和ViewGroup中有不同的實現,后面會展開介紹


那么在多層結構中TouchEvent到底怎么傳遞呢?
這仨方法用處和調用順序是什么呢?

下面我們來擼個Demo實踐下~
【例一】
倆ViewGroup和一個View,方法全部默認不修改~

嵌套布局

則當點擊到Child上時,Touch事件的相關方法調用順序就是

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

為什么是這樣一個從父級到子級再到父級的U型順序呢?
其實看源碼就知道啦,核心在于ViewGroup的dispatchTouchEvent方法
為了方便理解,我們縮減下代碼,如下

boolean dispatchTouchEvent() {
    // 是否攔截
    boolean intercepted = onInterceptTouchEvent();

    if(!intercepted) {
        // 如果不攔截遍歷所有child,判斷是否有分發
        boolean handled;
        if (child == null) {
            // 等同于handled = onTouchEvent()
            handled = super.dispatchTouchEvent();
        } else {
            // 如果有child,再調用child的分發方法
            handled = child.dispatchTouchEvent();
        }

        if(handled) {
            touchTarget = child;
            break;
        }   
    }

    if(touchTarget == null) {
        // 如果所有child中都沒有消費掉事件
        // 那么就把自己作為沒child的普通View
        handled = super.dispatchTouchEvent();
    }

    return handled;
}

** 方法的作用是將屏幕點擊事件向下(子一級)傳遞到目標控件上,或者傳遞給自己,如果自己就是目標的話**

**如果事件被(自己或者下面某一層的子控件)處理掉了的話,就返回true,否則返回false **

那問題來了,如果我沒有child了,或者我就是一個View,那我的dispatchTouchEvent返回值要如何獲取呢?
這種情況下就會使用父類的dispatchTouchEvent方法,
也就是調用View類中的實現,簡化代碼如下

boolean dispatchTouchEvent() {
    // 實質上就是調用onTouchEvent用其返回值
    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;
}

由此可見,只要是enable=false或者沒有設置過touchListener, 那么他一定會調用onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值

這樣看源碼可能還是不太理解U型順序
那我們把代碼也按照上面的三層結構嵌套起來,就很好理解了,如下

例一

其中super.dispatchTouchEvent實際上就是調用了onTouchEvent方法,同時使用其返回值~
通過上圖上的源碼執行順序就知道為什么日志會這樣輸出了

  1. grandpa dispatchTouchEvent ACTION_DOWN
  1. grandpa onInterceptTouchEvent ACTION_DOWN
  2. --- parent dispatchTouchEvent ACTION_DOWN
  3. --- parent onInterceptTouchEvent ACTION_DOWN
  4. --- --- child dispatchTouchEvent ACTION_DOWN
  5. --- --- child onTouchEvent ACTION_DOWN
  6. --- parent onTouchEvent ACTION_DOWN
  7. grandpa onTouchEvent ACTION_DOWN

dispatchTouchEvent分發的方法我們大概了解了,
onInterceptTouchEvent攔截方法是做什么用的呢?

該方法用于攔截事件向下分發
當返回值為true時,就會攔截TouchEvent不再向下傳遞,直接交給自己的onTouchEvent方法處理。返回false則不攔截。

再做個試驗
【例二】
把例一中的Parent層的onInterceptTouchEvent返回值改為true。
運行一下,點View,看下輸出結果:

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

即當事件一層層向下傳遞到parent時,被他就攔截了下來然后自己消費使用。
再看一下源碼中的執行順序原理,如下圖


例二

intercepted為true~ 沒有進入條件,也就是圖片里X的地方~
就跳過了child.dispatchTouchEvent的向下事件分發了


最后還剩個onTouchEvent方法
方法的主體內容其實是處理具體操作邏輯的,是產生一次點擊還是一次橫縱向的滑動等

而他的返回值才會影響整個事件分發機制,
其意義在于通知父級的ViewGroup們是否已經消費找到目標Target了

同樣,再試驗一下
【例三】
只把例一中的Parent的TouchEvent返回值改為true。攔截方法不變
點一下View,則輸出日志為

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
--- parent dispatchTouchEvent ACTION_UP
--- parent onTouchEvent ACTION_UP

暫時先看Down的邏輯,對應的源碼執行順序如下

例三

Down部分和例一的前7步流程都是一樣的
但是例三源碼圖片中7的地方,
parent調用super.dispatchTouchEvent實際上是調用了onTouchEvent方法,
這里因為我們修改成了true,所以dispatchTouchEvent最終也返回true。

所以返回到grandpa中,touchTarget 就非空了,
因此grandpa的onTouchEvent也沒有執行~

Up部分我們后面再解釋~

到這里我們就可以看出來
事件一旦被某一層消費掉,其它層就不會再消費了


好了,到這里其實對事件分發的機制就有個大概了解了
看了源碼也知道里面的原理是怎么回事

但是
為什么例一二中沒有Up,而例三中有呢?
為什么Up和Down的順序不同呢?
為什么順序是這樣一個U型的呢?
看的我云里霧里的,光看源碼和簡單的demo還是太抽象了啊

為了方便理解,我們先來個具體事件的類比
事件的消費,就類似我們用了一個機會券,然后用它去買了一個手機
而事件的傳遞,就類似于這個機會券在不同朋友直接的流通傳遞

下面開始描述下這個傳遞的具體過程
有三個人ABC,之間的關系是A和B認識,B和C認識,但A和C不認識
某天A接到別人給它的一張購買iphone8的機會券,用它才有資格買手機

拿例一做比較對象,下面開始整個類比流程~

  1. A首先接到了這個信息,然后準備開始思考下這個劵的歸屬
    (grandpa調用dispatchTouchEvent開始分發)
  1. A先想了一下是交給其他人呢?還是自己先用掉這個劵呢
    (grandpa調用onInterceptTouchEvent判斷是否攔截)
  1. A尋思暫時不攔截了吧,然后把劵給了B,讓他去處理下這張劵
    (grandpa不攔截,調用child.dispatchTouchEvent)
  1. B拿到劵后第一反應也是,我要自己用還是問有沒有朋友要呢?
    (parent調用onInterceptTouchEvent判斷是否攔截)
  1. B也有點糾結,算了先問問有沒有其他朋友要用吧,就給了C
    (parent不攔截,調用child.dispatchTouchEvent給C分發)
  1. C拿到劵,額我沒朋友,那就不問誰了,那我自己要不要用呢?
    不用了最新窮~消費不起,那還給B吧。
    (child的分發就是看自己消費與否,返回false給B)
  1. B一看,不要啊~ 那我自己要不要消費呢?還是不了,還給A吧
    (parent調用super.dispatchTouchEvent,返回false給A)
  1. A拿回了轉了一圈的劵,我手機也沒壞啊也不買了~
    (grandpa調用super.dispatchTouchEvent,返回false)

上面就是例一中1~8步驟的情況,所以最終輸出的日志就是

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN
grandpa onTouchEvent ACTION_DOWN

所有人都不消費劵,沒分發出去。
其中步驟6 7 8中都調用了super.dispatchTouchEvent方法,上面我們介紹過,
這個方法內部實際上是調用的onTouchEvent方法~
所以最后的輸出日志順序就是從父到子依次調用分發和攔截,然后從子到父依次調用消費。

而例二也是同理,區別在于
當B拿到券的時候,選擇了攔截下來不再詢問其他朋友了,
但是B又發現自己比較窮,所以也沒消費,直接又還回給了A,
A同樣也不想要新手機也沒有消費這個劵~
所以最終的順序就是,從A到B再返回A就結束了,沒有經過C

例三的情況就不太一樣了
當A->B->C傳遞到C時,C不消費又返回給了B,B一想別浪費了吧,決定消費掉了劵~
相當于B這個parent調用了onTouchEvent消費方法,返回了true也就是用掉了它,
然后反饋給A說那個券我用了,就等于parent.dispatchTouchEvent返回true給上一級的A了,
A聽到消息后哦了一下~都用掉了,那自己也不用再去考慮用不用的事了
也就是A不會再調用grandpa.onTouchEvent方法了

到這里再回頭看dispatchTouchEvent返回值的作用就更明確了
它的返回值其實是用于標志這個事件是否“用掉了”,
無論是我自己或者下面的子一級用掉了都算是用掉~

再比如這個例子中,如果我們讓C消費掉事件,
那么B收到C的消息后,也會調用parent.dispatchTouchEvent返回true給A,
所以這個方法返回值的true是只要用掉就行,無論自己還是下面某一級,
而非我把事件傳遞下去就是true了,下面沒人用最終其實還是返回false的

好了,先總結一下

  1. dispatchTouchEvent方法內容里處理的是分發過程。可以理解為從A->B->C一層層分發的動作
    dispatchTouchEvent的返回值則代表是否將事件分發出去用掉了,自己用或者給某一層子級用都算分發成功。比如B把券用了,或者他發出去給的C把券用了,這兩種情況下B的dispatchTouchEvent都會返回true給A
  2. onInterceptTouchEvent會在第一輪從父到子的時候在分發時調用,以它去決定是否攔截掉此事件不再向下分發。如果攔截下來,就會調用自己的onTouchEvent處理;如果不攔截,則繼續向下傳遞
  3. onTouchEvent代表消費掉事件。方法內容是具體的事件處理方法,如何處理點擊滑動等。
    onTouchEvent的返回值則代表對上級的反饋,通知這個東西我用掉啦,然后他的父級就會讓分發方法也返回true

舉了這個例子主要是為了說明分發、攔截、消費的流程,可以更具象化的理解,
這樣我們再去用它去解釋為什么例一、二中沒有Up,而例三中有就更容易了

還是做個類比
我們的這個買手機其實是一套流程,用券之后還要支付余下的費用~
用券只是第一步,類似于Down
而支付余下的費用就類似于Up
結合到一起才是一個完整的行為
類似于一個Down+一個Up才是一次完整的點擊

前倆例子里為什么沒有Up呢,很好理解,
機會券啊!我都沒用券呢沒購買資格啊,有錢也沒用啊!!!

所以例一二中既然沒人用券,那自然也就不用考慮后續的購買行為了,因此只有Down,沒Up

而一旦有人消費了,那后續的事件也就會來了
好,我們拿例三做類比,B消費掉了這個券
那么現在第二輪來了,銷售員帶著手機先跑來找A,聽說有人要買是誰是誰~

  1. 這個流程依然是先從A開始分配
    (grandpa.dispatchTouchEvent)
  1. A這個時候其實還可以不告訴銷售員誰買的~
    (grandpa.onInterceptTouchEvent 判斷是否攔截)
  1. 但是A還是沒攔下來,告訴銷售員是B買的
    (grandpa不攔截,然后調用child.dispatchTouchEvent)
  1. 銷售員找到了B,B說沒誰了,就是我了
    (parent沒有調用攔截方法)
    然后B付錢結賬尾款,完成了整個行為
    (parent調用onTouchEvent返回true消費掉事件)

所以在例三中的Up順序就是

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP
--- parent dispatchTouchEvent ACTION_UP
--- parent onTouchEvent ACTION_UP

這次有了目標,所以不用再來個U型循環了,直接定位到目標B然后結束~
那么這個目標是怎么個處理機制呢,我們會在后面詳細解釋~


回到例三,其實這里有個地方可以做點手腳的
就是在售貨員上門找A的時候,A可以不告訴售貨員B在哪~攔截下來

這次我們在例三的基礎上進行修改,再整個試驗
【例四】
在grandpa類的onInterceptTouchEvent中添加個判斷,
如果動作是UP就return true攔截掉,DOWN則不攔截和之前一樣

run下代碼,看下輸出日志

grandpa dispatchTouchEvent ACTION_DOWN
grandpa onInterceptTouchEvent ACTION_DOWN
--- parent dispatchTouchEvent ACTION_DOWN
--- parent onInterceptTouchEvent ACTION_DOWN
--- --- child dispatchTouchEvent ACTION_DOWN
--- --- child onTouchEvent ACTION_DOWN
--- parent onTouchEvent ACTION_DOWN

grandpa dispatchTouchEvent ACTION_UP
grandpa onInterceptTouchEvent ACTION_UP

--- parent dispatchTouchEvent ACTION_CANCEL
--- parent onTouchEvent ACTION_CANCEL

前面Down行為和例三一樣,后面就不同了
UP流程變了,然后多了個CANCEL的動作
這里我們可以理解為

  1. 售貨員找到A問誰用的劵啊
    (grandpa調用dispatchTouchEvent分發UP事件)
  1. A說我不告訴你!你就留我這吧!我得不到的(沒券沒資格買)別人也別想得到!!!
    (grandpa調用onInterceptTouchEvent返回true,攔截UP)
  1. 然后A告訴B,別等了孫砸!你的券沒用啦!!!!
    (parent調用dispatchTouchEvent分發CANCEL動作)
  1. 然后B也不用再考慮是否消費了,劵丟了吧~
    (parent使用CANCEL動作調用onTouchEvent方法,結束)

當然,一般某層要用到事件時都會第一輪向下分發就攔截下來,然后用掉
所以例子三的情況比較少,不會那么無私的先問完所有朋友再考慮自己

而例四的情況也比較少,你要不用就一直不用,要用就直接攔截使用,
一般不會開始說不用~ 后來第二輪的時候又攔腰一刀大家一起死吧!!!的這么賤~


到這里其實大概也就了解的差不多了,還剩一個TouchTarget目標的概念,
為什么例三中Up和Down流程不同?
我們再回頭去看完整點的源碼~ 這次雖然也是省略代碼,但是比之前的完善點

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 1.每次起始動作就重置之前的TouchTarget等參數
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 2.如果是起始動作才攔截,或者已經有人消費掉了事件,再去判斷攔截
            // 起始動作是第一次向下分發的時候,每個view都可以決定是否攔截,然后進一步判斷是否消費,很好理解
            // 如果有人消費掉了事件,那么也攔截~ 就像例四中的情況,也可以再次判斷是否攔截的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 3.這里可以設置一個disallowIntercept標志,如果是true,就是誰收到事件后都不準攔截!!!
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // 4.如果未攔截,只有Down動作才去子一級去找目標對象~
            // 因為找目標這個操作只有Down中才會處理
            if (actionMasked == MotionEvent.ACTION_DOWN ) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        newTouchTarget = getTouchTarget(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 5.把自己當做目標,去判斷自己的onTouchEvent是否消費
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 6.如果有人消費掉了事件,找出他~
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                // 7.消費對象信息其實是一個鏈式對象,記載著一個一個傳遞的人的信息,遍歷調用它child的分發方法
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                target = next;
            }
        }
    }

    return handled;
}

注意,有一個dispatchTransformedTouchEvent方法,內部簡化代碼為

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

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

    return handled;
}

其實就是判斷如果沒child了(是ViewGroup但是沒子控件,或者自己就是View),
如果沒child,就調用View的dispatchTouchEvent方法,
實質就是調用onTouchEvent判斷是否消費掉事件
如果有child,就調用child的dispatchTouchEvent將事件一層層向下分發

例一二其實只用看之前的最簡化源碼就理解了~
我們這里用這個比較完善的源碼分析解釋例三四中的復雜情況
其中關鍵主要在于多了一個TouchTarget的處理

其實我們在處理事件的時候,會在第一輪Down的時候先定位到目標,是誰消費了
然后在后續的Move、Up中,利用之前定位的信息更方便的找到目標,直接處理

從上面的源碼中注釋2代碼的位置我們可以看出來,
第一次Down的時候我們才會去判斷是否攔截,或者有目標的時候才攔截
因為第一次傳券的時候可以攔截,而如果沒人用券也就是沒有目標那第二輪就不用攔截了,都買不了手機

如果有人消費呢,比如例三中parent消費掉了事件
那么上面源碼就會在Down時,進入到注釋4代碼的位置,去child一層層找到目標,
當找到某層onTouchEvent返回true消費掉事件的對象后,就會調用addTouchTarget記錄下這個目標
那么第二輪UP到來時,就會進入注釋2代碼條件,再判斷是否攔截,例三中是不做攔截
再往下運行,因為不是Down,所以不會進入注釋4代碼的判斷條件
到最后,就會在注釋5和6代碼中二選一,例三里是B消費了,有目標,所以進入條件6,
然后在注釋7代碼處用dispatchTransformedTouchEvent方法,將Up直接向下層層傳遞給目標

向下傳遞的核心主要是在于dispatchTransformedTouchEvent方法
第一輪動作的Down時,只要不攔截,就會在注釋4代碼處遍歷所有child調用該方法層層傳遞下去
而后續其他動作時,就會進入注釋6代碼條件,然后遍歷TouchTarget中的信息用該方法層層分發

但是要注意不要誤解
第一次Down的時候會for循環所有child,因為A可能有多個朋友B1、B2、B3。。。他會挨個問誰要券啊~
所以第二輪Up的時候也會while(target.next)的迭代循環挨個判斷~但是next是遍歷同級,不是子級
dispatchTrancformTouchEvent(target.child)這里的.child才是向子一級一層一層分發傳遞的地方

這個TouchTarget對象,主要保存的是傳遞路線信息,它是一個鏈式結構
不過這個路線不是A->B->C的一個單子,而是ABC每個人都會保存一個向下的路線信息

比如例子三中B用了券,反饋給了A~ 那么A這里就會保存一個A->B的信息,就是從我這里去找目標B
如果把例一中修改成C消費掉事件,那么A就會保存一個A->B,然后B中還會保存一個B->C的信息,
這樣銷售員來找A的時候,如果A不攔截,就會順著A->B的信息找到B,再順著B手里的B->C信息找到C
當找到最后一個對象的時候,發現C手里沒有下一個目標的路線信息了,那你就是目標沒跑了~

Cancel部分就不解釋了,dispatchTrancformTouchEvent中會判斷,如果cancel=true動作,
則會把動作改成ACTION_CANCEL一層一層的傳下去~
其他還有一些不攔截標志、id什么的設置細節就不介紹了,下面可以自己閱讀下源碼鞏固完善下,
當然我暫時也沒達到每一行代碼都完全掌握的地步,如果文章有不合適的地方歡迎指正和共同討論~

最后宣傳一下個人的Github賬號,有多個不錯的開源項目喲~
歡迎follow我和star代碼~
https://github.com/boredream

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容