Flutter 中自定義鍵盤

相關API解讀

  • 背景
  • 1.基礎 API
  • 2.Window
  • 3.BindingBase
  • 4.ServicesBinding
  • 5.Flutter 中如何彈出系統鍵盤?
  • 6.InsightBank 如何彈出自定義鍵盤
  • 7.如何解決自定義鍵盤與原生鍵盤互相切換問題圈膏?

背景

這是關于 bug 的故事梭灿。
為了解決自定義鍵盤與原生鍵盤互相切換bottom計算不準確的問題曹铃,研究了系統鍵盤與自定義鍵盤是如何工作的秸仙。

  1. didChangeMetrics() 如何計算鍵盤的高度
  2. debug 模式切換鍵盤沒有問題飞蚓,到 relase 版本下卻不行了踱稍?

一弟跑、基礎 API

  • TextInputType: 定義各種鍵盤的類型灾前,與 Native中接口相同(number、phone窖认、email...)
  • CustomTextInputType:目前只有Android平臺使用豫柬。繼承 TextInputType 自定義鍵盤類型告希。
  • FocusNode: StatefullWidget 可以使用一個對象來獲取鍵盤焦點和處理鍵盤事件。
  • TextEditingController: 增刪改查烧给。輸入的信息(需要在 dispos 中回收資源)燕偶。
  • InputController: validate用于檢測文本信息

二、 window

它公開了顯示的大小础嫡,核心調度程序API指么,輸入事件回調(語言改變、 textScaleFactor 改變回調 )榴鼎,圖形繪制API以及其他此類核心服務伯诬。
MediaQuery 的 MediaQueryData 來源于 WidgetsBinding.instance.window
https://flutter.github.io/assets-for-api-docs/assets/widgets/window_padding.mp4

  • Window.viewInsets: 系統為系統UI(例如鍵盤)保留的物理像素,它將完全遮蓋該區(qū)域中繪制的所有內容巫财。onMetricsChanged 可以監(jiān)聽到發(fā)生改變
  • Window.viewPadding:是顯示器每一側的物理像素盗似,可能會由于系統UI或顯示器的物理侵入而被部分遮擋
  • Window.padding: 它將允許viewInsets插圖消耗視圖填充

Window.viewInsets 更新流程

image.png

  1. Engine 通過 _updateWindowMetrics() 更新 window
  2. window 通過 handleMetricsChanged() 調用 RendererBingdingwidgetsBinding 去更新UI,回調 didChangeMetrics()

三平项、BindingBase

  • 各種Binding服務 minxin 的基類
  • 提供window
  • 提供鎖事件處理赫舒,用于熱加載。熱加載時會把所有輸入事件鎖起來闽瓢,等熱加載完成再執(zhí)行接癌。
/// 提供單例服務的mixin們的基類
///
/// 使用on繼承此類并實現initInstances方法 這個mixin在app的生命周期內只能被構建一次,在checked
/// 模式下會對此斷言
///
/// 用于編寫應用程序的最頂層將具有一個繼承自[BindingBase]并使用所有各種[BindingBase] 
/// mixins(例如[ServicesBinding])的具體類扣讼。
/// 比如Flutter中的Widgets庫引入了一個名為[WidgetsFlutterBinding]的binding缺猛,定義了如何綁定
/// 可以隱性(例如,[WidgetsFlutterBinding]從[runApp]啟動)椭符,或者需要應用程序顯式調用構造函數
abstract class BindingBase {
  BindingBase() {
    initInstances();//在構造函數里進行初始化
    initServiceExtensions();//初始化擴展服務
  }

  ui.Window get window => ui.window;//提供window
  
  @protected
  @mustCallSuper
  void initInstances() {//初始化荔燎,其他binding mixin可以重寫此類
  }

  @protected
  @mustCallSuper
  void initServiceExtensions() {}//用于子類重寫該方法,用于注冊一些擴展服務艰山。

  @protected
  bool get locked => _lockCount > 0;
  int _lockCount = 0;

/// /鎖定異步事件和回調的分派湖雹,直到回調的未來完成為止咏闪。
/// 這會導致輸入滯后曙搬,因此應盡可能避免。 它主要用于非用戶交互時間
  @protected
  Future<void> lockEvents(Future<void> callback()) {
    developer.Timeline.startSync('Lock events');

    _lockCount += 1;
    final Future<void> future = callback();
    future.whenComplete(() {
      _lockCount -= 1;
      if (!locked) {
        unlocked();
      }
    });
    return future;
  }

