Android 全局手势识别原理分析

       通过标题可以看到,本文是对全局手势识别进行分析,那什么是全局手势呢?简单来说就是在任何界面都需要识别的手势,比如:在任何界面从手机屏幕左侧滑动,当前的界面会退出(类似back键);
       我们知道,在Android系统中一个Activity在显示时,当对屏幕触摸事件进行响应时,经过了许多逻辑处理,详细分析可以参考之前对IMS原理分析的一系列文章:
       Android IMS原理解析
       Android IMS原理解析之InputReader
       Android IMS原理解析之InputDispatcher
       Android IMS原理解析之InputChannel
       Android IMS原理解析之processEvent
       Android IMS原理解析之dispatchEvent

       接下来对全局事件注册监听及处理进行分析:

一.注册Native监听

       在WMS启动时就进行注册了,具体的逻辑是在构造方法内完成的,代码如下:

private final PointerEventDispatcher mPointerEventDispatcher

private WindowManagerService(Context context, InputManagerService inputManager,
            boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
            WindowManagerPolicy policy) {
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    if(mInputManager != null) {
       final InputChannel inputChannel = mInputManager.monitorInput(TAG_WM);
       mPointerEventDispatcher = inputChannel != null ? new PointerEventDispatcher(inputChannel) : null;
    }
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
}

       根据调用关系来看一下InputManagerService中monitorInput()方法:

public InputChannel monitorInput(String inputChannelName) {

    InputChannel[] inputChannels = InputChannel.openInputChannelPair(inputChannelName);
    nativeRegisterInputChannel(mPtr, inputChannels[0], null, true);
    inputChannels[0].dispose(); // don't need to retain the Java object reference
    return inputChannels[1];
}

       在该方法内部是通过InputChannel的openInputChannelPair()来创建一对InputChannel,将inputChannels[0]进行native注册,inputChannels[1]作为接收者,可以看到,此处跟普通的Window接收事件的处理是一致的,不同点在nativeRegisterInputChannel()传递的参数,主要有两处:
       1.第三个参数InputWindowHandle传入的为null,因为是全局手势识别,所以不能指定特定的InputWindowHandle;
       2.第四个参数monitor传入的为true,普通window传入的是false,通过名称可以看到,是监测,即接收所有事件;
       接着上面的分析,在WindowManagerService的构造方法内部,通过mInputManager.monitorInput()的返回值InputChannel,创建了PointerEventDispatcher对象,一起看一下PointerEventDispatcher的实现:

public class PointerEventDispatcher extends InputEventReceiver {
    ArrayList<PointerEventListener> mListeners = new ArrayList<PointerEventListener>();
    PointerEventListener[] mListenersArray = new PointerEventListener[0];

    public PointerEventDispatcher(InputChannel inputChannel) {
        super(inputChannel, UiThread.getHandler().getLooper());
    }

    @Override
    public void onInputEvent(InputEvent event, int displayId) {
        try {
            if (event instanceof MotionEvent
                    && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                final MotionEvent motionEvent = (MotionEvent) event;
                PointerEventListener[] listeners;
                synchronized (mListeners) {
                    if (mListenersArray == null) {
                        mListenersArray = new PointerEventListener[mListeners.size()];
                        mListeners.toArray(mListenersArray);
                    }
                    listeners = mListenersArray;
                }
                for (int i = 0; i < listeners.length; ++i) {
                    listeners[i].onPointerEvent(motionEvent, displayId);
                }
            }
        } finally {
            finishInputEvent(event, false);
        }
    }

    public void registerInputEventListener(PointerEventListener listener) {
        synchronized (mListeners) {
            if (mListeners.contains(listener)) {
                throw new IllegalStateException("registerInputEventListener: trying to register" +
                        listener + " twice.");
            }
            mListeners.add(listener);
            mListenersArray = null;
        }
    }

    public void unregisterInputEventListener(PointerEventListener listener) {
        synchronized (mListeners) {
            if (!mListeners.contains(listener)) {
                throw new IllegalStateException("registerInputEventListener: " + listener +
                        " not registered.");
            }
            mListeners.remove(listener);
            mListenersArray = null;
        }
    }
}

       可以看到,PointerEventDispatcher继承了InputEventReceiver,即:触摸事件从native返回后会回调InputEventReceiver的dispatchInputEvent()方法,接着调用其继承类即PointerEventDispatcher的onInputEvent()方法,在该方法内遍历了所有的Listener并回调onPointerEvent()方法,通过registerInputEventListener()来注册监听;

