Flutter-輸入框潛在bug

問(wèn)題

問(wèn)題描述:當(dāng)Flutter的輸入框中支持上了表情符號(hào)(emoji)诲泌,無(wú)論你用maxLength還是inputFormatters屬性,都會(huì)出現(xiàn)長(zhǎng)度超過(guò)你給定的值或表達(dá)式坦胶,而且光標(biāo)還會(huì)在達(dá)到最后字符的時(shí)候往前移動(dòng)一個(gè)字符。以下都是圍繞當(dāng)輸入框有表情符號(hào)開展的晴楔。

問(wèn)題原因:原本我是以為光標(biāo)的rect數(shù)據(jù)顿苇,所以在text_painter.dart類看了Rect _getRectFromDownstream(int offset, Rect caretPrototype)方法,用來(lái)計(jì)算光標(biāo)的矩陣税弃。(文字屬性是downStream的方法走這個(gè)方法纪岁,有對(duì)應(yīng)的一個(gè)是upstream的方法的)然后在追查到最后的editable.dart的_paintCaretPaint方法繪制里,發(fā)現(xiàn)出錯(cuò)在rect矩陣數(shù)據(jù)里则果,繪制沒有任何問(wèn)題蜂科。然后回到計(jì)算光標(biāo)矩陣的地方顽决,在觀察到往前移動(dòng)的光標(biāo)異常問(wèn)題是因?yàn)閛ffset引起的。

根本原因:無(wú)論是maxLength還是inputFormatters最后都是以inputFormatters作為EditableText的構(gòu)造函數(shù)导匣,在EditableText沒有處理好限字符的問(wèn)題才菠。這里主要就是看LengthLimitingTextInputFormatter類了,因?yàn)镋ditableText都是用各種inputFormatters處理文案的問(wèn)題贡定。


_formatAndSetValue方法

字符比限制的數(shù)字多一個(gè)原因:由于沒有處理好最大字符數(shù)的問(wèn)題赋访,當(dāng)flutter/engine的原生輸入框數(shù)據(jù)返回時(shí)候就是用戶真實(shí)輸入的數(shù)據(jù),所以到了EditableText更新text也是不做長(zhǎng)度判斷缓待,所以才會(huì)導(dǎo)致比限制的數(shù)字多一個(gè)的問(wèn)題蚓耽。

光標(biāo)前移原因:這里是因?yàn)閒lutter層在計(jì)算最大字符數(shù)時(shí)候,觸發(fā)刷新旋炒,native又返回一次輸入框的數(shù)據(jù)步悠,這里可以在text.input.dart類中_handleTextInputInvocation(MethodCall methodCall)中返回?cái)?shù)據(jù)解析生成的TextEditingValue中光標(biāo)位置就是用戶輸入的maxLength。

如何解決

通過(guò)上面的問(wèn)題大概知道就是當(dāng)文字中混有表情符號(hào)(emoji)時(shí)候瘫镇,flutter計(jì)算text時(shí)候就會(huì)出現(xiàn)漏洞鼎兽。這里先補(bǔ)給下最重要的知識(shí)點(diǎn),全面認(rèn)識(shí)TextEditingValue類铣除。

TextEditingValue