  @protected
  @mustCallSuper
  void unlocked() {//解鎖
    assert(!locked);
  }

/// 開發(fā)過程中使用
///
/// 使整個應用程序重新繪制鸽嫂,例如熱裝后纵装。
///通過發(fā)送`ext.flutter.reassemble`服務擴展信號來手動進行
/// 在此方法運行時,事件被鎖定(例如据某,指針事件未鎖定)
  Future<void> reassembleApplication() {
    return lockEvents(performReassemble);
  }

/// 不會繼續(xù)指向舊代碼橡娄,并刷新以前計算的值的所有緩存,以防新代碼對它們進行不同的計算癣籽。
/// 例如挽唉,渲染層在調用時觸發(fā)整個應用程序重新繪制滤祖。
  @mustCallSuper
  @protected
  Future<void> performReassemble() {//重繪方法,需要重寫
    return Future<void>.value();
  }
    ···省略一些服務擴展方法
}

其他 Bingding

各種Bingding.png

  • GestureBinding:提供了window.onPointerDataPacket 回調瓶籽,綁定Framework手勢子系統匠童,是Framework事件模型與底層事件的綁定入口。
  • ServicesBinding:提供了window.onPlatformMessage 回調塑顺, 用于綁定平臺消息通道(message channel)汤求,主要處理原生和Flutter通信。
  • SchedulerBinding:提供了window.onBeginFrame和window.onDrawFrame回調严拒,監(jiān)聽刷新事件扬绪,綁定Framework繪制調度子系統。
  • PaintingBinding:綁定繪制庫裤唠,主要用于處理圖片緩存挤牛。
  • SemanticsBinding:語義化層與Flutter engine的橋梁,主要是輔助功能的底層支持种蘸。
  • RendererBinding:提供了window.onMetricsChanged 赊颠、window.onTextScaleFactorChanged 等回調。它是渲染樹與Flutter engine的橋梁劈彪。
  • WidgetsBinding: 提供了window.onLocaleChanged竣蹦、onBuildScheduled 等回調。它是Flutter widget層與engine的橋梁沧奴。

四痘括、ServicesBinding mixin

偵聽平臺消息,并將其定向到defaultBinaryMessenger滔吠。另注冊了 LicenseEntryCollector 做證書相關處理纲菌。
DefaultBinaryMessenger 繼承自 BinaryMessenger,BinaryMessenger 是一個信使疮绷,它通過Flutter平臺發(fā)送二進制數據翰舌。

image.png

DefaultBinaryMessenger 繼承自 BinaryMessenger

/// 一個信使,它通過Flutter平臺發(fā)送二進制數據冬骚。

abstract class BinaryMessenger {
  /// A const constructor to allow subclasses to be const.
  const BinaryMessenger();

  /// 設置一個回調椅贱,以在給定通道上接收來自平臺插件的消息,而無需對其進行解碼只冻。 
  /// 給定的回調將替換當前注冊的回調頻道(如果有)庇麦。 要刪除處理程序,請將null作為[handler]參數傳遞喜德。
  void setMessageHandler(String channel, Future<ByteData> handler(ByteData message));
  
  /// 與上述類似山橄,但不會回調native端
  void setMockMessageHandler(String channel, Future<ByteData> handler(ByteData message));

  /// 將二進制消息發(fā)送到給定通道上的平臺插件。返回一個二進制的Future
  Future<ByteData> send(String channel, ByteData message);

  /// 處理 native 端發(fā)來的消息
  Future<void> handlePlatformMessage(String channel, ByteData data, ui.PlatformMessageResponseCallback callback);

}

DefaultBinaryMessenger

image.png

另外 PlatformBinaryMessenger 與 DefaultBinaryMessenger 實現類似舍悯,不同點在于 PlatformBinaryMessenger 不支持 setMockMessageHandler() 航棱。

  • setMessageHandler:以 channel 為 key睡雇,MessageHandler 為 value ,放入 ChannelBuffer 中饮醇。
  • setMockMessageHandler: 以 channel 為 key入桂,MessageHandler 為 value 。實現自定義 channel 方法驳阎。
  • ChannelBuffer: 允許在 Engine 和 Framework 之間存儲消息抗愁。在 Framework 處理之前,消息會一直存儲呵晚。
