Flutter 支持圖片以及特殊文字的輸入框(二)實(shí)現(xiàn)過(guò)程

extended_text_field 相關(guān)文章

上一篇關(guān)于extended_text_field的文章主要介紹下用法,這篇文章介紹下,實(shí)現(xiàn)的過(guò)程。

image
image

過(guò)程

文字中插入圖片

關(guān)于怎么在文字里面加入圖片,在這篇文章里面我就不再介紹了,有興趣的同學(xué)可以先看一下Extended Text,原理是一毛一樣的。

鍵盤與輸入框的關(guān)聯(lián)

我寫的好多組件都是對(duì)官方組件的擴(kuò)展,所以對(duì)官方源碼一定要讀懂,知道它是做什么用的,才能在這個(gè)基礎(chǔ)上擴(kuò)展自己的功能。

image

除了工具類,其他都是從官方那邊copy過(guò)來(lái),然后進(jìn)行修改的。

我們先打開extended_editable_text.dart

image

可以看到它是繼承這個(gè)TextInputClient的,而TextInputClient是一個(gè)抽象類,而TextInputConnection是鍵盤的通信的關(guān)鍵先生,它將鍵盤的動(dòng)作反饋給TextInputClient,我們順便來(lái)看看它的實(shí)現(xiàn)。

class TextInputConnection {
  TextInputConnection._(this._client)
    : assert(_client != null),
      _id = _nextId++;

  static int _nextId = 1;
  final int _id;

  final TextInputClient _client;

  /// Whether this connection is currently interacting with the text input control.
  bool get attached => _clientHandler._currentConnection == this;

  /// Requests that the text input control become visible.
  void show() {
    assert(attached);
    SystemChannels.textInput.invokeMethod<void>('TextInput.show');
  }

  /// Requests that the text input control change its internal state to match the given state.
  void setEditingState(TextEditingValue value) {
    assert(attached);
    SystemChannels.textInput.invokeMethod<void>(
      'TextInput.setEditingState',
      value.toJSON(),
    );
  }

  /// Stop interacting with the text input control.
  ///
  /// After calling this method, the text input control might disappear if no
  /// other client attaches to it within this animation frame.
  void close() {
    if (attached) {
      SystemChannels.textInput.invokeMethod<void>('TextInput.clearClient');
      _clientHandler
        .._currentConnection = null
        .._scheduleHide();
    }
    assert(!attached);
  }
}

可以看到3里面的幾個(gè)方法都有調(diào)用
SystemChannels.textInput.invokeMethod

這種代碼是不是很熟悉,methodchannel,用過(guò)的人都知道,可以跟原生進(jìn)行交互,那么就很簡(jiǎn)單了。

text field會(huì)在點(diǎn)擊的時(shí)候獲得焦點(diǎn),并且打開鍵盤的鏈接,這樣就可以接受到鍵盤的響應(yīng),那么原生反饋Flutter是在哪里呢,是在_TextInputClientHandler _clientHandler這個(gè)里面.
我們也看看_TextInputClientHandler里面的代碼

class _TextInputClientHandler {
  _TextInputClientHandler() {
    SystemChannels.textInput.setMethodCallHandler(_handleTextInputInvocation);
  }

  TextInputConnection _currentConnection;

  Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
    if (_currentConnection == null)
      return;
    final String method = methodCall.method;
    final List<dynamic> args = methodCall.arguments;
    final int client = args[0];
    // The incoming message was for a different client.
    if (client != _currentConnection._id)
      return;
    switch (method) {
      case 'TextInputClient.updateEditingState':
        _currentConnection._client.updateEditingValue(TextEditingValue.fromJSON(args[1]));
        break;
      case 'TextInputClient.performAction':
        _currentConnection._client.performAction(_toTextInputAction(args[1]));
        break;
      case 'TextInputClient.updateFloatingCursor':
        _currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
        break;
      default:
        throw MissingPluginException();
    }
  }

  bool _hidePending = false;

  void _scheduleHide() {
    if (_hidePending)
      return;
    _hidePending = true;

    // Schedule a deferred task that hides the text input. If someone else
    // shows the keyboard during this update cycle, then the task will do
    // nothing.
    scheduleMicrotask(() {
      _hidePending = false;
      if (_currentConnection == null)
        SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
    });
  }
}

final _TextInputClientHandler _clientHandler = _TextInputClientHandler();

又是跟methodchannel一毛一樣,可以監(jiān)聽原生的回調(diào),其實(shí)啊,SystemChannels.textInput就是一個(gè)methodchannel

