說說Flutter中的無名英雄 —— Focus

Focus Deer

Focus系列的Widget及功能類在Flutter中可以說是無名英雄的存在艘款,默默的付出但卻不太為人所知离熏。在日常開發(fā)使用中也不太會用到它杰扫,這是為什么呢凤壁?帶著這個問題我們開始今天的內(nèi)容吩屹。

1.Focus相關(guān)介紹

這里大致介紹一些Focus相關(guān)Widget及功能類,便于后面理解Focus Tree部分拧抖。本篇源碼基于1.20.0-2.0.pre煤搜。

1.1 FocusNode

FocusNode是用于Widget獲取鍵盤焦點和處理鍵盤事件的對象。它是繼承自ChangeNotifier唧席,所以我們可以在任意位置獲取對應(yīng)的FocusNode信息擦盾。

下面說幾個FocusNode常用方法:

  • requestFocus用作請求焦點,注意這個請求焦點的執(zhí)行放在了scheduleMicrotask中淌哟,因此結(jié)果可能會延遲最多一幀迹卢。

  • unfocus用作取消焦點,默認行為為UnfocusDisposition.scope

void unfocus({UnfocusDisposition disposition = UnfocusDisposition.scope,}) {
  ....
}

UnfocusDisposition枚舉類是焦點取消后的行為徒仓,分為scopepreviouslyFocusedChild兩種腐碱。

  1. scope表示向上尋找最近的FocusScopeNode

  2. previouslyFocusedChild是尋找上一個焦點位置掉弛,如果沒有則給當(dāng)前FocusScopeNode症见。

具體實現(xiàn)可見unfocus源碼,這里就不多說了殃饿。

  • dispose這個沒啥說的谋作,注意使用FocusNode完后及時銷毀。

1.2 FocusScopeNode

FocusScopeNodeFocusNode的子類壁晒。它將FocusNode組織到一個作用域中瓷们,形成一組可以遍歷的節(jié)點。它會提供最后一個獲取焦點的FocusNode(focusedChild)秒咐,如果其中一個節(jié)點的焦點被移除谬晕,那么此FocusScopeNode將再次獲得焦點,同時_focusedChildren清空携取。

  /// Returns the child of this node that should receive focus if this scope
  /// node receives focus.
  ///
  /// If [hasFocus] is true, then this points to the child of this node that is
  /// currently focused.
  ///
  /// Returns null if there is no currently focused child.
  FocusNode get focusedChild {
    return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
  }

  // A stack of the children that have been set as the focusedChild, most recent
  // last (which is the top of the stack).
  final List<FocusNode> _focusedChildren = <FocusNode>[];

注意這里的_focusedChildren并不是FocusScopeNode下出現(xiàn)的所有FocusNode攒钳,而是獲取過焦點的FocusNode才會在里面。源碼實現(xiàn)如下:

  void _setAsFocusedChildForScope() {
    FocusNode scopeFocus = this;
    for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
      // 從聚焦的歷史中移除
      ancestor._focusedChildren.remove(scopeFocus);
      // 再將它添加至最后雷滋,這樣上面的focusedChild可以獲取到最后獲取過焦點的節(jié)點
      ancestor._focusedChildren.add(scopeFocus);
      scopeFocus = ancestor;
    }
  }

FocusScopeNode比較重要的方法是setFirstFocus不撑,用來設(shè)置子作用域節(jié)點。

  void setFirstFocus(FocusScopeNode scope) {
    if (scope._parent == null) {
      // scope沒有父節(jié)點晤斩,將scope添加至當(dāng)前節(jié)點下
      _reparent(scope);
    }
    if (hasFocus) {
      // 當(dāng)前作用域存在焦點焕檬,_doRequestFocus將焦點移到scope上,同時記錄節(jié)點澳泵。
      scope._doRequestFocus(findFirstFocus: true);
    } else {
      // 當(dāng)前作用域不存在焦點实愚,記錄節(jié)點。
      scope._setAsFocusedChildForScope();
    }
  }

1.3 Focus

