Flutter笔记-深入分析GestureDetector

android中存在事件冲突,flutter其实也存在,但是官方友好的出了一个控件(GestureDetector)来解决这个问题
在事件分发那里,分析了基础手势控件Listener的事件传递(传送门),我们先看下面这个例子:

class TouchDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Listener(
      child: Container(
        color: Colors.blue,
        width: 300,
        height: 300,
        child: Stack(
          //将布局按比例分成坐标,原点(0,0)位于中间,(-1,-1)为左上
          alignment: Alignment(0, 0),
          children: <Widget>[
            Listener(
              child: Container(
                color: Colors.red,
                width: 100,
                height: 100,
              ),
              onPointerDown: (event){
                print("内层");
              },
            ),
          ],
        )
      ),
      onPointerDown: (event){
        print("外层");
      },
    );
  }
}

这个界面简单,就是一个大的正方形里有一个红色的小正方形。当我们点击红色正方形时:

I/flutter ( 4898): 内层
I/flutter ( 4898): 外层

打印结果是先内层,再外层。根据事件分发里Listener的分析,可以知道result列表的添加顺序是先内层,再外层
Ok,进入正题,GestureDetector如何解决事件冲突的?

GestureDetector

GestureDetector是一个StatelessWidget,直接看build方法

class GestureDetector extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    //保存手势识别工厂的map
    //GestureRecognizerFactory保存GestureRecognizer的构造方法和init方法
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    //单击事件
    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        //1.构造方法
        () => TapGestureRecognizer(debugOwner: this),
        //2.初始化方法
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel;
        },
      );
    }
    //双击事件
    if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance
            ..onDoubleTap = onDoubleTap;
        },
      );
    }
    //长按事件
    if (onLongPress != null || onLongPressUp !=null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
        () => LongPressGestureRecognizer(debugOwner: this),
        (LongPressGestureRecognizer instance) {
          instance
            ..onLongPress = onLongPress
            ..onLongPressUp = onLongPressUp;
        },
      );
    }
    //垂直拖拽事件
    if (onVerticalDragDown != null ||
        onVerticalDragStart != null ||
        onVerticalDragUpdate != null ||
        onVerticalDragEnd != null ||
        onVerticalDragCancel != null) {
      gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
        () => VerticalDragGestureRecognizer(debugOwner: this),
        (VerticalDragGestureRecognizer instance) {
          instance
            ..onDown = onVerticalDragDown
            ..onStart = onVerticalDragStart
            ..onUpdate = onVerticalDragUpdate
            ..onEnd = onVerticalDragEnd
            ..onCancel = onVerticalDragCancel;
        },
      );
    }
     //水平拖拽事件
    if (onHorizontalDragDown != null ||
        onHorizontalDragStart != null ||
        onHorizontalDragUpdate != null ||
        onHorizontalDragEnd != null ||
        onHorizontalDragCancel != null) {
      gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
        () => HorizontalDragGestureRecognizer(debugOwner: this),
        (HorizontalDragGestureRecognizer instance) {
          instance
            ..onDown = onHorizontalDragDown
            ..onStart = onHorizontalDragStart
            ..onUpdate = onHorizontalDragUpdate
            ..onEnd = onHorizontalDragEnd
            ..onCancel = onHorizontalDragCancel;
        },
      );
    }
    //同时水平和垂直拖拽事件
    if (onPanDown != null ||
        onPanStart != null ||
        onPanUpdate != null ||
        onPanEnd != null ||
        onPanCancel != null) {
      gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
        () => PanGestureRecognizer(debugOwner: this),
        (PanGestureRecognizer instance) {
          instance
            ..onDown = onPanDown
            ..onStart = onPanStart
            ..onUpdate = onPanUpdate
            ..onEnd = onPanEnd
            ..onCancel = onPanCancel;
        },
      );
    }
    //缩放手势事件
    if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
      gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
        () => ScaleGestureRecognizer(debugOwner: this),
        (ScaleGestureRecognizer instance) {
          instance
            ..onStart = onScaleStart
            ..onUpdate = onScaleUpdate
            ..onEnd = onScaleEnd;
        },
      );
    }

    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
}

在gestures中,已经将回调事件方法分好了手势识别类,继续往下看

class RawGestureDetector extends StatefulWidget {
  ...
  @override
  RawGestureDetectorState createState() => RawGestureDetectorState();
}

查看state的build方法