image

從上面代碼我們看到。如果進(jìn)行了鍵盤輸入,那么原生會(huì)通知flutter去updateEditingValue,并且把這個(gè)時(shí)候的數(shù)值轉(zhuǎn)遞過(guò)來(lái)

case 'TextInputClient.updateEditingState':
        _currentConnection._client.updateEditingValue(TextEditingValue.fromJSON(args[1]));
        break;

這個(gè)值是結(jié)構(gòu)是TextEditingValue,它包括了文本,光標(biāo)(選中)位置,以及composing(我的理解是,比如中文輸入的時(shí)候是字母,然后下面有下劃線,只有當(dāng)輸入完畢選擇的時(shí)候才會(huì)顯示成中文)

  /// The current text being edited.
  final String text;

  /// The range of text that is currently selected.
  final TextSelection selection;

  /// The range of text that is still being composed.
  final TextRange composing;

現(xiàn)在我們知道flutter的輸入框跟鍵盤是怎么進(jìn)行交互的了,總結(jié)一下,

  • 鍵盤通過(guò)TextInputConnection,執(zhí)行3個(gè)方法傳遞變化給輸入框
  /// Requests that this client update its editing state to the given value.
  void updateEditingValue(TextEditingValue value);

  /// Requests that this client perform the given action.
  void performAction(TextInputAction action);

  /// Updates the floating cursor position and state.
  void updateFloatingCursor(RawFloatingCursorPoint point);
  • 輸入框通過(guò)TextInputConnection,也可以把TextEditingValue傳遞給鍵盤,
  /// Requests that the text input control change its internal state to match the given state.
  void setEditingState(TextEditingValue value)
  
   /// Requests that the text input control become visible.
  void show() 
  
  /// Stop interacting with the text input control.
  ///
  /// After calling this method, the text input control might disappear if no
  /// other client attaches to it within this animation frame.
  void close()

接下來(lái)我們移動(dòng)到buildTextSpan 方法

  /// Builds [TextSpan] from current editing value.
  ///
  /// By default makes text in composing range appear as underlined.
  /// Descendants can override this method to customize appearance of text.
  TextSpan buildTextSpan(BuildContext context)

可以看到這里是將TextEditingValue轉(zhuǎn)換為了TextSpan,那么我們的機(jī)會(huì)是不是就來(lái)了,我們可以在這里通過(guò)SpecialTextSpanBuilder,把TextEditingValue的值轉(zhuǎn)換為我們想要的特殊的TextSpan.

TextSpan buildTextSpan(BuildContext context) {
    if (!widget.obscureText && _value.composing.isValid) {
      final TextStyle composingStyle = widget.style.merge(
        const TextStyle(decoration: TextDecoration.underline),
      );
      var beforeText = _value.composing.textBefore(_value.text);
      var insideText = _value.composing.textInside(_value.text);
      var afterText = _value.composing.textAfter(_value.text);

      if (supportSpecialText) {
        var before = widget.specialTextSpanBuilder
            .build(beforeText, textStyle: widget.style);
        var after = widget.specialTextSpanBuilder
            .build(afterText, textStyle: widget.style);

        List<TextSpan> children = List<TextSpan>();

        if (before != null && before.children != null) {
          _createImageConfiguration(<TextSpan>[before], context);
          before.children.forEach((sp) {
            children.add(sp);
          });
        } else {
          children.add(TextSpan(text: beforeText));
        }

        children.add(TextSpan(
          style: composingStyle,
          text: insideText,
        ));

        if (after != null && after.children != null) {
          _createImageConfiguration(<TextSpan>[after], context);
          after.children.forEach((sp) {
            children.add(sp);
          });
        } else {
          children.add(TextSpan(text: afterText));
        }

        return TextSpan(style: widget.style, children: children);
      }

      return TextSpan(style: widget.style, children: <TextSpan>[
        TextSpan(text: beforeText),
        TextSpan(
          style: composingStyle,
          text: insideText,
        ),
        TextSpan(text: afterText),
      ]);
    }

    String text = _value.text;
    if (widget.obscureText) {
      text = RenderEditable.obscuringCharacter * text.length;
      final int o =
          _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
      if (o != null && o >= 0 && o < text.length)
        text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
    }

    if (supportSpecialText) {
      var specialTextSpan =
          widget.specialTextSpanBuilder?.build(text, textStyle: widget.style);
      if (specialTextSpan != null) {
        _createImageConfiguration(<TextSpan>[specialTextSpan], context);
        return specialTextSpan;
      }
    }

    return TextSpan(style: widget.style, text: text);
  }