TextEditingValue類是所有輸入框封裝文案與光標(biāo)的基類谚咬。TextEditingValue里面的屬性分別都有自己用處。
text:輸入框要展示的文案尚粘。
作用:展示文案的內(nèi)容择卦。
selection:TextSelection類(affinity:文字的TextAffinity屬性;baseOffset:字符開始的偏移量(有光標(biāo)情況:光標(biāo)的起始位置)郎嫁;extentOffset:字符插入的位置(有光標(biāo)情況:光標(biāo)的結(jié)束位置)
作用:用戶選擇插入字符的位置秉继。
composing:TextRange類(int start,int end)泽铛。start是文本編輯的起始位置秕噪,end是文本編輯的結(jié)束位置。
作用:用來(lái)判斷該文本是否還在編輯狀態(tài)厚宰,如果還在編輯狀態(tài)腌巾,native層在_handleTextInputInvocation返回時(shí)候,將不帶上TextRange的start和end標(biāo)識(shí)的字符串铲觉。(這個(gè)屬性專門用來(lái)處理iOS自帶的輸入框問(wèn)題澈蝙,會(huì)在章節(jié)《遇到的問(wèn)題》中描述,坑爹)


解決辦法
  1. 從源碼中能看到EditableText每次接收到新的字符串內(nèi)容撵幽,都會(huì)在_formatAndSetValue方法調(diào)用onChanged(String text)方法灯荧。其中可以看到這里inputFormatters沒有處理好帶表情的TextEditingValue,所以我們要做的就是在onChange方法中處理好TextEditingValue盐杂。


    _formatAndSetValue方法
  • 方法一(能達(dá)到超長(zhǎng)的回調(diào)逗载,也是比較挫的方法)
  1. 最好自己封裝一個(gè)XXXTextField類哆窿,統(tǒng)一處理這類問(wèn)題±髡澹或者自己寫一個(gè)inputFormatters挚躯,把官網(wǎng)的彌補(bǔ)下。現(xiàn)在我先介紹我自己比較挫的解決辦法先擦秽。
在封裝的類中增加兩個(gè)屬性
//最大長(zhǎng)度
final int selfMaxLength;
//達(dá)到最大長(zhǎng)度后的回調(diào)
final Function maxLengthCallBack

class _XXXTextFieldState extends State<XXXTextField> {
      ……
      //是否已經(jīng)觸發(fā)過(guò)一次超長(zhǎng)
      bool hasAlreadyMaxLength = false;
      //第一次觸發(fā)超長(zhǎng)的文案
      String lastMaxContent = "";
      
      @override
      void initState() {
          if (widget.controller.text.length == widget.selfMaxLength) {
                  //UI帶進(jìn)來(lái)的文案超長(zhǎng)码荔,記錄
                  hasAlreadyMaxLength = true;
                  lastMaxContent = widget.controller.text;
            } else {
                   //UI帶進(jìn)來(lái)的文案沒有超長(zhǎng)
                  hasAlreadyMaxLength = false;
                  lastMaxContent = "";
            }
      }
      //maxLength or inputFormatters正常設(shè)置不影響
      @override
      Widget build(BuildContext context) {
            return Container(
                  child: TextField(
                  key: widget.key,
                  ……
                  onChanged: (String value) {
                    setState(() {});
                    if (widget.onChanged != null) {
                      widget.onChanged(value);
                    }
                    _actionMaxLengthState(value);
                  },
                  ……
                )
            )
      }
    
    /***真正處理邏輯***/
   //第一次文案超長(zhǎng)情況下,把光標(biāo)定位到最后一個(gè)符號(hào)后面感挥,裁切native返回的文案
   void _resetSelection(String newText) {
    var sRunes = newText.runes;
    String result;
    for (int i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        if (result.runes.last == 105) {
          //如果刪除后剩下的還有一個(gè)空格缩搅,繼續(xù)刪除
          result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
        }
        break;
      }
    }
    TextSelection temp = widget.controller.value.selection.copyWith(
      baseOffset: result.length,
      extentOffset: result.length,
    );
    TextRange fixRange = widget.controller.value.composing;
    if (widget.controller.value.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start, end: result.length);
    }
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
    lastMaxContent = result;
  }

  //當(dāng)用戶在超長(zhǎng)情況下,繼續(xù)怎樣輸入触幼,顯示第一次超長(zhǎng)的文案硼瓣,把光標(biāo)定位到最后個(gè)字符位置。
  void _initOldDataSelection(String newText) {
    TextSelection actualSelection = widget.controller.value.selection;
    actualSelection = widget.controller.value.selection.copyWith(
      baseOffset: lastMaxContent.length,
      extentOffset: lastMaxContent.length,
    );
    //ios:當(dāng)TextRange不為-1時(shí)候置谦,下次update會(huì)把start和end直接的變量值全部丟棄堂鲤。當(dāng)你確定內(nèi)容不變時(shí)候,請(qǐng)把他們變成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
  }

  
  void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
      if (widget.maxLengthCallBack != null) widget.maxLengthCallBack();
      if (hasAlreadyMaxLength) {
        _initOldDataSelection(newText);
      } else {
        hasAlreadyMaxLength = true;
        _resetSelection(newText);
      }
    } else {
      //對(duì)controller的text設(shè)置霉祸,一定要是對(duì)value改變筑累,要不然直接設(shè)置text袱蜡,selection就是為-1默認(rèn)值丝蹭。
      widget.controller.value =
          TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
      //如果用戶刪除文案,達(dá)不到超長(zhǎng)效果坪蚁,就清除超長(zhǎng)數(shù)據(jù)
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }
}
  1. 調(diào)用時(shí)候就用封裝的XXXTextField類奔穿,然后添加selfMaxLength最大長(zhǎng)度和maxLengthCallBack最大長(zhǎng)度返回參數(shù)(callBack自己看業(yè)務(wù)吧)。


    調(diào)用示例
  • 方法二:
    自定義LengthLimitingTextInputFormatter,道理和上面挫的方法差不多敏晤,這里就不一一解析了贱田。如果用這個(gè)也要獲取超長(zhǎng)回調(diào),直接設(shè)置個(gè)callBack即可嘴脾,直接看代碼
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter/services.dart';