class RawGestureDetectorState extends State<RawGestureDetector> {
  ...
  @override
  Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child
    );
    //excludeFromSemantics默认为false的
    if (!widget.excludeFromSemantics)
      result = _GestureSemantics(owner: this, child: result);
    return result;
  }

  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    //给每个手势识别类添加手指按下事件
    for (GestureRecognizer recognizer in _recognizers.values)
      //关键
      recognizer.addPointer(event);
  }
}

GestureRecognizer的子类比较多,这里我们选水平滑动HorizontalDragGestureRecognizer进行分析, addPointer位于其父类DragGestureRecognizer中:

@override
  void addPointer(PointerEvent event) {
    //开始追踪手势
    startTrackingPointer(event.pointer);
    //惯性滑动追踪类
    _velocityTrackers[event.pointer] = VelocityTracker();
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
      _initialPosition = event.position;
      _pendingDragOffset = Offset.zero;
      _lastPendingEventTimestamp = event.timeStamp;
      if (onDown != null)  //回调方法onDown
        invokeCallback<void>('onDown', () => onDown(DragDownDetails(globalPosition: _initialPosition)));
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
    }
  }

_DragState共三个状态:ready(准备,初始值)、possible(正在手势竞争中)、accepted(竞争胜利)

@protected
void startTrackingPointer(int pointer) {
  //handleEvent为一个函数
  GestureBinding.instance.pointerRouter
    .addRoute(pointer, handleEvent);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  //每个手势的入场券列表赋值
  _entries[pointer] = _addPointerToArena(pointer);
}

addRoute传递的handleEvent为一个函数,这个函数后面再分析

a. pointerRouter中的_routeMap赋值

_routeMap的值就是通过addRoute方法来添加的

typedef PointerRoute = void Function(PointerEvent event);
//参数二为handleEvent函数
void addRoute(int pointer, PointerRoute route) {
    final LinkedHashSet<PointerRoute> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<PointerRoute>());
    assert(!routes.contains(route));
    routes.add(route);
  }

b. gestureArena中的_arenas赋值

回到前面,继续看_addPointerToArena方法

GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
      return _team.add(pointer, this);
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }

基本和上面的添加方法是一致的

GestureArenaEntry add(int pointer, GestureArenaMember member) {
    //put: 重复的key覆盖
    //putIfAbsent: 重复的key不添加
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
      return _GestureArena();
    });
    state.add(member);
    assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
    //返回了GestureArenaEntry对象,持有手势竞技场管理类对象、手势的id、手势识别类的对象
    return GestureArenaEntry._(this, pointer, member);
  }

_instance是GestureBinding中的一个静态对象,构造函数中对其赋值,还记得事件分发那最终分析到handleEvent:

final PointerRouter pointerRouter = PointerRouter();
final GestureArenaManager gestureArena = GestureArenaManager();

@override 
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  }
}

之前到了这一步就没有继续追踪下去,其实内部还通过列表进行了操作,看下route方法做了什么

  void route(PointerEvent event) {
    //根据前面的分析,_routeMap列表中已经有数据了
    final LinkedHashSet<PointerRoute> routes = _routeMap[event.pointer];
    final List<PointerRoute> globalRoutes = List<PointerRoute>.from(_globalRoutes);
    if (routes != null) {
      //遍历列表
      for (PointerRoute route in List<PointerRoute>.from(routes)) {
        if (routes.contains(route))
           //执行
          _dispatch(event, route);
      }
    }
    for (PointerRoute route in globalRoutes) {
      if (_globalRoutes.contains(route))
        _dispatch(event, route);
    }
  }

PointerRoute是一个带参数无返回类型的函数(注:PointerRouter是一个类)

void _dispatch(PointerEvent event, PointerRoute route){
  try {
    //执行匿名函数,即handleEvent函数
    route(event);
  } catch (exception, stack) {
    ...
  }
}

执行HorizontalDragGestureRecognizer中的handleEvent

