Flutter筆記-事件分發(fā)

ps: 文中flutter源碼版本 1.0.0


1. 手勢分配流程

我們從頭開始分析类嗤,先看runApp(rootWidget):

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

ensureInitialized()進行了一系列綁定,包含了手勢

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    //不存在則會創(chuàng)建一個
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

這是一個典型的單例模式复哆,調(diào)用構(gòu)造函數(shù)论皆,然后調(diào)用父類的構(gòu)造函數(shù)

abstract class BindingBase {
  BindingBase() {
    developer.Timeline.startSync('Framework initialization');
    assert(!_debugInitialized);
    initInstances();
    assert(_debugInitialized);
    assert(!_debugServiceExtensionsRegistered);
    initServiceExtensions();
    assert(_debugServiceExtensionsRegistered);
    developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
    developer.Timeline.finishSync();
  }
  ...
}

構(gòu)造函數(shù)中會調(diào)用initInstances()方法莹痢,那么這個方法在哪實現(xiàn)的背零?
關(guān)鍵點在于with(dart語法,混合)情组,重復的屬性或方法取最后的mixin類,即initInstances()和instance等方法和屬性取WidgetsBinding中的

mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    //注金刁,這個super是找的上一級混合類
    super.initInstances();
    _instance = this;
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    ui.window.onLocaleChanged = handleLocaleChanged;
    ui.window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
    SystemChannels.system.setMessageHandler(_handleSystemMessage);
  }

  static WidgetsBinding get instance => _instance;
  static WidgetsBinding _instance;
  ...
}

通過super.initInstances()辜王,會逐漸往前調(diào)用,簡單理解就是所有混合類的initInstances()都將被調(diào)用
最終會調(diào)用我們所要找的手勢綁定類

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    //1.調(diào)用_handlePointerDataPacket方法
    ui.window.onPointerDataPacket = _handlePointerDataPacket;
  }

  @override
  void unlocked() {
    super.unlocked();
    _flushPointerEventQueue();
  }

  static GestureBinding get instance => _instance;
  static GestureBinding _instance;

  final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();

  void _handlePointerDataPacket(ui.PointerDataPacket packet) {
 _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, ui.window.devicePixelRatio));
    if (!locked)
     //2.刷新手勢事件隊列
      _flushPointerEventQueue();
  }
  ...
  void _flushPointerEventQueue() {
    assert(!locked);
    //3. 處理手勢事件
    while (_pendingPointerEvents.isNotEmpty)
      _handlePointerEvent(_pendingPointerEvents.removeFirst());
  }

  
  final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};

  void _handlePointerEvent(PointerEvent event) {
    assert(!locked);
    HitTestResult result;
    if (event is PointerDownEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      result = HitTestResult();
      //4. 手勢添加到測試result列表中
      hitTest(result, event.position);
      _hitTests[event.pointer] = result;
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $result');
        return true;
      }());
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      result = _hitTests.remove(event.pointer);
    } else if (event.down) {
      result = _hitTests[event.pointer];
    } else {
      return; 
    }
    if (result != null)
      //5.分發(fā)事件
      dispatchEvent(event, result);
  }

  
  @override 
  void hitTest(HitTestResult result, Offset position) {
    //WidgetsFlutterBinding調(diào)用時添加到result中
    result.add(HitTestEntry(this));
  }

  @override 
  void dispatchEvent(PointerEvent event, HitTestResult result) {
    assert(!locked);
    assert(result != null);
    //只有在result列表中才會進行事件處理
    for (HitTestEntry entry in result.path) {
      try {
        //6. 處理事件撕瞧,加入了列表陵叽,包裝了一層
        entry.target.handleEvent(event, entry);
      } catch (exception, stack) {
        //處理異常錯誤,忽略
       ...
      }
    }
  }
  //WidgetsFlutterBinding默認調(diào)用
  @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);
    }
  }
}