class MTLengthLimitingTextInputFormatter extends TextInputFormatter {
  MTLengthLimitingTextInputFormatter(this.maxLength) : assert(maxLength == null || maxLength == -1 || maxLength > 0);

  final int maxLength;

  bool hasAlreadyMaxLength = false;
///超過(guò)文本長(zhǎng)度回調(diào)
final Function maxLengthCallBack;

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue, // unused.
    TextEditingValue newValue,
  ) {
    hasAlreadyMaxLength = oldValue.text.length >= maxLength;
    if (!hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
      ///第一次超長(zhǎng)
      if(maxLengthCallBack != null) maxLengthCallBack();
      return _resetSelection(newValue);
    } else if (hasAlreadyMaxLength && maxLength != null && maxLength > 0 && newValue.text.length >= maxLength) {
      ///第二次往后超長(zhǎng)
      if(maxLengthCallBack != null) maxLengthCallBack();
      return _initOldDataSelection(oldValue, newValue);
    } else {
     hasAlreadyMaxLength = false;
      return newValue;
    }
  }

  TextEditingValue _resetSelection(TextEditingValue newValue) {
    hasAlreadyMaxLength = true;
    var sRunes = newValue.text.runes;
    String result;
    int i = 0;
    for (i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= maxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        if (result.runes.last == 105) {
          //如果刪除后剩下的還有一個(gè)空格男摧,繼續(xù)刪除
          result = String.fromCharCodes(result.runes, 0, result.runes.length - 1);
        }
        break;
      }
    }
    TextSelection temp = newValue.selection.copyWith(
      baseOffset: result.length,
      extentOffset: result.length,
    );
    TextRange fixRange = newValue.composing;
    if (newValue.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start - i, end: result.length);
    }
    return TextEditingValue(text: result, selection: temp, composing: fixRange);
  }

  TextEditingValue _initOldDataSelection(TextEditingValue oldValue, TextEditingValue newValue) {
    TextSelection actualSelection = newValue.selection;
    actualSelection = newValue.selection.copyWith(
      baseOffset: oldValue.text.length,
      extentOffset: oldValue.text.length,
    );
    //ios:當(dāng)TextRange不為-1時(shí)候,下次update會(huì)把start和end直接的變量值全部丟棄译打。當(dāng)你確定內(nèi)容不變時(shí)候耗拓,請(qǐng)把他們變成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    return TextEditingValue(text: oldValue.text, selection: actualSelection, composing: fixRange);
  }
}