@override
  void handleEvent(PointerEvent event) {
    assert(_state != _DragState.ready);
    if (!event.synthesized
        && (event is PointerDownEvent || event is PointerMoveEvent)) {
      final VelocityTracker tracker = _velocityTrackers[event.pointer];
      assert(tracker != null);
      tracker.addPosition(event.timeStamp, event.position);
    }
    //手势为PointerMoveEvent
    if (event is PointerMoveEvent) {
      final Offset delta = event.delta;
      if (_state == _DragState.accepted) {
        //如果手势竞争胜利,则回调onUpdate方法
        if (onUpdate != null) {
          invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
            sourceTimeStamp: event.timeStamp,
            delta: _getDeltaForDetails(delta),
            primaryDelta: _getPrimaryValueFromOffset(delta),
            globalPosition: event.position,
          )));
        }
      } else {
        //手势开始竞争判断
        _pendingDragOffset += delta;
        _lastPendingEventTimestamp = event.timeStamp;
        //当添加满足,则决策判断为accepted
        if (_hasSufficientPendingDragDeltaToAccept)
          resolve(GestureDisposition.accepted);
      }
    }
    //如果是up或canel事件,则删除路由,即pointerRouter移除此handleEvent,即此次事件不再调用
    stopTrackingIfPointerNoLongerDown(event);
  }

_hasSufficientPendingDragDeltaToAccept如何为true呢?

const double kTouchSlop = 18.0; 
@override
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dx.abs() > kTouchSlop;

只要移动的x差值绝对值大于18符合条件,垂直滑动则为dy,至于PanGestureRecognizer:

const double kPanSlop = kTouchSlop * 2.0; 
@override
bool get _hasSufficientPendingDragDeltaToAccept {
  //斜边的绝对值大于2倍的kTouchSlop
  return _pendingDragOffset.distance > kPanSlop;
}

由上面可以得出,当down手势传递过来,只记录速度,而move手势传递过来,列表中所有的DragGestureRecognizer类都会调用handleEvent

如果此时列表中同时HorizontalDragGestureRecognizer和VerticalDragGestureRecognizer,那么它们谁获得胜利呢?

主要看dx和dy的差值谁先大于kTouchSlop,但如果正好在斜45°移动时,dx和dy差值变化是一样的,这是主要看它们加入列表的先后顺序决定,先添加的获胜

_state 状态修改为_DragState.accepted过程

假如此时已经满足条件,进入resolve方法

void resolve(GestureDisposition disposition) {
    //_entries前面流程创建了,_entries.values键值对中取出全部的value列表
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }

会遍历所有 GestureArenaEntry 的 resolve

class GestureArenaEntry {
  GestureArenaEntry._(this._arena, this._pointer, this._member);

  final GestureArenaManager _arena; //当前竞技场管理者
  final int _pointer;  //手势的id
  final GestureArenaMember _member; //即HorizontalDragGestureRecognizer
  void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
}

管理者管理所有的事件

void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    //省略断言
    ...
    //state.members为当前竞技场里所有手势识别类的列表
    //所有不满足的类移出列表
    if (disposition == GestureDisposition.rejected) {
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen)
        _tryToResolveArena(pointer, state);
    } else {
      assert(disposition == GestureDisposition.accepted);
      //默认为true,当竞技场close是修改为false,此时为false
      if (state.isOpen) {
        //记录获胜的手势识别类
        state.eagerWinner ??= member;
      } else {
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
        //调用
        _resolveInFavorOf(pointer, state, member);
      }
    }

state.isOpen这个值在PointerDownEvent手势后会修改为false

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    //省略断言
    ...
    _arenas.remove(pointer);
    for (GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
  }

其他的手势识别类调用rejectGesture,而胜者调用acceptGesture

@override
  void acceptGesture(int pointer) {
    if (_state != _DragState.accepted) {
      //修改状态
      _state = _DragState.accepted;
      final Offset delta = _pendingDragOffset;
      final Duration timestamp = _lastPendingEventTimestamp;
      _pendingDragOffset = Offset.zero;
      _lastPendingEventTimestamp = null;
      //回调onStart方法
      if (onStart != null) {
        invokeCallback<void>('onStart', () => onStart(DragStartDetails(
          sourceTimeStamp: timestamp,
          globalPosition: _initialPosition,
        )));
      }
      //回调onUpdate方法
      if (delta != Offset.zero && onUpdate != null) {
        final Offset deltaForDetails = _getDeltaForDetails(delta);
        invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
          sourceTimeStamp: timestamp,
          delta: deltaForDetails,
          primaryDelta: _getPrimaryValueFromOffset(delta),
          globalPosition: _initialPosition + deltaForDetails,
        )));
      }
    }
  }

胜利后,因为修改了状态,所以之后的move手势不会再走这一步,在前面的方法就直接回调onUpdate方法
再来看看竞争失败后会怎么样

@override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