Focus是一個Widget,可以用來分配焦點給它本身及其子Widget腊敲。內(nèi)部管理著一個FocusNode击喂,監(jiān)聽焦點的變化,來保持焦點層次結(jié)構(gòu)與Widget層次結(jié)構(gòu)同步碰辅。

我們常用的InkWell就使用了它懂昂,而Button、 Chip等大量的Widget又使用了InkWell没宾,所以Focus可以說是無處不在凌彬。

我們來看一下InkResponse源碼:

InkResponse源碼

這里發(fā)現(xiàn)了Focus,我們看看它的onFocusChange實現(xiàn):

  void _handleFocusUpdate(bool hasFocus) {
    _hasFocus = hasFocus;
    _updateFocusHighlights();
    if (widget.onFocusChange != null) {
      widget.onFocusChange(hasFocus);
    }
  }

有焦點變化時修改_hasFocus值調(diào)用_updateFocusHighlights方法榕吼。

  void _updateFocusHighlights() {
    bool showFocus;
    switch (FocusManager.instance.highlightMode) {
      case FocusHighlightMode.touch:
        showFocus = false;
        break;
      case FocusHighlightMode.traditional:
        showFocus = _shouldShowFocus;
        break;
    }
    updateHighlight(_HighlightType.focus, value: showFocus);
  }

最終調(diào)用updateHighlight方法讓W(xué)Idget有一個獲取焦點時的高亮顯示饿序。

這里有個枚舉類FocusHighlightMode,它是表示使用何種交互模式獲取的焦點羹蚣。分為touchtraditional原探。

默認的區(qū)分實現(xiàn)如下:

  static FocusHighlightMode get _defaultModeForPlatform {
    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.iOS:
        if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
          return FocusHighlightMode.traditional;
        }
        return FocusHighlightMode.touch;
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        return FocusHighlightMode.traditional;
    }
    return null;
  }

移動端在沒有鼠標連接的情況下都是touch,桌面端都為傳統(tǒng)的方式(鍵盤和鼠標)顽素。

所以這也回答我一開始的問題咽弦,我們一般只考慮了移動設(shè)備,也就是touch的部分胁出,這部分其實我們不太需要給按鈕處理焦點效果型型,可能類似給Android TV盒子用的這類App才需要。而Flutter提供的Widget需要考慮各個平臺效果全蝶,所以才使用了這些闹蒜。類似在上面的InkResponse源碼中,還出現(xiàn)了MouseRegion這個Widget抑淫,它是跟蹤鼠標移動的绷落,比如在Web端鼠標移動到按鈕上,按鈕會有一個變化效果始苇。

1.4 FocusScope

FocusScopeFocus類似砌烁,不過它的內(nèi)部管理的是FocusScopeNode。它不改變主焦點催式,它只是改變了接收焦點的作用域節(jié)點函喉。這個在源碼中使用的不多,但卻都很重要的位置荣月。

比如NavigatorRoute管呵,首先Navigator有一個FocusScope,自動獲取焦點哺窄。在它承載的一個個路由上也會添加FocusScope撇寞,這樣當(dāng)頁面跳轉(zhuǎn)/Dialog彈框時可以將焦點的作用域移動到上面(通過setFirstFocus方法)顿天。

類似Drawer也是一樣堂氯。當(dāng)抽屜打開時蔑担,我們的焦點作用域就要移動到Drawer,所以也要使用FocusScope咽白。

如果我們要管理焦點啤握,在頁面中有一個Stack,上層覆蓋了下層Widget導(dǎo)致下面不可操作晶框。這時我們就可以使用FocusScope將焦點作用域移動至上面排抬。

2.Focus Tree

Flutter里面有按照分類不同存在各種各樣的“樹”,比如常說的三棵樹Widget Tree授段、Element Tree 和 RenderObject Tree蹲蒲,其他的比如我之前博客說過的Semantics Tree,和這里要介紹的Focus Tree侵贵。

Focus Tree是與Widget Tree獨立開的届搁、結(jié)構(gòu)相對簡單的樹,它是維護Widget Tree中可聚焦Widget之間的層次關(guān)系窍育。Focus Tree因為無法通過工具來可視化觀察卡睦,我們可以使用Focus Tree的管理類FocusManager中的debugDumpFocusTree方法打印出來。