二.注册事件回调

       上面分析了在WMS构造方法内部建立了对native的监听,即当有触摸事件产生时,会回调到onInputEvent()方法,最终再回调所有PointerEventListener的onPointerEvent(),那么PointerEventListener就是全局手势的接收者,onPointerEvent()就是对全局手势的处理方法;
       先看一下PointerEventListener,定义在WindowManagerPolicy.java里面:

public interface PointerEventListener {

    void onPointerEvent(MotionEvent motionEvent);

    default void onPointerEvent(MotionEvent motionEvent, int displayId) {
        if (displayId == DEFAULT_DISPLAY) {
            onPointerEvent(motionEvent);
        }
    }
}

       PointerEventListener是一个接口,来看一下具体实现类,实现类为SystemGesturesPointerEventListener:

public class SystemGesturesPointerEventListener implements PointerEventListener {
    ;;;;;;;;;;;;;;;;;;;;;;;;;

    public SystemGesturesPointerEventListener(Context context, Callbacks callbacks) {
        mContext = context;
        mCallbacks = checkNull("callbacks", callbacks);
        mSwipeStartThreshold = checkNull("context", context).getResources()
                .getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height);
        mSwipeDistanceThreshold = mSwipeStartThreshold;
        if (DEBUG) Slog.d(TAG,  "mSwipeStartThreshold=" + mSwipeStartThreshold
                + " mSwipeDistanceThreshold=" + mSwipeDistanceThreshold);
    }

    private static <T> T checkNull(String name, T arg) {
        if (arg == null) {
            throw new IllegalArgumentException(name + " must not be null");
        }
        return arg;
    }

    public void systemReady() {
        Handler h = new Handler(Looper.myLooper());
        mGestureDetector = new GestureDetector(mContext, new FlingGestureDetector(), h);
        mOverscroller = new OverScroller(mContext);
    }

    @Override
    public void onPointerEvent(MotionEvent event) {
        if (mGestureDetector != null && event.isTouchEvent()) {
            mGestureDetector.onTouchEvent(event);
        }
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                ;;;;;;;;;;;;;;;;;;;;;
                mCallbacks.onDown();
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                ;;;;;;;;;;;;;;;;;;;;;;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mSwipeFireable) {
                    final int swipe = detectSwipe(event);
                    mSwipeFireable = swipe == SWIPE_NONE;
                    if (swipe == SWIPE_FROM_TOP) {
                        mCallbacks.onSwipeFromTop();
                    } else if (swipe == SWIPE_FROM_BOTTOM) {
                        mCallbacks.onSwipeFromBottom();
                    } else if (swipe == SWIPE_FROM_RIGHT) {
                        mCallbacks.onSwipeFromRight();
                    } else if (swipe == SWIPE_FROM_LEFT) {
                        mCallbacks.onSwipeFromLeft();
                    }
                }
                break;
            case MotionEvent.ACTION_HOVER_MOVE:
                if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
                    if (!mMouseHoveringAtEdge && event.getY() == 0) {
                        mCallbacks.onMouseHoverAtTop();
                        mMouseHoveringAtEdge = true;
                    } else if (!mMouseHoveringAtEdge && event.getY() >= screenHeight - 1) {
                        mCallbacks.onMouseHoverAtBottom();
                        mMouseHoveringAtEdge = true;
                    } else if (mMouseHoveringAtEdge
                            && (event.getY() > 0 && event.getY() < screenHeight - 1)) {
                        mCallbacks.onMouseLeaveFromEdge();
                        mMouseHoveringAtEdge = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mSwipeFireable = false;
                mDebugFireable = false;
                if (mScrollFired)
                    mCallbacks.onScroll(false);
                mScrollFired = false;
                mCallbacks.onUpOrCancel();
                break;
            default:
                if (DEBUG) Slog.d(TAG, "Ignoring " + event);
        }
    }

    private void captureDown(MotionEvent event, int pointerIndex) {
        final int pointerId = event.getPointerId(pointerIndex);
        final int i = findIndex(pointerId);
        if (i != UNTRACKED_POINTER) {
            mDownX[i] = event.getX(pointerIndex);
            mDownY[i] = event.getY(pointerIndex);
            mDownTime[i] = event.getEventTime();
            if (DEBUG) Slog.d(TAG, "pointer " + pointerId +
                    " down x=" + mDownX[i] + " y=" + mDownY[i]);
        }
    }

    private int findIndex(int pointerId) {
        for (int i = 0; i < mDownPointers; i++) {
            if (mDownPointerId[i] == pointerId) {
                return i;
            }
        }
        if (mDownPointers == MAX_TRACKED_POINTERS || pointerId == MotionEvent.INVALID_POINTER_ID) {
            return UNTRACKED_POINTER;
        }
        mDownPointerId[mDownPointers++] = pointerId;
        return mDownPointers - 1;
    }

    private int detectSwipe(MotionEvent move) {
        final int historySize = move.getHistorySize();
        final int pointerCount = move.getPointerCount();
        for (int p = 0; p < pointerCount; p++) {
            final int pointerId = move.getPointerId(p);
            final int i = findIndex(pointerId);
            if (i != UNTRACKED_POINTER) {
                for (int h = 0; h < historySize; h++) {
                    final long time = move.getHistoricalEventTime(h);
                    final float x = move.getHistoricalX(p, h);
                    final float y = move.getHistoricalY(p,  h);
                    final int swipe = detectSwipe(i, time, x, y);
                    if (swipe != SWIPE_NONE) {
                        return swipe;
                    }
                }
                final int swipe = detectSwipe(i, move.getEventTime(), move.getX(p), move.getY(p));
                if (swipe != SWIPE_NONE) {
                    return swipe;
                }
            }
        }
        return SWIPE_NONE;
    }

    private int detectSwipe(int i, long time, float x, float y) {
        final float fromX = mDownX[i];
        final float fromY = mDownY[i];
        final long elapsed = time - mDownTime[i];
        if (DEBUG) Slog.d(TAG, "pointer " + mDownPointerId[i]
                + " moved (" + fromX + "->" + x + "," + fromY + "->" + y + ") in " + elapsed);
        if (fromY <= mSwipeStartThreshold
                && y > fromY + mSwipeDistanceThreshold
                && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_TOP;
        }
        if (fromY >= screenHeight - mSwipeStartThreshold
                && y < fromY - mSwipeDistanceThreshold
                && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_BOTTOM;
        }
        if (fromX >= screenWidth - mSwipeStartThreshold
                && x < fromX - mSwipeDistanceThreshold
                && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_RIGHT;
        }
        if (fromX <= mSwipeStartThreshold
                && x > fromX + mSwipeDistanceThreshold
                && elapsed < SWIPE_TIMEOUT_MS) {
            return SWIPE_FROM_LEFT;
        }
        return SWIPE_NONE;
    }

    private final class FlingGestureDetector extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            if (!mOverscroller.isFinished()) {
                mOverscroller.forceFinished(true);
            }
            return true;
        }
        @Override
        public boolean onFling(MotionEvent down, MotionEvent up,
                float velocityX, float velocityY) {
            mOverscroller.computeScrollOffset();
            long now = SystemClock.uptimeMillis();

            if (mLastFlingTime != 0 && now > mLastFlingTime + MAX_FLING_TIME_MILLIS) {
                mOverscroller.forceFinished(true);
            }
            mOverscroller.fling(0, 0, (int)velocityX, (int)velocityY,
                    Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
            int duration = mOverscroller.getDuration();
            if (duration > MAX_FLING_TIME_MILLIS) {
                duration = MAX_FLING_TIME_MILLIS;
            }
            mLastFlingTime = now;
            mCallbacks.onFling(duration);
            return true;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                   float distanceX, float distanceY) {
            if (!mScrollFired) {
                mCallbacks.onScroll(true);
                mScrollFired = true;
            }
            return true;
        }
    }

    interface Callbacks {
        void onSwipeFromTop();
        void onSwipeFromBottom();
        void onSwipeFromRight();
        void onSwipeFromLeft();
        void onFling(int durationMs);
        void onScroll(boolean started);
        void onDown();
        void onUpOrCancel();
        void onMouseHoverAtTop();
        void onMouseHoverAtBottom();
        void onMouseLeaveFromEdge();
        void onDebug();
    }
}

       通过代码逻辑可以看到,里面实现了左滑、右滑、上滑、下滑等等全局事件,当检测到对应的事件后,会执行回调方法,具体实现及处理是在PhoneWindowManager里面:

WindowManagerFuncs mWindowManagerFuncs;
private SystemGesturesPointerEventListener mSystemGestures;

public void init(Context context, IWindowManager windowManager,
            WindowManagerFuncs windowManagerFuncs) {
    mWindowManagerFuncs = windowManagerFuncs;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    // monitor for system gestures
    mSystemGestures = new SystemGesturesPointerEventListener(context,
            new SystemGesturesPointerEventListener.Callbacks() {
                @Override
                public void onSwipeFromTop() {
                }
                @Override
                public void onSwipeFromBottom() {
                }
                @Override
                public void onSwipeFromRight() {
                }
                @Override
                public void onSwipeFromLeft() {
                }
                ;;;;;;;;;;;;;;;;;;;
        });
    //通过mWindowManagerFuncs进行注册
    mWindowManagerFuncs.registerPointerEventListener(mSystemGestures);
}

       可以看到,在PhoneWindowManager的init()方法内部,会创建SystemGesturesPointerEventListener,并通过WindowManagerFuncs的registerPointerEventListener()进行注册,接下来看一下init()方法的调用入口和WindowManagerFuncs的实现,通过源码发现,这两者都跟WindowManagerService有关系,WindowManagerService是在SystemServer里面的startOtherServices()里面启动的:

//SystemServer.java
private void startOtherServices() {
    wm = WindowManagerService.main(context, inputManager,
            mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,
            !mFirstBoot, mOnlyCore, new PhoneWindowManager());
}