遇到的問(wèn)題

一、iOS自帶原生輸入框奏司,當(dāng)用戶輸入拼音乔询,點(diǎn)擊中文的時(shí)候,輸入框中會(huì)顯示類似:w d我的韵洋。這種明顯處于bug的問(wèn)題讓我一個(gè)安卓菜雞一臉懵逼竿刁。后面仔細(xì)看了源碼的參數(shù)黄锤,才發(fā)現(xiàn)我的TextEditingValue少了一個(gè)composing屬性,這個(gè)屬性就是用來(lái)告訴 iOS 的native層哪些文案還在編輯食拜,等用戶選中要的文案后便可刪除鸵熟。所以后面我給加了composing屬性。
出問(wèn)題代碼(就用_actionMaxLengthState舉例子监婶,其他函數(shù)看上面解決辦法即可):

void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
     ……
    } else {
      //對(duì)controller的text設(shè)置旅赢,一定要是對(duì)value改變,要不然直接設(shè)置text惑惶,selection就是為-1默認(rèn)值煮盼。
      widget.controller.value = TextEditingValue(text: newText, selection: widget.controller.value.selection);
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }

修復(fù)后的代碼:

void _actionMaxLengthState(String newText) {
    if (newText.length >= widget.selfMaxLength) {
        ……
    } else {
      //對(duì)controller的text設(shè)置,一定要是對(duì)value改變带污,要不然直接設(shè)置text僵控,selection就是為-1默認(rèn)值。
      widget.controller.value =
          TextEditingValue(text: newText, selection: widget.controller.value.selection, composing: widget.controller.value.composing);
      hasAlreadyMaxLength = false;
      lastMaxContent = "";
    }
  }

二鱼冀、在第一個(gè)問(wèn)題解決后报破,緊接著在iOS上遇到第二個(gè)問(wèn)題。當(dāng)用戶多次輸入超長(zhǎng)內(nèi)容后(內(nèi)容中包含表情)千绪,會(huì)出現(xiàn)表情后的文案全部消失充易。這里出現(xiàn)的問(wèn)題還是composing,沒有完全理解含義荸型,直接使用native返回的composing所致盹靴。
出問(wèn)題代碼(_initOldDataSelection方法):

void _initOldDataSelection(String newText) {
    ……
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: widget.controller.value.composing);
  }

修復(fù)后的代碼(修復(fù)后的意思即是用戶多次超長(zhǎng)的內(nèi)容,我?guī)陀脩艋謴?fù)瑞妇,屬于文本不在編輯狀態(tài)稿静,iOS的native層不能把我的內(nèi)容刪除):

void _initOldDataSelection(String newText) {
    ……
    //ios:當(dāng)TextRange不為-1時(shí)候,下次update會(huì)把start和end直接的變量值全部丟棄辕狰。當(dāng)你確定內(nèi)容不變時(shí)候改备,請(qǐng)把他們變成-1
    TextRange fixRange = TextRange(start: -1, end: -1);
    widget.controller.value = TextEditingValue(text: lastMaxContent, selection: actualSelection, composing: fixRange);
  }

三、文案剪切出現(xiàn)崩潰蔓倍。原因是我通過(guò)substring()方法來(lái)剪切悬钳,眾所周知表情都是由多個(gè)字符拼接而成的表達(dá)式,如果剛好最后一個(gè)是表情偶翅,我把其中表情一個(gè)字符剪切掉了默勾,導(dǎo)致無(wú)法正常顯示表情符號(hào),自然會(huì)崩潰倒堕。
出問(wèn)題代碼(_initOldDataSelection方法):

void _resetSelection(String newText) {
    String result = newText.substring(0, widget.selfMaxLength);
    ……
  }

修復(fù)的代碼:

void _resetSelection(String newText) {
    var sRunes = newText.runes;
    String result;
    for (int i = 0; i < sRunes.length; i++) {
      if (String.fromCharCodes(sRunes, 0, sRunes.length - i).length <= widget.selfMaxLength) {
        result = String.fromCharCodes(sRunes, 0, sRunes.length - i);
        break;
      }
    }
    ……
  }