所以這里我新建一個項目漱抓,寫一個小例子來看一下表锻。代碼很簡單,Column里一個TextFieldFlatButton 乞娄。

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          TextField(),
          FlatButton(
            child: Text('打印FocusTree'),
            onPressed: () {
              WidgetsBinding.instance.addPostFrameCallback((_) {
                debugDumpFocusTree();
              });
            },
          ),
        ],
      ),
    );
  }
}

點擊按鈕雷厂,打印結(jié)果如下:

 FocusManager#4148c
  │ UPDATE SCHEDULED
  │ primaryFocus: FocusScopeNode#af55c(_ModalScopeState<dynamic>
  │   Focus Scope [PRIMARY FOCUS])
  │ nextFocus: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH])
  │ primaryFocusCreator: FocusScope ← _ActionsMarker ← Actions ←
  │   PageStorage ← Offstage ← _ModalScopeStatus ←
  │   _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#bfb70]
  │   ← _EffectiveTickerMode ← TickerMode ←
  │   _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#3fa85]
  │   ← _Theatre ← Overlay-[LabeledGlobalKey<OverlayState>#2d724] ←
  │   _FocusMarker ← Semantics ← FocusScope ← AbsorbPointer ←
  │   _PointerListener ← Listener ← HeroControllerScope ←
  │   Navigator-[GlobalObjectKey<NavigatorState>
  │   _WidgetsAppState#9404f] ← ?
  │
  └─rootScope: FocusScopeNode#185ad(Root Focus Scope [IN FOCUS PATH])
    │ IN FOCUS PATH
    │ focusedChildren: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS
    │   PATH])
    │
    └─Child 1: FocusNode#5bacc(Shortcuts [IN FOCUS PATH])
      │ context: Focus
      │ NOT FOCUSABLE
      │ IN FOCUS PATH
      │
      └─Child 1: FocusNode#1cd76(FocusTraversalGroup [IN FOCUS PATH])
        │ context: Focus
        │ NOT FOCUSABLE
        │ IN FOCUS PATH
        │
        └─Child 1: FocusScopeNode#4f0d5(Navigator Scope [IN FOCUS PATH])
          │ context: FocusScope
          │ IN FOCUS PATH
          │
          └─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])
            │ context: FocusScope
            │ PRIMARY FOCUS
            │
            ├─Child 1: FocusNode#e72e2
            │   context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
            │
            └─Child 2: FocusNode#0b7c0
                context: Focus

我從下往上說一下代表的含義:

  1. Child 1: FocusNode#e72e2Child 2: FocusNode#0b7c0一看就是同級,代表的就是TextFieldFlatButton 蔗牡。

  2. 上一層FocusScopeNode#af55c是當(dāng)前的頁面弹沽,可以看到焦點目前在它上面(PRIMARY FOCUS)。它是在
    MaterialPageRoute -> PageRoute -> ModalRoute ->createOverlayEntries -> _buildModalScope方法溶其,調(diào)用_ModalScope創(chuàng)建的骚腥。

  3. 再上一層FocusScopeNode#4f0d5Navigator,代碼如下:

final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');

@override
  Widget build(BuildContext context) {
    return HeroControllerScope(
      child: Listener(
        onPointerDown: _handlePointerDown,
        onPointerUp: _handlePointerUpOrCancel,
        onPointerCancel: _handlePointerUpOrCancel,
        child: AbsorbPointer(
          absorbing: false,
          child: FocusScope(
            node: focusScopeNode, // <---
            autofocus: true,
            child: Overlay(
              key: _overlayKey,
              initialEntries: overlay == null ?  _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
            ),
          ),
        ),
      ),
    );
  }
  1. 再往上兩層是WidgetsAppShortcutsFocusTraversalGroup創(chuàng)建的瓶逃。
WidgetsApp源碼
  1. 最頂層就是rootScope它是在WidgetsBinding初始化時調(diào)用BuildOwner創(chuàng)建FocusManager而來的束铭。
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _buildOwner = BuildOwner();
    ...
  }
  ...
}
class BuildOwner {
  /// Creates an object that manages widgets.
  BuildOwner({ this.onBuildScheduled });