根據(jù)官方的源碼,我對(duì)各種情況進(jìn)行了處理,并且通過(guò)SpecialTextSpanBuilder將文本轉(zhuǎn)換了我們想要的TextSpan,為繪制做好準(zhǔn)備。

繪制過(guò)程

拿到TextSpan,那么下一步,我們就要準(zhǔn)備去繪制文字了,我們?nèi)タ纯?br> extended_render_editable.dart

大概看了下源碼,就感覺跟extended text 里面的extended_render_paragraph差別不大,區(qū)別是輸入框增加了對(duì)光標(biāo),以及選中背景的繪制。

那么套路都是一樣,找到_paintContents方法,我們將在這里繪制圖片以及一些特殊文本。

  • 源碼的繪制順序是 選中背景,光標(biāo),文本(當(dāng)然根據(jù)平臺(tái)不同,光標(biāo)和文本順序也不同),

  • 修改之后 繪制順序?yàn)?選中背景,特殊文本(圖片等),光標(biāo),文本(當(dāng)然根據(jù)平臺(tái)不同,光標(biāo)和文本順序也不同)

移動(dòng)到_paintSpecialText方法中,跟Extended Text一樣,支持圖片和自定義背景2種特殊文本,區(qū)別只是我只遍歷children,不會(huì)再到children的children里面去找特殊文本了

void _paintSpecialText(PaintingContext context, Offset offset) {
    if (!handleSpecialText) return;

    final Canvas canvas = context.canvas;

    canvas.save();

    ///move to extended text
    canvas.translate(offset.dx, offset.dy);

    ///we have move the canvas, so rect top left should be (0,0)
    final Rect rect = Offset(0.0, 0.0) & size;
    _paintSpecialTextChildren(text.children, canvas, rect);
    canvas.restore();
  }

  void _paintSpecialTextChildren(
      List<TextSpan> textSpans, Canvas canvas, Rect rect,
      {int textOffset: 0}) {
    if (textSpans == null) return;

    for (TextSpan ts in textSpans) {
      Offset topLeftOffset = getOffsetForCaret(
        TextPosition(offset: textOffset),
        rect,
      );
      //skip invalid or overflow
      if (topLeftOffset == null ||
          (textOffset != 0 && topLeftOffset == Offset.zero)) {
        return;
      }

      if (ts is ImageSpan) {
        ///imageSpanTransparentPlaceholder \u200B has no width, and we define image width by
        ///use letterSpacing,so the actual top-left offset of image should be subtract letterSpacing(width)/2.0
        Offset imageSpanOffset = topLeftOffset -
            Offset(getImageSpanCorrectPosition(ts, textDirection), 0.0);

        if (!ts.paint(canvas, imageSpanOffset)) {
          //image not ready
          ts.resolveImage(
              listener: (ImageInfo imageInfo, bool synchronousCall) {
            if (synchronousCall)
              ts.paint(canvas, imageSpanOffset);
            else {
              if (owner == null || !owner.debugDoingPaint) {
                markNeedsPaint();
              }
            }
          });
        }
      } else if (ts is BackgroundTextSpan) {
        var painter = ts.layout(_textPainter);
        Rect textRect = topLeftOffset & painter.size;
        Offset endOffset;
        if (textRect.right > rect.right) {
          int endTextOffset = textOffset + ts.toPlainText().length;
          endOffset = _findEndOffset(rect, endTextOffset);
        }

        ts.paint(canvas, topLeftOffset, rect,
            endOffset: endOffset, wholeTextPainter: _textPainter);
      }
//      else if (ts.children != null) {
//        _paintSpecialTextChildren(ts.children, canvas, rect,
//            textOffset: textOffset);
// 
     }
      textOffset += ts.toPlainText().length;
    }
  }

光標(biāo)以及交互的處理

我們處理了關(guān)聯(lián),繪制,最后我們需要處理光標(biāo)以及交互。

我們把眼光移動(dòng)到extended_text_selection.dart

ExtendedTextSelectionOverlay 跟它的名字一樣,它是OverlayEntry,主要是負(fù)責(zé)顯示那個(gè) 比如(copy,paste,select all)這種菜單的。

眼光再次移動(dòng)到 extended_text_field.dart

這個(gè)里面定義很多交互,它們有的用來(lái)移動(dòng)光標(biāo),有的用來(lái)選中文本,有的用來(lái)選中整個(gè)word。