四灾测、在第一次達(dá)到最大長(zhǎng)度時(shí)候,出現(xiàn)數(shù)組越界的情況。這個(gè)也是因?yàn)闆]有正確處理好TextRange的start和end媳搪,直接無(wú)腦用native層返回的start和end铭段。所以裁切后的text的TextRange我們要根據(jù)實(shí)際長(zhǎng)度定義end值。
出問(wèn)題代碼(_resetSelection方法):

void _resetSelection(String newText) {
   ……
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: widget.controller.value.composing);
    lastMaxContent = result;
  }

修復(fù)的代碼:

void _resetSelection(String newText) {
    ……
    //result最后展示在屏幕上的文案
    TextRange fixRange = widget.controller.value.composing;
    if (widget.controller.value.composing.end > result.length) {
      fixRange = TextRange(start: fixRange.start, end: result.length);
    }
    widget.controller.value = TextEditingValue(text: result, selection: temp, composing: fixRange);
    lastMaxContent = result;
  }

五秦爆、切換輸入法時(shí)候序愚,文案恢復(fù)修改前。這個(gè)屬于業(yè)務(wù)層用法出錯(cuò)等限,每次build都重新生成一個(gè)textController爸吮,導(dǎo)致源碼修改的controller的text,每次build都被丟掉了望门。

錯(cuò)誤方法:給一個(gè)TextField的textController每次一build都新建一個(gè)新的controller形娇。

@override
Widget build(BuildContext context) {
return Container(
child: TextField(
……
          controller: TextEditingController.fromValue(TextEditingValue(
          text: _txtController.text,
          selection: TextSelection.fromPosition(
                  TextPosition(affinity: TextAffinity.downstream, offset: _txtController.text.length)))),
          )
       )
}


正確方法:
@override
void initState() {
super.initState();
        _txtController.text = widget.defaultName;
        _txtController.value = TextEditingValue(
                    text: _txtController.text,
                    selection: TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset:                   _txtController.text.length)));
}

@override
Widget build(BuildContext context) {
return Container(
            child: TextField(
                inputFormatters: [
                        MTLengthLimitingTextInputFormatter(15),
                ],
            ……
            )
        )
}

總結(jié)

  1. flutter官網(wǎng)確實(shí)有很多存在的問(wèn)題,但這些問(wèn)題正為我們提供研究源碼的動(dòng)力筹误。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桐早,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子厨剪,更是在濱河造成了極大的恐慌哄酝,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祷膳,死亡現(xiàn)場(chǎng)離奇詭異陶衅,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)直晨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門搀军,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人抡秆,你說(shuō)我怎么就攤上這事奕巍∫鞑撸” “怎么了儒士?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)檩坚。 經(jīng)常有香客問(wèn)我着撩,道長(zhǎng),這世上最難降的妖魔是什么匾委? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任拖叙,我火速辦了婚禮,結(jié)果婚禮上赂乐,老公的妹妹穿的比我還像新娘薯鳍。我一直安慰自己,他們只是感情好挨措,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布挖滤。 她就那樣靜靜地躺著崩溪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斩松。 梳的紋絲不亂的頭發(fā)上伶唯,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音惧盹,去河邊找鬼乳幸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钧椰,可吹牛的內(nèi)容都是我干的粹断。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫡霞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姿染!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起秒际,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悬赏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后娄徊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闽颇,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年寄锐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兵多。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡橄仆,死狀恐怖剩膘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盆顾,我是刑警寧澤怠褐,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站您宪,受9級(jí)特大地震影響奈懒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宪巨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一磷杏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捏卓,春花似錦极祸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)峦椰。三九已至,卻和暖如春汰规,著一層夾襖步出監(jiān)牢的瞬間汤功,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工溜哮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滔金,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓茂嗓,卻偏偏與公主長(zhǎng)得像餐茵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子述吸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

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