  /// The object in charge of the focus tree.
  FocusManager focusManager = FocusManager();
  
  ...
}
class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
  final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
  
  FocusManager() {
    rootScope._manager = this;
    ...
  }
  ...
}
  1. 最后是FocusManager類的相關(guān)信息。
  • primaryFocus:當(dāng)前的主焦點厢绝。
  • rootScope:當(dāng)前Focus Tree的根節(jié)點契沫。
  • highlightMode:當(dāng)前獲取焦點的交互模式,上面有提到昔汉。
  • highlightStrategy:交互模式的策略懈万,默認automatic根據(jù)接收到的最后一種輸入方式,自動切換。也可以指定使用某一種方式会通。
  • FocusManager也繼承自ChangeNotifier口予,所以我們可以通過addListener監(jiān)聽primaryFocus的變化。

3.Focus Tree變化

現(xiàn)在我先點擊一下輸入框涕侈,在點擊按鈕沪停,打印結(jié)果如下(只取最后幾層):

primaryFocus: FocusNode#e72e2([PRIMARY FOCUS])
...
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
  │ context: FocusScope
  │ IN FOCUS PATH
  │ focusedChildren: FocusNode#e72e2([PRIMARY FOCUS])
  │
  ├─Child 1: FocusNode#e72e2([PRIMARY FOCUS])
  │   context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
  │   PRIMARY FOCUS
  │
  └─Child 2: FocusNode#0b7c0
      context: Focus

可以看到當(dāng)前焦點primaryFocusFocusNode#e72e2也就是到了TextField上。注意這里的focusedChildren此時只有FocusNode#e72e2裳涛。

因為我點擊了TextField木张,此時軟鍵盤彈出。現(xiàn)在我需要關(guān)閉軟鍵盤端三,我這里有四種方法:

  1. 使用SystemChannels.textInput.invokeMethod('TextInput.hide')方法舷礼,這種方法關(guān)閉軟鍵盤后焦點不變,還在TextField上郊闯,所以有一個問題妻献。比如這時你push到一個新的頁面再pop返回,此時軟鍵盤會再次彈出虚婿。這里不推薦使用旋奢。

  2. 使用FocusScope.of(context).requestFocus(FocusNode())方法,并打印一下Focus Tree然痊。

primaryFocus: FocusNode#7da34([PRIMARY FOCUS])
└─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
  │ context: FocusScope
  │ IN FOCUS PATH
  │ focusedChildren: FocusNode#7da34([PRIMARY FOCUS]),
  │   FocusNode#e72e2
  │
  ├─Child 1: FocusNode#e72e2
  │   context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
  │
  ├─Child 2: FocusNode#0b7c0
  │   context: Focus
  └─Child 3: FocusNode#7da34([PRIMARY FOCUS])
      PRIMARY FOCUS

可以看到其實就在當(dāng)前節(jié)點下創(chuàng)建了一個FocusNode#7da34并把焦點轉(zhuǎn)移給它至朗。注意這里的focusedChildren此時有FocusNode#7da34FocusNode#e72e2

  1. 使用FocusScope.of(context).unfocus()方法重復(fù)上面的步驟剧浸,并打印一下Focus Tree锹引。