按著注釋順序逐個分析下來丛版,在第四步時巩掺,如果不注意,可能就會犯錯
回到最開始页畦,WidgetsFlutterBinding混合了許多方法胖替,其中的 RendererBinding混合了HitTestable,重寫了hitTest(HitTestResult result, Offset position)方法豫缨,所以這的hitTest是使用RendererBinding中的而非GestureBinding的

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _pipelineOwner = PipelineOwner(
      onNeedVisualUpdate: ensureVisualUpdate,
      onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
      onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
    );
    ui.window
      ..onMetricsChanged = handleMetricsChanged
      ..onTextScaleFactorChanged = handleTextScaleFactorChanged
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsAction = _handleSemanticsAction;
    initRenderView();
    _handleSemanticsEnabledChanged();
    assert(renderView != null);
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }
 
  void initRenderView() {
    assert(renderView == null);
    renderView = RenderView(configuration: createViewConfiguration());
    renderView.scheduleInitialFrame();
  }
  ...
  @override
  void hitTest(HitTestResult result, Offset position) {
    assert(renderView != null);
    //唯一的區(qū)別是使用了renderView中的hitTest
    //這么寫独令,猜猜也知道肯定要去RenderView尋找答案
    renderView.hitTest(result, position: position);
    //當上面遍歷完后,仍然會調(diào)用GestureBinding中的hitTest
    super.hitTest(result, position); 
  }
  ...
}

renderView在initInstances中創(chuàng)建好芭,實際使用中燃箭,初始化時是無手勢的,真正進行變化的在unlocked() 方法中(同步鎖舍败,用于處理手勢事件)
所以招狸,這里又回到了RenderView的hitTest方法中來

bool hitTest(HitTestResult result, { Offset position }) {
    //child的類型是RenderBox(即RenderObject)
    if (child != null)
      child.hitTest(result, position: position);
    result.add(HitTestEntry(this));
    return true;
  }

HitTestEntry是什么敬拓,其實就主要包含一個HitTestTarget,也就是handleEvent(PointerEvent event, HitTestEntry entry)方法的抽象類

class HitTestEntry {
  const HitTestEntry(this.target);
  final HitTestTarget target;
  @override
  String toString() => '$target';
}

回到之前第六步瓢颅,entry.target.handleEvent(event, entry)中的target也是一個RenderView恩尾,而child.hitTest(result, position: position)是這樣的

bool hitTest(HitTestResult result, { @required Offset position }) {
    //斷言判斷,省略
    ...
    if (_size.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
  }

這也是為什么要重寫hitTestChildren()或hitTestSelf(position)的原因挽懦,當他們都為false時翰意,result就不會添加這個控件,即事件分發(fā)不會分配到該控件上信柿。

2. 手勢控件分析

分析完流程冀偶,再看看手勢監(jiān)聽的控件

前面我們知道,常用手勢控件有Listener和GestureDetector渔嚷,后者是對前者的封裝进鸠,這里對基礎(chǔ)手勢簡單分析下

我們按照以下的結(jié)構(gòu)進行分析,手勢監(jiān)聽控件里添加一個文本

Listener(
  child: Text("這是一個測試"),
)

a. Listener中的流程

逐一分析形病,先分析Listener:

class Listener extends SingleChildRenderObjectWidget {
  const Listener({
    Key key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerUp,
    this.onPointerCancel,
    this.behavior = HitTestBehavior.deferToChild,
    Widget child
  }) : assert(behavior != null),
       super(key: key, child: child);

  final PointerDownEventListener onPointerDown;
  final PointerMoveEventListener onPointerMove;
  final PointerUpEventListener onPointerUp;
  final PointerCancelEventListener onPointerCancel;
  final HitTestBehavior behavior;

  @override
  RenderPointerListener createRenderObject(BuildContext context) {
    return RenderPointerListener(
      onPointerDown: onPointerDown,
      onPointerMove: onPointerMove,
      onPointerUp: onPointerUp,
      onPointerCancel: onPointerCancel,
      behavior: behavior
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderPointerListener renderObject) {
    renderObject
      ..onPointerDown = onPointerDown
      ..onPointerMove = onPointerMove
      ..onPointerUp = onPointerUp
      ..onPointerCancel = onPointerCancel
      ..behavior = behavior;
  }
 ...
}

直接查看RenderPointerListener源碼客年,這里傳遞了幾個回調(diào)方法

class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
  RenderPointerListener({
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerUp,
    this.onPointerCancel,
    HitTestBehavior behavior = HitTestBehavior.deferToChild,
    RenderBox child
  }) : super(behavior: behavior, child: child);

  PointerDownEventListener onPointerDown;
  PointerMoveEventListener onPointerMove;
  PointerUpEventListener onPointerUp;
  PointerCancelEventListener onPointerCancel;

  @override
  void performResize() {
    size = constraints.biggest;
  }

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (onPointerDown != null && event is PointerDownEvent)
      return onPointerDown(event);
    if (onPointerMove != null && event is PointerMoveEvent)
      return onPointerMove(event);
    if (onPointerUp != null && event is PointerUpEvent)
      return onPointerUp(event);
    if (onPointerCancel != null && event is PointerCancelEvent)
      return onPointerCancel(event);
  }
  ...
}