//WindowManagerService.java
public static WindowManagerService main(final Context context, final InputManagerService im,
            final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
            WindowManagerPolicy policy) {
    sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs,
                    onlyCore, policy), 0);
    return sInstance;
}
private WindowManagerService(Context context, InputManagerService inputManager,
            boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
            WindowManagerPolicy policy) {
    mPolicy = policy;
    ;;;;;;;;;;;;;;;;;;;;
    initPolicy();
}
private void initPolicy() {
    mPolicy.init(mContext, WindowManagerService.this, WindowManagerService.this);
}

       WindowManagerPolicy是一个接口,PhoneWindowManager是WindowManagerPolicy的实现者,所以PhoneWindowManager的init()方法是在WindowManagerService里面调用的,通过传入的参数可以看到,第二和第三参数传的都是WindowManagerService本身,说明WindowManagerService实现了WindowManagerFuncs,一起看一下:

public class WindowManagerService extends IWindowManager.Stub
        implements Watchdog.Monitor, WindowManagerPolicy.WindowManagerFuncs {
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    @Override
    public void registerPointerEventListener(PointerEventListener listener) {
        mPointerEventDispatcher.registerInputEventListener(listener);
    }

    @Override
    public void unregisterPointerEventListener(PointerEventListener listener) {
        mPointerEventDispatcher.unregisterInputEventListener(listener);
    }
}
}

       当在PhoneWindowManager的init()方法内部通过WindowManagerFuncs的registerPointerEventListener()进行注册时,会调用到WMS内部的实现方法,然后调用mPointerEventDispatcher的对应方法;
       根据第一章分析,mPointerEventDispatcher是在WMS的构造方法内部创建的,此时就建立了监听,当native层的手势事件传上来时,会通过mPointerEventDispatcher来回调所有的PointerEventListener实现者;

三.Native层处理

       关于触摸事件从接收、传递、处理在前面的几篇文章里面已经详细分析了,全局手势事件也是大致相同的处理流程,本章仅将涉及全局手势事件处理相关的点来进行分析;
       先从注册讲起,在第一章里面讲到,在执行nativeRegisterInputChannel()时,传入的最后参数Monitor是为true,看一下对应的实现路径:framework/services/core/jni/com_android_server_input_InputManagerService.cpp