child: IgnorePointer(
        ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true),
        child: TextSelectionGestureDetector(
          onTapDown: _handleTapDown,
          onForcePressStart:
              forcePressEnabled ? _handleForcePressStarted : null,
          onSingleTapUp: _handleSingleTapUp,
          onSingleTapCancel: _handleSingleTapCancel,
          onSingleLongTapStart: _handleSingleLongTapStart,
          onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
          onSingleLongTapEnd: _handleSingleLongTapEnd,
          onDoubleTapDown: _handleDoubleTapDown,
          onDragSelectionStart: _handleMouseDragSelectionStart,
          onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
          behavior: HitTestBehavior.translucent,
          child: child,
        ),
      ),
image

關(guān)鍵的點(diǎn)來(lái)了,因?yàn)槲覀儼盐谋巨D(zhuǎn)換為了特殊TextSpan,導(dǎo)致其實(shí)繪制的文字跟實(shí)際文本是不一樣的,比如對(duì)于圖片,之前它是"[1]"文本,但在繪制的時(shí)候它其實(shí)只是"",一個(gè)空的占位符號(hào)。

再詳細(xì)點(diǎn)的例子就是,比如我點(diǎn)擊在一個(gè)表情的后面,對(duì)于TextPainter來(lái)說(shuō),它告訴你的位置1,但是對(duì)于真實(shí)文本來(lái)說(shuō),它的位置應(yīng)該是3.

我們使用的真實(shí)值以及鍵盤的值是用TextEditingValue 來(lái)保存的,而我們繪畫文本是用TextSpan以及TextPainter來(lái)進(jìn)行計(jì)算的,所以我們需要給他們2者之間來(lái)一個(gè)轉(zhuǎn)換,讓我們把目光移動(dòng)到extended_text_field_utils.dart

在這個(gè)里面,我寫了雙方進(jìn)行轉(zhuǎn)換的方法,他們是以下方法

TextPosition convertTextInputPostionToTextPainterPostion(
    TextSpan text, TextPosition textPosition)
    
TextSelection convertTextInputSelectionToTextPainterSelection(
    TextSpan text, TextSelection selection)

TextPosition convertTextPainterPostionToTextInputPostion(
    TextSpan text, TextPosition textPosition)
    
TextSelection convertTextPainterSelectionToTextInputSelection(
    TextSpan text, TextSelection selection)

其實(shí)道理很簡(jiǎn)單,就是雙方文字的差異就是這個(gè)光標(biāo)表示方法的差異,就像上面的例子,"[1]" 和 ""之間差距是2,這就會(huì)導(dǎo)致它們表示的光標(biāo)位置差距也是2,根據(jù)這個(gè)原理我們就可以把它們進(jìn)行互相的轉(zhuǎn)換了。

感興趣的同學(xué)可以去看看代碼,如果有更優(yōu)化的解放,請(qǐng)告訴我一下,謝謝。

其他的坑

  • 圖片光標(biāo)以及選中背景的位置問(wèn)題

因?yàn)镮mageSpan的做法是使用\u200B(ZERO WIDTH SPACE,就是寬帶為0的空白),而使用letterSpacing當(dāng)作寬度,所以通過(guò)
TextPainter計(jì)算出來(lái)的位置,是在letterSpacing的中間,圖片繪畫的地方應(yīng)該要向前移動(dòng)width / 2.0。也就是說(shuō)如果光標(biāo)在圖片前,要向前移動(dòng)width / 2.0。如果光標(biāo)在圖片之后,要向后移動(dòng)width / 2.0。
對(duì)于選中背景也是同樣的道理。

// zmt
    double imageTextSpanWidth = 0.0;
    Offset imageSpanEndCaretOffset;
    if (handleSpecialText) {
      var textSpan = text.getSpanForPosition(textPosition);
      if (textSpan != null) {
        if (textSpan is ImageSpan) {
          if (textInputPosition.offset >= textSpan.start &&
              textInputPosition.offset < textSpan.end) {
            imageTextSpanWidth -=
                getImageSpanCorrectPosition(textSpan, textDirection);
          } else if (textInputPosition.offset == textSpan.end) {
            ///_textPainter.getOffsetForCaret is not right.
            imageSpanEndCaretOffset = _textPainter.getOffsetForCaret(
                  TextPosition(
                      offset: textPosition.offset - 1,
                      affinity: textPosition.affinity),
                  effectiveOffset & size,
                ) +
                Offset(
                    getImageSpanCorrectPosition(textSpan, textDirection), 0.0);
          }
        }
      } else {
        //handle image text span is last one, textPainter will get wrong offset
        //last one
        textSpan = text.children?.last;
        if (textSpan != null && textSpan is ImageSpan) {
          imageSpanEndCaretOffset = _textPainter.getOffsetForCaret(
                TextPosition(
                    offset: textPosition.offset - 1,
                    affinity: textPosition.affinity),
                effectiveOffset & size,
              ) +
              Offset(getImageSpanCorrectPosition(textSpan, textDirection), 0.0);
        }
      }
    }

    final Offset caretOffset = (imageSpanEndCaretOffset ??
            _textPainter.getOffsetForCaret(textPosition, _caretPrototype) +
                Offset(imageTextSpanWidth, 0.0)) +
        effectiveOffset;
  • 特殊文本輸入時(shí)候的光標(biāo)修正