/// The default implementation of [BinaryMessenger].
/// 發(fā)送消息到 native蜘腌,并處理 native 返回的消息
class _DefaultBinaryMessenger extends BinaryMessenger {
  const _DefaultBinaryMessenger._();

  static final Map<String, MessageHandler> _handlers =
      <String, MessageHandler>{};

  static final Map<String, MessageHandler> _mockHandlers =
      <String, MessageHandler>{};

  Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
    final Completer<ByteData> completer = Completer<ByteData>();
    ///  ui.window.sendPlatformMessage 調用 C++ 的方法。
    ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
      try {
        completer.complete(reply);
      } catch (exception, stack) {
      ...
      }
    });
    return completer.future;
  }

  @override
  Future<void> handlePlatformMessage(
    String channel,
    ByteData data,
    ui.PlatformMessageResponseCallback callback,
  ) async {
    ByteData response;
    try {
      final MessageHandler handler = _handlers[channel];
      if (handler != null) {
        response = await handler(data);
      } else {
        /// 存儲通道消息饵隙,直到通道被完全路由為止撮珠,即,當消息處理程序附加到框架側的通道時金矛。
        ui.channelBuffers.push(channel, data, callback);
        callback = null;
      }
    } catch (exception, stack) {
    ...
    } finally {
      if (callback != null) {
        callback(response);
      }
    }
  }

  @override
  Future<ByteData> send(String channel, ByteData message) {
    final MessageHandler handler = _mockHandlers[channel];
    if (handler != null)
      return handler(message);
    return _sendPlatformMessage(channel, message);
  }

  @override
  void setMessageHandler(String channel, MessageHandler handler) {
    if (handler == null)
      _handlers.remove(channel);
    else
      _handlers[channel] = handler;
    /// 允許在引擎和框架之間存儲消息芯急。
    ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
      await handlePlatformMessage(channel, data, callback);
    });
  }

  @override
  void setMockMessageHandler(String channel, MessageHandler handler) {
    if (handler == null)
      _mockHandlers.remove(channel);
    else
      _mockHandlers[channel] = handler;
  }
}

五、Flutter 中如何彈出系統鍵盤驶俊?

image.png
  • 1.從 FocusManager 開始娶耍,requestFocus() 獲得 inputWidget 輸入的焦點
FocusScope.of(context).requestFocus(focusNode)
    1. FocusManager 調用內部的 _listeners 列表, 執(zhí)行 EditableText._handleFocusChanged()饼酿,EditableText 會在 initState() 和 didUpdateWidget() 中去注冊監(jiān)聽榕酒。
  /// 解決自定義鍵盤與原生鍵盤提供方案。
  @override
  void didChangeMetrics() {
    // window.viewInsets.bottom 用來獲得鍵盤的高度
    if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
      _showCaretOnScreen();// 計算屏幕中位置
    }
    _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
  }


  /// EditableText 1706
  void _handleFocusChanged() {
    /// 打開和關閉輸入連接
    _openOrCloseInputConnectionIfNeeded();
    // 播放 Cursor 動畫
    _startOrStopCursorTimerIfNeeded();
    // SelectionOverlay 復制粘貼相關
    _updateOrDisposeSelectionOverlayIfNeeded();

    if (_hasFocus) {
      // Listen for changing viewInsets, which indicates keyboard showing up.
      // 監(jiān)聽viewInsets的改變故俐,當鍵盤出現的時候
      WidgetsBinding.instance.addObserver(this);
      _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
      _showCaretOnScreen();
      if (!_value.selection.isValid) {
        // 如果我們收到焦點時選擇無效想鹰,請將光標放在末尾。
        _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
      }
    } else {
      WidgetsBinding.instance.removeObserver(this);
      // 如果失去焦點药版,則清除選擇和合成狀態(tài)辑舷。
      _value = TextEditingValue(text: _value.text);
    }
  }
    1. _openOrCloseInputConnectionIfNeeded() 中會建立與 Native 鍵盤的連接。同時通過 TextInput.attach 關聯鍵盤事件槽片。
  /// EditableText 1391的構造方法
  void _openInputConnection() {
    if (!_hasInputConnection) { // 注冊與原生的通信
      final TextEditingValue localValue = _value;
      _lastKnownRemoteTextEditingValue = localValue;
      _textInputConnection = TextInput.attach(// 
        this,
        TextInputConfiguration(
          inputType: widget.keyboardType,
          ...
        ),
      );
      _textInputConnection.show(); // 展示原生鍵盤

      _updateSizeAndTransform();
      final TextStyle style = widget.style;
      _textInputConnection
        ..setStyle(
          fontFamily: style.fontFamily,
          ...
        )
        ..setEditingState(localValue);
    } else {
      _textInputConnection.show();
    }
  }