static void nativeRegisterInputChannel(JNIEnv* env, jclass /* clazz */,
        jlong ptr, jobject inputChannelObj, jobject inputWindowHandleObj, jboolean monitor) {
    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);

    sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,
            inputChannelObj);

    sp<InputWindowHandle> inputWindowHandle =
            android_server_InputWindowHandle_getHandle(env, inputWindowHandleObj);

    status_t status = im->registerInputChannel(
            env, inputChannel, inputWindowHandle, monitor);
}

status_t NativeInputManager::registerInputChannel(JNIEnv* /* env */,
        const sp<InputChannel>& inputChannel,
        const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {
    return mInputManager->getDispatcher()->registerInputChannel(
            inputChannel, inputWindowHandle, monitor);
}

       根据调用关系,接着看一下InputDispatcher.cpp,对应的实现路径为:frameworks/native/services/inputflinger/InputDispatcher.cpp

status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,
        const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {

    { // acquire lock
        AutoMutex _l(mLock);
        sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);

        int fd = inputChannel->getFd();
        mConnectionsByFd.add(fd, connection);

        if (monitor) {
            mMonitoringChannels.push(inputChannel);
        }

        mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
    } // release lock

    // Wake the looper because some connections have changed.
    mLooper->wake();
    return OK;
}

       可以看到,在registerInputChannel()里面会进行判断,如果monitor为true,会向mMonitoringChannels队列里面push inputChannel,普通的窗口传入的monitor都是false,该mMonitoringChannels有什么作用呢?直接看代码:

void InputDispatcher::addMonitoringTargetsLocked(Vector<InputTarget>& inputTargets) {
    for (size_t i = 0; i < mMonitoringChannels.size(); i++) {
        inputTargets.push();

        InputTarget& target = inputTargets.editTop();
        target.inputChannel = mMonitoringChannels[i];
        target.flags = InputTarget::FLAG_DISPATCH_AS_IS;
        target.xOffset = 0;
        target.yOffset = 0;
        target.pointerIds.clear();
        target.scaleFactor = 1.0f;
    }
}

       在addMonitoringTargetsLocked()里面会遍历mMonitoringChannels来添加到inputTargets队列里面,看一下addMonitoringTargetsLocked()的调用处理,以dispatchMotionLocked()为例:

bool InputDispatcher::dispatchMotionLocked(
        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
    ;;;;;;;;;;;;;;;;;;;;;;;;;;

    bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;

    // Identify targets.
    Vector<InputTarget> inputTargets;

    bool conflictingPointerActions = false;
    int32_t injectionResult;
    if (isPointerEvent) {
        // Pointer event.  (eg. touchscreen)
        injectionResult = findTouchedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
    } else {
        // Non touch event.  (eg. trackball)
        injectionResult = findFocusedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime);
    }
    if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
        return false;
    }

    setInjectionResultLocked(entry, injectionResult);
    ;;;;;;;;;;;;;;;;;;;;;;;;;
    addMonitoringTargetsLocked(inputTargets);

    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

       通过前面的文章分析,InputDispatcher负责事件派发,在dispatchMotionLocked()里面来确定需要发送的inputTargets,在该方法内部执行了addMonitoringTargetsLocked(inputTargets),即在发现需要派发的inputTarget后,又添加了monitor inputTarget;

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
        EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {

    ALOG_ASSERT(eventEntry->dispatchInProgress); // should already have been set to true

    pokeUserActivityLocked(eventEntry);

    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);

        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);
        if (connectionIndex >= 0) {
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
        } else {
        }
    }
}

       在dispatchEventLocked()内部会遍历inputTargets进行发送,也就是说,当我们在触摸屏幕后事件在派发时,不仅会派发到当前focus窗口,也会发送到全局手势监听者,比如:屏幕左侧滑动退出应用,如果以模拟发送back键来实现时,在退出应用前,会看到应用内部也会响应左滑事件;

四.总结

       全局手势处理跟普通窗口处理流程大致是相同的,全局手势在任何界面下都会接收到,可以根据需求本地实现PointerEventListener来实现不同的全局手势,以上层实现流程图来结束本文章:


image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容