primaryFocus: FocusScopeNode#4f0d5(Navigator Scope [PRIMARY FOCUS])
└─Child 1: FocusScopeNode#4f0d5(Navigator Scope [PRIMARY FOCUS])
  │ context: FocusScope
  │ PRIMARY FOCUS
  │
  └─Child 1: FocusScopeNode#af55c(_ModalScopeState<dynamic> Focus Scope)
    │ context: FocusScope
    │ focusedChildren: FocusNode#e72e2, FocusNode#7da34
    │
    ├─Child 1: FocusNode#e72e2
    │   context: EditableText-[LabeledGlobalKey<EditableTextState>#c2f8a]
    │
    ├─Child 2: FocusNode#0b7c0
    │   context: Focus   
    └─Child 3: FocusNode#7da34

可以看到焦點直接到了Navigator上,為什么不是當(dāng)前頁面FocusScopeNode#af55c呢唆香?

因為這里FocusScope.of(context)方法所返回的FocusScopeNode就是當(dāng)前頁面FocusScopeNode#af55c嫌变,這時候你再取消了焦點,那么焦點此時就向上尋找躬它,到了Navigator上腾啥。

注意這里的focusedChildren此時有FocusNode#e72e2FocusNode#7da34。不過看到這里你有沒有發(fā)現(xiàn)一個問題冯吓。焦點已經(jīng)不在FocusScopeNode#af55c的作用域里面了倘待,但是focusedChildren里卻還存在數(shù)據(jù),如果我們這時使用如FocusScope.of(context).focusedChild方法组贺,那么得到的結(jié)果就是不正確的凸舵。

穩(wěn)妥的做法是使用下面的第四種方法。

  1. 最后一個方法就是給TextField添加屬性focusNode失尖,直接調(diào)用_focusNode.unfocus()
final FocusNode _focusNode = FocusNode();
TextField(
  focusNode: _focusNode,
),
_focusNode.unfocus();

這里我就不貼結(jié)果了啊奄,大體和一開始的一樣渐苏,此時focusedChildren為空不打印。這樣就可以將焦點成功歸還上級作用域(當(dāng)前頁面)菇夸,不過這樣如果頁面復(fù)雜琼富,可能會比較繁瑣,你需要每個添加FocusNode來管理峻仇。所以更推薦使用:

FocusManager.instance.primaryFocus?.unfocus();

它可以直接獲取到當(dāng)前的焦點公黑,便于我們直接取消焦點。所以對比這四個方法摄咆,肯定后者比較好了,也避免了因數(shù)據(jù)錯誤導(dǎo)致的其他隱患人断。

4.結(jié)語

通過觀察Focus Tree的變化吭从,我們大致可以理解Focus Tree的組成及變化規(guī)律,如果你有控制焦點的需求恶迈,本篇或許可以為你帶來幫助涩金。

關(guān)于Focus其實還有許多細節(jié),比如FocusAttachment如何管理FocusNode 暇仲、FocusNode的遍歷順序?qū)崿F(xiàn) FocusTraversalGroup等步做。由于篇幅有限,這里就不介紹了奈附,有興趣的可以看看源碼全度。

本篇是“說說”系列第四篇,前三篇鏈接奉上:

如果本文對你有所幫助或啟發(fā)的話斥滤,還請不吝點贊收藏支持一波将鸵。同時也多多支持我的Flutter開源項目flutter_deer

我們下個月見~~

5.參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末佑颇,一起剝皮案震驚了整個濱河市顶掉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌挑胸,老刑警劉巖痒筒,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茬贵,居然都是意外死亡簿透,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門闷沥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萎战,“玉大人,你說我怎么就攤上這事舆逃÷煳” “怎么了戳粒?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長虫啥。 經(jīng)常有香客問我蔚约,道長,這世上最難降的妖魔是什么涂籽? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任苹祟,我火速辦了婚禮,結(jié)果婚禮上评雌,老公的妹妹穿的比我還像新娘树枫。我一直安慰自己,他們只是感情好景东,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布砂轻。 她就那樣靜靜地躺著,像睡著了一般斤吐。 火紅的嫁衣襯著肌膚如雪搔涝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天和措,我揣著相機與錄音庄呈,去河邊找鬼。 笑死派阱,一個胖子當(dāng)著我的面吹牛诬留,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播颁褂,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼故响,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了颁独?” 一聲冷哼從身側(cè)響起彩届,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎誓酒,沒想到半個月后樟蠕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡靠柑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年寨辩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歼冰。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡靡狞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出隔嫡,到底是詐尸還是另有隱情甸怕,我是刑警寧澤甘穿,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站梢杭,受9級特大地震影響温兼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜武契,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一募判、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧咒唆,春花似錦届垫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恨溜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間找前,已是汗流浹背糟袁。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留躺盛,地道東北人项戴。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像槽惫,于是被迫代替她去往敵國和親周叮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345