這里有handleEvent方法,符合了之前的猜測漠吻,然后使用回調(diào)方法處理事件
繼續(xù)往下量瓜,看其子類,RenderProxyBoxWithHitTestBehavior中有hitTest方法

@override
  bool hitTest(HitTestResult result, { Offset position }) {
    bool hitTarget = false;
    if (size.contains(position)) {
      hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
      //要往result中添加數(shù)據(jù)需要滿足3個條件中任意一個即可
     //1. hitTestChildren為true途乃,重寫了绍傲,看下面說明(默認是false)
     //2. hitTestSelf為true,并未重寫(默認是false)
     //3. behavior為translucent(默認類型是deferToChild)
      if (hitTarget || behavior == HitTestBehavior.translucent)
        result.add(BoxHitTestEntry(this, position));
    }
    return hitTarget;
  }

接著再在其子類RenderProxyBoxMixin中找到了hitTestChildren方法:

@override
  bool hitTestChildren(HitTestResult result, { Offset position }) {
    return child?.hitTest(result, position: position) ?? false;
  }

b. child值獲取過程

child?.hitTest(result, position: position)耍共,這的child是什么烫饼?

child位于RenderObjectWithChildMixin(RenderPointerListener繼承的RenderProxyBox的混合類)中,是一個RenderObject试读,直接猜測的話應該就是我們傳的Text控件

那么并未通過構(gòu)造函數(shù)傳值杠纵,值如何獲取到的呢?

之前我們知道鹏往,控件都需要經(jīng)過build過程淡诗,通過rebuild()接著執(zhí)行performRebuild()

  @override
  void performRebuild() {
    //斷言判斷和錯誤處理省略
    ...
    Widget built;
    try {
      //實際上這就是StatelessWidget.build或State.build
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      ...
    } finally {
      ...
    }
    try {
      //這個built即是后面的newWidget
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      ...
    }
  ...
  }

更新子孩子

  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ...
    return inflateWidget(newWidget, newSlot);
  }

@protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
    final Element newChild = newWidget.createElement();
    //登記
    newChild.mount(this, newSlot);
    assert(newChild._debugLifecycleState == _ElementLifecycle.active);
    return newChild;
  }

Element中的createElement()是一個抽象方法,我們尋找他的實現(xiàn)類SingleChildRenderObjectElement(因為Listener是一個SingleChildRenderObjectWidget)

@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    //向下子控件遍歷
    _child = updateChild(_child, widget.child, null);
  }

這里有個循環(huán)伊履,不斷遍歷下去韩容,直到無子類控件,我們看看父類的mount做了什么?

//RenderObjectElement中mount
 @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    //關(guān)鍵唐瀑,widget如果是Listener群凶,_renderObject則是返回的RenderPointerListener,基類方法哄辣,通用的
    _renderObject = widget.createRenderObject(this);
    assert(() { _debugUpdateRenderObjectOwner(); return true; }());
    assert(_slot == newSlot);
   //關(guān)聯(lián)object對象
    attachRenderObject(newSlot);
    _dirty = false;
  }
//RenderObjectElement中attachRenderObject
@override
  void attachRenderObject(dynamic newSlot) {
    assert(_ancestorRenderObjectElement == null);
    _slot = newSlot;
    //找到父控件的RenderObjectElement请梢,因為都是單孩子控件赠尾,所以也是SingleChildRenderObjectElement
    //父類的添加在 inflateWidget,這里并不詳述
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    //單從單詞意思上就能猜到是這個了   
 _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
    if (parentDataElement != null)
      _updateParentData(parentDataElement.widget);
  }

RenderObjectElement中的insertChildRenderObject是一個抽象類毅弧,我們再次回到SingleChildRenderObjectElement

@override
  void insertChildRenderObject(RenderObject child, dynamic slot) {
    //指定是RenderObjectWithChildMixin類型气嫁,和前面對應上了
    final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject;
    assert(slot == null);
    assert(renderObject.debugValidateChild(child));
    //終于找到了,給child賦值了
    renderObject.child = child;
    assert(renderObject == this.renderObject);
  }

c. Text中手勢分析

前面推測出child是一個RenderObject够坐,通過widget.createRenderObject(this)返回的寸宵,但是Text是一個StatelessWidget,并沒有createRenderObject方法

大膽的假設一下元咙,內(nèi)部肯定間接的實現(xiàn)了一個RenderObject類

來看源碼:

class Text extends StatelessWidget {
  ...