(1)_hasInputConnection方法會初始化 TextInput() 對象何缓,調用 binaryMessenger.setMethodCallHandler(),創(chuàng)建與原生的通信筐乳。_handleTextInputInvocation() 注冊了交互的事件歌殃,比如:更新光標的位置乔妈、給默認值蝙云、關閉連接.

  /// TextInput 828 的構造方法
  TextInput._() {
    _channel = OptionalMethodChannel(
      'flutter/textinput',
      JSONMethodCodec(),
  );
    _channel.setMethodCallHandler(_handleTextInputInvocation);
  }

  /// 監(jiān)聽鍵盤輸入中事件
  Future<dynamic> _handleTextInputInvocation(MethodCall methodCall) async {
    // 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':
        // TextInputAction.send波材,go 事件等。通過 onSubmitted() 回調
        _currentConnection._client.performAction(_toTextInputAction(args[1]));
        break;
      case 'TextInputClient.updateFloatingCursor':
        // 更新浮動光標的位置和狀態(tài)身隐。
        _currentConnection._client.updateFloatingCursor(_toTextPoint(_toTextCursorAction(args[1]), args[2]));
        break;
      case 'TextInputClient.onConnectionClosed':
        _currentConnection._client.connectionClosed();
        break;
      default:
        throw MissingPluginException();
    }
  }