因?yàn)橹С质謩?dòng)輸入也要轉(zhuǎn)換特殊文本,所以存在這種情況。

image

我先輸入了[],再把光標(biāo)移動(dòng)到中間,輸入1,這個(gè)時(shí)候會(huì)轉(zhuǎn)換為表情1,但是光標(biāo)沒有停留在表情之后,如果你這個(gè)時(shí)候再輸入,它就會(huì)在1后面增加。對(duì)于這種情況,我們要做一下處理。

///correct caret Offset
///make sure caret is not in image span
TextEditingValue correctCaretOffset(TextEditingValue value, TextSpan textSpan,
    TextInputConnection textInputConnection) {
  if (value.selection.isValid && value.selection.isCollapsed) {
    int caretOffset = value.selection.extentOffset;
    var imageSpans = textSpan.children.where((x) => x is ImageSpan);
    //correct caret Offset
    //make sure caret is not in image span
    for (ImageSpan ts in imageSpans) {
      if (caretOffset > ts.start && caretOffset < ts.end) {
        //move caretOffset to end
        caretOffset = ts.end;
        break;
      }
    }

    ///tell textInput caretOffset is changed.
    if (caretOffset != value.selection.baseOffset) {
      value = value.copyWith(
          selection: value.selection
              .copyWith(baseOffset: caretOffset, extentOffset: caretOffset));
      textInputConnection?.setEditingState(value);
    }
  }
  return value;
}

當(dāng)光標(biāo)位置處于表情文字中間的時(shí)候,我們把光標(biāo)移動(dòng)到表情的后面去,并且通知鍵盤,光標(biāo)位置變化了。這樣我們?cè)倮^續(xù)輸入的時(shí)候,就沒有問(wèn)題了。

  • getFullHeightForCaret api在低版本不支持

TextPainter的getFullHeightForCaret 在低版本上面不支持,如果你是適合的版本建議打開下面的注釋,這樣光標(biāo)的高度會(huì)更舒服。

    ///zmt
    ///1.5.7
    ///under lower version of flutter, getFullHeightForCaret is not support
    ///
    // Override the height to take the full height of the glyph at the TextPosition
    // when not on iOS. iOS has special handling that creates a taller caret.
    // TODO(garyq): See the TODO for _getCaretPrototype.
//    if (defaultTargetPlatform != TargetPlatform.iOS &&
//        _textPainter.getFullHeightForCaret(textPosition, _caretPrototype) !=
//            null) {
//      caretRect = Rect.fromLTWH(
//        caretRect.left,
//        // Offset by _kCaretHeightOffset to counteract the same value added in
//        // _getCaretPrototype. This prevents this from scaling poorly for small
//        // font sizes.
//        caretRect.top - _kCaretHeightOffset,
//        caretRect.width,
//        _textPainter.getFullHeightForCaret(textPosition, _caretPrototype),
//      );
//    }

廣告時(shí)間

當(dāng)這5個(gè)都介紹完畢的時(shí)候,我們就講的差不多了,為了方便大家查看我修改的地方,你只需要搜索 zmt ,就能快速找到我為支持?jǐn)U展功能而添加的代碼了。

image

最后放上 extended_text_field,如果你有什么不明白或者對(duì)這個(gè)方案有什么改進(jìn)的地方,請(qǐng)告訴我,歡迎加入Flutter Candies,一起生產(chǎn)可愛的Flutter 小糖果(QQ群:181398081)

最最后放上Flutter Candies全家桶,真香。

custom flutter candies(widgets) for you to easily build flutter app, enjoy it.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,527評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,656評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,853評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,103評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,487評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,815評(píng)論 2 372

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