@protected
  void stopTrackingPointer(int pointer) {
    if (_trackedPointers.contains(pointer)) {
      //删除路由
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
     //移出手势id
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
  }

isOpen值修改

回到最开始,gestureArena.close(event.pointer)中

void close(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    //关闭竞技场,等待胜者出来
    state.isOpen = false;
    assert(_debugLogDiagnostic(pointer, 'Closing', state));
    _tryToResolveArena(pointer, state);
  }

查看 _tryToResolveArena方法

void _tryToResolveArena(int pointer, _GestureArena state) {
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    //只剩一个,即无竞争者
    if (state.members.length == 1) {
      //直接调用
      scheduleMicrotask(() => _resolveByDefault(pointer, state));
    } else if (state.members.isEmpty) {
      //无识别类对象
      _arenas.remove(pointer);
      assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
    } else if (state.eagerWinner != null) {
      //得出胜利者
      assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
      //移出胜利者之外的其他手势识别类
      _resolveInFavorOf(pointer, state, state.eagerWinner);
    }
  }

而gestureArena.sweep(event.pointer)则是清除所有的路由和记录(当无选手hold)

hold和release

目前源码里只DoubleTapGestureRecognizer使用

void hold(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    state.isHeld = true;
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
  }

  void release(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; 
    state.isHeld = false;
    assert(_debugLogDiagnostic(pointer, 'Releasing', state));
    if (state.hasPendingSweep)
      sweep(pointer);
  }

hold后,状态isHeld修改为true,在sweep中就不会清理

void sweep(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return;
    assert(!state.isOpen);
    //hold,直接返回,不做清空处理
    if (state.isHeld) {
      //修改状态,在release方法中用于重新调用sweep方法
      state.hasPendingSweep = true;
      assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
      return; 
    }
    assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
      //第一个胜者接收手势
      state.members.first.acceptGesture(pointer);
      //所有选手退出竞技场
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }

来看一下DoubleTapGestureRecognizer中具体怎么使用

void _handleEvent(PointerEvent event) {
    final _TapTracker tracker = _trackers[event.pointer];
    assert(tracker != null);
    if (event is PointerUpEvent) {
      if (_firstTap == null)
        //这里会hold,让竞技场等待下一次分出胜负
        _registerFirstTap(tracker);
      else
        //调用release,释放
        _registerSecondTap(tracker);
    } else if (event is PointerMoveEvent) {
      if (!tracker.isWithinTolerance(event, kDoubleTapTouchSlop))
        _reject(tracker);
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
    }
  }

记录第一次点击

void _registerFirstTap(_TapTracker tracker) {
    //开启计时器,超时时间为300ms
    _startDoubleTapTimer();
    //调用hold
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _clearTrackers();
    //记录
    _firstTap = tracker;
  }

记录第二次点击

void _registerSecondTap(_TapTracker tracker) {
    _firstTap.entry.resolve(GestureDisposition.accepted);
    tracker.entry.resolve(GestureDisposition.accepted);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    //双击回调方法是否存在
    if (onDoubleTap != null)
      invokeCallback<void>('onDoubleTap', onDoubleTap);
    _reset();
  }

在_reset() 清空记录,状态重置

void _reset() {
    //停止计时器
    _stopDoubleTapTimer();
    if (_firstTap != null) {
       //清空firstTap
       final _TapTracker tracker = _firstTap;
      _firstTap = null;
      _reject(tracker);
     //调用release
      GestureBinding.instance.gestureArena.release(tracker.pointer);
    }
    _clearTrackers();
  }

总结

  1. 路由在Listener的onPointerDown建立,在gestureArena的sweep清空(可能在这之前已经清空)
  2. 一个完整的事件由down手势开始、up手势结束。down手势时列表中登记所有要竞争的手势识别类(竞技选手),竞技场管理者进行管理,后续的事件会通过路由依次传递到列表的手势识别类中,当登记手势识别类不满足时会退出竞争

GestureArenaManager:竞技场管理者,根据Pointer的多少来构建场地

  • close:竞技场关闭,PointerDown手势触发
  • sweep:竞技场清理,PointerUp手势触发
  • hold:保持,sweep时不会清理
  • release:释放,取消保持,会调用sweep
    _GestureArena:竞技场场地
    GestureArenaEntry:竞技场入场券
    GestureRecognizer:竞技选手
  • acceptGesture: 胜者回调
  • rejectGesture: 败者回调
  • addPointer: 添加手势,启动追踪
  • invokeCallback: 回调方法
class GestureArenaManager {
    final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
}

class _GestureArena {
    final List<GestureArenaMember> members = <GestureArenaMember>[];
}

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

推荐阅读更多精彩内容