  @override
  Widget build(BuildContext context) {
    final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
    TextStyle effectiveTextStyle = style;
    if (style == null || style.inherit)
      effectiveTextStyle = defaultTextStyle.style.merge(style);
    if (MediaQuery.boldTextOverride(context))
      effectiveTextStyle = effectiveTextStyle.merge(const TextStyle(fontWeight: FontWeight.bold));
    //內(nèi)部使用的是 RichText
    Widget result = RichText(
      textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
      textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null.
      locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
      softWrap: softWrap ?? defaultTextStyle.softWrap,
      overflow: overflow ?? defaultTextStyle.overflow,
      textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
      maxLines: maxLines ?? defaultTextStyle.maxLines,
      text: TextSpan(
        style: effectiveTextStyle,
        text: data,
        children: textSpan != null ? <TextSpan>[textSpan] : null,
      ),
    );
    if (semanticsLabel != null) {
      result = Semantics(
        textDirection: textDirection,
        label: semanticsLabel,
        child: ExcludeSemantics(
          child: result,
        )
      );
    }
    return result;
  }
  ...
}

查看RichText:

class RichText extends LeafRenderObjectWidget {
  //找到了該方法
  @override
  RenderParagraph createRenderObject(BuildContext context) {
    assert(textDirection != null || debugCheckHasDirectionality(context));
    return RenderParagraph(text,
      textAlign: textAlign,
      textDirection: textDirection ?? Directionality.of(context),
      softWrap: softWrap,
      overflow: overflow,
      textScaleFactor: textScaleFactor,
      maxLines: maxLines,
      locale: locale ?? Localizations.localeOf(context, nullOk: true),
    );
}

符合之前的假設梯影,里面真創(chuàng)建了RenderObject對象

class RenderParagraph extends RenderBox {
  ...
  //自身可以點擊
  @override
  bool hitTestSelf(Offset position) => true;
  //重寫了事件處理方式
  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is! PointerDownEvent)
      return;
    _layoutTextWithConstraints(constraints);
    final Offset offset = entry.localPosition;
    final TextPosition position = _textPainter.getPositionForOffset(offset);
    final TextSpan span = _textPainter.text.getSpanForPosition(position);
    span?.recognizer?.addPointer(event);
  }
  ...
}

RenderParagraph使用的是RenderBox中的hitTest方法

bool hitTest(HitTestResult result, { @required Offset position }) {
    //斷言判斷,省略
    ...
    if (_size.contains(position)) {
      if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true;
      }
    }
    return false;
}

hitTestSelf通過庶香,會將RenderParagraph加入列表中甲棍,同時返回true,然后父類也會添加到列表中赶掖,這樣都會接收到分發(fā)的事件

d. 理一理

理一下順序:


分配流程

3. 總結(jié)

hitTestChildren:點擊事件傳給子控件
hitTestSelf:自己接收到事件
handleEvent:處理事件

下一篇:深入分析GestureDetector

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末感猛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子奢赂,更是在濱河造成了極大的恐慌唱遭,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呈驶,死亡現(xiàn)場離奇詭異,居然都是意外死亡疫鹊,警方通過查閱死者的電腦和手機袖瞻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拆吆,“玉大人聋迎,你說我怎么就攤上這事≡嬉” “怎么了霉晕?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長捞奕。 經(jīng)常有香客問我牺堰,道長,這世上最難降的妖魔是什么颅围? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任伟葫,我火速辦了婚禮,結(jié)果婚禮上院促,老公的妹妹穿的比我還像新娘筏养。我一直安慰自己斧抱,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布渐溶。 她就那樣靜靜地躺著辉浦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茎辐。 梳的紋絲不亂的頭發(fā)上宪郊,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音荔茬,去河邊找鬼废膘。 笑死,一個胖子當著我的面吹牛慕蔚,可吹牛的內(nèi)容都是我干的丐黄。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼孔飒,長吁一口氣:“原來是場噩夢啊……” “哼灌闺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坏瞄,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤桂对,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鸠匀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蕉斜,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年缀棍,在試婚紗的時候發(fā)現(xiàn)自己被綠了宅此。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡爬范,死狀恐怖父腕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情青瀑,我是刑警寧澤璧亮,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站斥难,受9級特大地震影響枝嘶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蘸炸,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一躬络、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧搭儒,春花似錦穷当、人聲如沸提茁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茴扁。三九已至,卻和暖如春汪疮,著一層夾襖步出監(jiān)牢的瞬間峭火,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工智嚷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留卖丸,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓盏道,卻偏偏與公主長得像稍浆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子猜嘱,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348