(2) TextInput.attach() 調用'TextInput.setClient'方法與 native 建立連接廷区,configuration信息傳入傳給native,例如鍵盤的類型贾铝。

  void _attach(TextInputConnection connection, TextInputConfiguration configuration) {
  final ByteData result = await binaryMessenger.send(
      'TextInput.setClient',
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    _currentConnection = connection;
    _currentConfiguration = configuration;
  }

(3) _textInputConnection.show() 調用 'TextInput.show 方法顯示鍵盤隙轻。
(4) _updateSizeAndTransform() 方法,調用 TextInput.setEditableSizeAndTransform 將文本大小參數傳遞過去垢揩,設置輸入文字的大小

    1. _startOrStopCursorTimerIfNeeded() 使用 Timer 控制光標玖绿。
  • 5 _updateOrDisposeSelectionOverlayIfNeeded() 選中的文本 '復制/粘貼' 相關配置。
  void _handleFocusChanged() {
    _openOrCloseInputConnectionIfNeeded(); /// 建立關閉連接
    _startOrStopCursorTimerIfNeeded(); /// 光標
    _updateOrDisposeSelectionOverlayIfNeeded(); /// 選擇的文本
    if (_hasFocus) {
      // Listen for changing viewInsets, which indicates keyboard showing up.
      WidgetsBinding.instance.addObserver(this);
      _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; /// 獲取系統鍵盤的高度更新視圖
      _showCaretOnScreen(); 
      if (!_value.selection.isValid) { /// TextSelectionOverlay 復制/粘貼 布局
        // Place cursor at the end if the selection is invalid when we receive focus.
        _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
      }
    } else {
      WidgetsBinding.instance.removeObserver(this);
      // Clear the selection and composition state if this widget lost focus.
      _value = TextEditingValue(text: _value.text);
    }
    updateKeepAlive();
  }

六叁巨、InsightBank 中如何調用自定義鍵盤

鍵盤相關的事件

  • TextInput.show: 展示自定義鍵盤
  • TextInput.hide: 隱藏自定義鍵盤
  • TextInput.setClient: 初始化鍵盤斑匪,設置鍵盤類型等,并監(jiān)聽輸入事件
  • TextInput.clearClient:關閉鍵盤锋勺,內存回收
  • TextInput.setEditingState:設置編輯狀態(tài)蚀瘸。光標的位置,當前文本
  • TextInput.setEditableSizeAndTransform: 設置寬高相關信息
  • TextInput.setStyle: 設置文本字體
  static void _interceptInput() {
    // custom keyboard will cover the driver's MockMessageHandler
    // so we disable custom keyboard when run driver test
    if (_isIntercepted || moduleConfig.isDriverTestMode) return;
    _isIntercepted = true;
    ServicesBinding.instance.defaultBinaryMessenger.setMockMessageHandler(
      _channelTextInput,
      (ByteData data) async {
        final methodCall = _codec.decodeMethodCall(data);
        switch (methodCall.method) {
          case _methodShow:
            return _handleShow(methodCall, data);
          case _methodHide:
            return _handleHide(methodCall, data);
          case _methodSetClient:
            return _handleSetClient(methodCall, data);
          case _methodClearClient:
            return _handleClearClient(methodCall, data);
          case _methodSetEditingState:
            return _handleSetEditingState(methodCall, data);
          default:
            return _dispatchOriginResponse(data);
        }
      },
    );
  }

七庶橱、如何解決自定義鍵盤與原生鍵盤互相切換問題

(1)之前的解決方案

image.png

final insets = window.viewInsets.bottom / window.devicePixelRatio;就是鍵盤的高度

(2)_showCaretOnScreen()
_showCaretOnScreen() 起到的作用如下

  1. 計算文字贮勃,橫向滾動
  2. 計算 bottomSpacing ,將輸入框顯示在屏幕上合適的位置苏章。

問題:

  1. 為什么使用的是:addPostFrameCallback 是否可以換成 setState()衙猪。
    換成 setState 后輸入文字出現閃爍的情況

  2. debug 模式下,鍵盤互相切換沒有問題布近,release版本卻不行(偶現問題)垫释。
    我猜想,一下前提條件

  • _showCaretOnScreenScheduled 控制 _showCaretOnScree() 調用
  • setState() 不會調用 _showCaretOnScreen()撑瞧,意味著收起鍵盤不會調用
  • window 更新 vidwinsets 是異步的棵譬。

由于 release 版本更快,window 更新時预伺,_showCaretOnScreenScheduled 控制著 _showCaretOnScree() 無法更新订咸,所以出現了輸入框被遮擋的情況。

同理 debug 版本相對慢酬诀,意味著 _showCaretOnScree() 執(zhí)行完成后脏嚷,因為 window 更新又執(zhí)行了一遍

  @override
  void didChangeMetrics() {
    // 展示鍵盤的事件
    if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) {
      _showCaretOnScreen();
    }
    _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
  }

  // 調用時機,didChangeMetrics(), initState(), 輸入字符時候瞒御。
  void _showCaretOnScreen() {
    if (_showCaretOnScreenScheduled) {
      return;
    }
    _showCaretOnScreenScheduled = true;
    // 這里沒有使用 setState 而是使用了 addPostFrameCallback
    SchedulerBinding.instance.addPostFrameCallback((Duration _) {
      _showCaretOnScreenScheduled = false;
      if (_currentCaretRect == null || !_scrollController.hasClients) {
        return;
      }
      final double scrollOffsetForCaret = _getScrollOffsetForCaret(_currentCaretRect);
      /// 1. 文字橫向滾動父叙,如果輸入文字超過一屏幕
      _scrollController.animateTo(
        scrollOffsetForCaret,
        duration: _caretAnimationDuration,
        curve: _caretAnimationCurve,
      );
      final Rect newCaretRect = _getCaretRectAtScrollOffset(_currentCaretRect, scrollOffsetForCaret);
      // Enlarge newCaretRect by scrollPadding to ensure that caret is not
      // positioned directly at the edge after scrolling.
      double bottomSpacing = widget.scrollPadding.bottom;
      if (_selectionOverlay?.selectionControls != null) {
        final double handleHeight = _selectionOverlay.selectionControls
          .getHandleSize(renderEditable.preferredLineHeight).height;
        final double interactiveHandleHeight = math.max(
          handleHeight,
          kMinInteractiveDimension,
        );
        final Offset anchor = _selectionOverlay.selectionControls
          .getHandleAnchor(
            TextSelectionHandleType.collapsed,
            renderEditable.preferredLineHeight,
          );
        final double handleCenter = handleHeight / 2 - anchor.dy;
        bottomSpacing = math.max(
          handleCenter + interactiveHandleHeight / 2,
          bottomSpacing,
        );
      }
      final Rect inflatedRect = Rect.fromLTRB(
          newCaretRect.left - widget.scrollPadding.left,
          newCaretRect.top - widget.scrollPadding.top,
          newCaretRect.right + widget.scrollPadding.right,
          newCaretRect.bottom + bottomSpacing,
      );

       /// 2. 找到輸入框光標的位置,顯示在屏幕上
      _editableKey.currentContext.findRenderObject().showOnScreen(
        rect: inflatedRect,
        duration: _caretAnimationDuration,
        curve: _caretAnimationCurve,
      );
    });
  }

總結:

  1. nativeBottomInsets 可以通過 window 直接獲取。
  2. addPostFrameCallback() 具有延遲計算的作用趾唱,第一幀使用老的 insets涌乳,這樣焦點還在原來的位置不變。
    即使甜癞,出現了以上情況夕晓,
    https://phabricator.d.xiaomi.net/D224058
 static double get nativeBottomInsets => window.viewInsets.bottom / window.devicePixelRatio;

 /// 在 ScrollView 中,原生鍵盤切換自定義鍵盤悠咱,會發(fā)生 ScrollView 滾動位置不準確問題蒸辆。
 ///
 /// 原因:ScrollView 的計算依賴與第一幀 viewInsets 去計算。
 /// updateBottomInsets() 會在 [window] 更新 viewInsets 之前觸發(fā)析既。所以使用 [onPostFrame] 延遲處理吁朦。
 /// 另外,setState() 可以更新 viewInsets 重繪渡贾,但在 release 版本下會變得不可控逗宜。
 void updateBottomInsets(double insets) {
   // 收起自定義鍵盤時,不能立即更新 insets 空骚,會造成 ScrollView 滾動位置不準確纺讲。
   if (insets < customBottomInsets) {
     return UiUtils.onPostFrame(() => setState(() => customBottomInsets = insets));
   }
   // 展示自定義鍵盤需要立刻更新 insets, 會造成 ScrollView 滾動位置不準確
   setState(() => customBottomInsets = insets);
 }`
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市囤屹,隨后出現的幾起案子熬甚,更是在濱河造成了極大的恐慌,老刑警劉巖肋坚,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乡括,死亡現場離奇詭異,居然都是意外死亡智厌,警方通過查閱死者的電腦和手機诲泌,發(fā)現死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铣鹏,“玉大人敷扫,你說我怎么就攤上這事〕闲叮” “怎么了葵第?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長合溺。 經常有香客問我卒密,道長,這世上最難降的妖魔是什么棠赛? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任哮奇,我火速辦了婚禮膛腐,結果婚禮上,老公的妹妹穿的比我還像新娘屏镊。我一直安慰自己依疼,他們只是感情好痰腮,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布而芥。 她就那樣靜靜地躺著,像睡著了一般膀值。 火紅的嫁衣襯著肌膚如雪棍丐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天沧踏,我揣著相機與錄音歌逢,去河邊找鬼。 笑死翘狱,一個胖子當著我的面吹牛秘案,可吹牛的內容都是我干的。 我是一名探鬼主播潦匈,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼阱高,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了茬缩?” 一聲冷哼從身側響起赤惊,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凰锡,沒想到半個月后未舟,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡掂为,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年裕膀,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勇哗。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡魂角,死狀恐怖,靈堂內的尸體忽然破棺而出智绸,到底是詐尸還是另有隱情野揪,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布瞧栗,位于F島的核電站斯稳,受9級特大地震影響,放射性物質發(fā)生泄漏迹恐。R本人自食惡果不足惜挣惰,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧憎茂,春花似錦珍语、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拳氢,卻和暖如春募逞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背馋评。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工放接, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人留特。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓纠脾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蜕青。 傳聞我的和親對象是個殘疾皇子苟蹈,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355