用Flutter自定義View钝鸽,實(shí)現(xiàn)高德地圖三級聯(lián)動(dòng)Drawer

作者:吉哈達(dá)
鏈接:https://juejin.im/post/5f179a5b6fb9a07ec07b56cd

前言

一直覺得高德地圖的首頁Drawer滑動(dòng)起來很漂亮,還有一些科技感萎庭,之前用android實(shí)現(xiàn)了一遍告材,趁著最近不忙再用Flutter實(shí)現(xiàn)一遍。

示意圖

為了方便區(qū)分布局結(jié)構(gòu)步鉴,我使用了不同的顏色

Drawer高度狀態(tài)

可以看到drawer 高度有三種情況:

最大高度

距離頂部有一小段空間揪胃,這里空間高度定位70,

drawer的高度為:屏幕高度-70

中等高度

這里我們將drawer的顯示高度定位300

最小高度

這里drawer的顯示高度定位150

Drawer的ui 結(jié)構(gòu)

可以看到drawer內(nèi)部的ui分為三塊:

搜索區(qū)域氛琢、多功能區(qū)域喊递、擴(kuò)展區(qū)域
復(fù)制代碼

同時(shí)drawer在最大高度和中等高度之間滾動(dòng)時(shí),多功能區(qū)域需要縮進(jìn)/展開 到 擴(kuò)展區(qū)域

代碼實(shí)現(xiàn)

基本布局

因?yàn)榇翱谧畹讓有枰@示地圖阳似,同時(shí)drawer要顯示不同的高度骚勘,所以這里我采用stack作為跟布局:

size由mediaQuery.of(context)獲得
復(fù)制代碼
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.white,
      child: Container(
        color: Colors.greenAccent,
        width: size.width,height: size.height,
        child: Stack(
          children: <Widget>[

            Positioned(
            top: initPositionTop,
            .......省去Drawer部分代碼
            )

          ],
        ),
      ),
    );
復(fù)制代碼

我們通過positioned包裹drawer,然后通過top來控制drawer上下移動(dòng)的高度,為了捕獲觸摸事件撮奏,我們需要用GestureDetector對我們的drawer進(jìn)行包裹俏讹,代碼:

            Positioned(
              top: initPositionTop,
              child: GestureDetector(
                onVerticalDragStart: verticalDragStart,
                onVerticalDragUpdate: verticalDragUpdate,
                onVerticalDragEnd: verticalDragEnd,
                ///Drawer
                child: Container(
                  width: size.width,height: drawerHeight,
                  color: Colors.white,
                  ///多功能區(qū)域需要實(shí)現(xiàn)縮進(jìn)和站看,所以這里使用stack作為drawer的內(nèi)部根布局
                  child: Stack(
                    children: <Widget>[
                      ///搜索區(qū)域
                      Container(
                        alignment: Alignment.center,
                        color: Colors.pink,
                        width: size.width,height: searchHeight - minHeight,
                        child: Text('我是搜索'),
                      ),
                      ///多功能區(qū)域
                      Positioned(
                        top: searchHeight - minHeight,
                        child: Container(
                          alignment: Alignment.center,
                          color: Colors.white,
                          width: size.width,height: rowH * 3+20,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              normalRow(),
                              normalRow(),
                              Container(
                                color: Colors.grey[300],
                                width: size.width,height: rowH,
                                alignment: Alignment.topCenter,
                                child: Text('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),
                              )
                            ],
                          ),
                        ),
                      ),
                      ///擴(kuò)展區(qū)域
                      Positioned(
                        top: expandPosTop + topArea,
                        child: Container(
                          color: Colors.lightGreen,
                          alignment: Alignment.topCenter,
                          width: size.width,height: drawerHeight - searchHeight -rowH,///這里需要在滾動(dòng)時(shí)向下滑動(dòng)
                          child: Text('我是擴(kuò)展區(qū)域'),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            )
復(fù)制代碼

至此整個(gè)UI布局就搞定了畜吊,接下來處理手勢滑動(dòng)泽疆。

手勢處理

首先我們只需要處理垂直滑動(dòng),因此在回調(diào)中玲献,我們實(shí)現(xiàn)這三個(gè)方法:

              child: GestureDetector(
                onVerticalDragStart: verticalDragStart, ///第一次觸摸屏幕時(shí)觸發(fā)
                onVerticalDragUpdate: verticalDragUpdate,///滑動(dòng)時(shí)會(huì)持續(xù)調(diào)用此方法
                onVerticalDragEnd: verticalDragEnd,///手指離屏?xí)r會(huì)調(diào)用此方法
復(fù)制代碼

dragStart

當(dāng)手指觸摸屏幕時(shí)殉疼,我們需要記錄下點(diǎn)擊位置:

Offset lastPos;

  void verticalDragStart(DragStartDetails details){
    lastPos = details.globalPosition;
  }
復(fù)制代碼

dragUpdate

之后在用戶滑動(dòng)時(shí)逗嫡,我們刷新drawer的position的top值(即initPositionTop),以此來達(dá)到drawer的滑動(dòng)效果株依。

如果只是簡單的滑動(dòng),我們可以直接將initPositionTop加上滑動(dòng)差值即可延窜,但是根據(jù)經(jīng)驗(yàn)判斷恋腕,后面肯定會(huì)需要滑動(dòng)方向,所以我在這里順便把滑動(dòng)的方向也記錄下來逆瑞,這個(gè)可以根據(jù)滑動(dòng)差值的正負(fù)來判斷:

enum SlideDirection{
  Up,
  Down
}
復(fù)制代碼
  void verticalDragUpdate(DragUpdateDetails details){

    double dis = details.globalPosition.dy - lastPos.dy;
    if(dis<0){
      direction = SlideDirection.Up;
    }else{
      direction = SlideDirection.Down;
    }

    if(direction == SlideDirection.Up){
      if(initPositionTop <= top1+cacheDy) return;
    }else if(direction == SlideDirection.Down){
      if(initPositionTop >= top3-cacheDy) return;
    }

    initPositionTop += dis;
    ///處理完一次后荠藤,記下當(dāng)前的位置
    lastPos = details.globalPosition;
    ///這里個(gè)方法暫時(shí)不用管
    refreshExpandWidgetTop();
    setState(() {

    });
  }
復(fù)制代碼

dragEnd

這里我們什么都不需要做,代碼如下:

  void verticalDragEnd(DragEndDetails details){
  }
復(fù)制代碼

這時(shí)我們運(yùn)行發(fā)現(xiàn)获高,drawer可以跟著手指的滑動(dòng)表現(xiàn)收起/展開的效果哈肖,但是我們的手指離屏后,drawer也就停在那了(原始版抽屜)念秧。

參見高德淤井,可以看到抽屜始終會(huì)停留在三級狀態(tài)中的一級,如果手指滑動(dòng)超出界限/未到界限摊趾,抽屜會(huì)自動(dòng)滾動(dòng)/滾回到最近的等級高度币狠,現(xiàn)在我們要進(jìn)行升級了。

升級

準(zhǔn)備工作

首先我們要記錄一下三個(gè)高度對應(yīng)的position的top值(drawer的實(shí)時(shí)top值以后就叫initPositionTop了):

  ///stack 中 根container 的position 的top 值的三種情況
  double top1;// DrawerLvl   lvl 1
  double top2;// DrawerLvl   lvl 2
  double top3;// DrawerLvl   lvl 3

  double initPositionTop;
  ///初始化
    top1 = size.height - drawerHeight;
    top2 = size.height - searchHeight;
    top3 = size.height - minHeight;
    ///頁面最初顯示的是 top2等級
    initPositionTop = top2;

復(fù)制代碼

然后我們需要記錄一下drawer的狀態(tài):

enum DrawerLvl{
  LVL1,
  LVL2,
  LVL3
}

  ///抽屜層級
  DrawerLvl drawerLvl = DrawerLvl.LVL2;
  ///滑動(dòng)方向
  SlideDirection direction;
復(fù)制代碼

分別對應(yīng)top1砾层,top2漩绵,top3

當(dāng)我們滑動(dòng)時(shí),如果從top1滑向top2肛炮,但是未到top2的高度止吐,就松手了,這時(shí)我們需要完成剩下的操作侨糟,這就用到了

AnimationController
Animation
復(fù)制代碼
animationController = AnimationController(vsync: this,duration: Duration(milliseconds: 300));
復(fù)制代碼

具體應(yīng)該滑回top1碍扔,還是滑向top2呢?這里我們需要定兩個(gè)閾值:

  ///層級之間的閾值
  double threshold1To2;
  double threshold2To3;
    ///構(gòu)造函數(shù)
  DrawerDemoState(this.size){
    drawerHeight = size.height-paddingTop;
    threshold1To2 = size.height/3;
    threshold2To3 = size.height - 250;
  }
復(fù)制代碼

升級 dragStart

現(xiàn)在我們開始對原有的方法升級

  void verticalDragStart(DragStartDetails details){
    ///確定drawer 初始狀態(tài)
    markDrawerLvl();
    ///將原有的動(dòng)畫置空
    animation = null;
    ///將控制器停止和復(fù)位
    if(animationController.isAnimating){
      animationController.stop();
    }
    animationController.reset();
    lastPos = details.globalPosition;
    log('start', '$initPositionTop');
  }
復(fù)制代碼

當(dāng)用戶觸摸時(shí)秕重,我們先要確定drawer的初始狀態(tài):


  markDrawerLvl(){
    double l1 = (top1-initPositionTop).abs();
    double l2 = (top2-initPositionTop).abs();
    double l3 = (top3-initPositionTop).abs();

    if(l1 == (math.min(l1, math.min(l2, l3)))){
      drawerLvl = DrawerLvl.LVL1;
    }else if(l2 == (math.min(l1, math.min(l2, l3)))){
      drawerLvl = DrawerLvl.LVL2;
    }else {
      drawerLvl = DrawerLvl.LVL3;
    }

  }
復(fù)制代碼

升級 dragUpdate

  void verticalDragUpdate(DragUpdateDetails details){

    double dis = details.globalPosition.dy - lastPos.dy;
    if(dis<0){
      direction = SlideDirection.Up;
    }else{
      direction = SlideDirection.Down;
    }

    ///cacheDy 避免滑動(dòng)過快溢出范圍導(dǎo)致的判斷失效

    if(direction == SlideDirection.Up){
    ///避免drawer滑出屏幕
      if(initPositionTop <= top1+cacheDy) return;
    }else if(direction == SlideDirection.Down){
      if(initPositionTop >= top3-cacheDy) return;
    }

    initPositionTop += dis;
    lastPos = details.globalPosition;
    ///暫時(shí)不用管
    refreshExpandWidgetTop();
    setState(() {

    });
  }
復(fù)制代碼

升級dragEnd

在用戶手指離開屏幕時(shí)蕴忆,我們就要進(jìn)行處理了,即:drawer是繼續(xù)滾動(dòng)悲幅,還是復(fù)位套鹅。

  void verticalDragEnd(DragEndDetails details){
    adjustPositionTop(details);
  }
復(fù)制代碼

這個(gè)方法較長,我將說明寫在注釋里

  void adjustPositionTop(DragEndDetails details){

    switch(direction){
      case SlideDirection.Up:
        if(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){
          ///用戶fling速度超過閾值后汰具,直接判定為滑向下一級別
          switch(drawerLvl){
            case DrawerLvl.LVL1:
            ///處于頂部上滑時(shí)卓鹿,不需要做處理
              // TODO: Handle this case.
              break;
            case DrawerLvl.LVL2:
              slideTo(begin: initPositionTop,end: top1);
              break;
            case DrawerLvl.LVL3:
              slideTo(begin: initPositionTop,end: top2);
              break;
          }
        }else{
            ///未超過閾值的話,我們則進(jìn)行復(fù)位或者繼續(xù)滑動(dòng)
          if(initPositionTop >= top1 && initPositionTop <= top2){
            ///在1留荔、2級之間

            這里根據(jù)手指離屏位置吟孙,進(jìn)行復(fù)位或者滑向下一等級高度的處理
            if(initPositionTop <= threshold1To2){
              ///小于二分之一屏幕高度 滾向top1

              slideTo(begin:initPositionTop, end:top1);
            }else{
              ///滑向top2

              slideTo(begin: initPositionTop,end: top2);
            }
          }else if(initPositionTop >= top2 && initPositionTop <= top3){
            ///2-3之間
            if(initPositionTop <= threshold2To3){
              ///滑向2
              slideTo(begin: initPositionTop,end: top2);
            }else{
              ///滑向3
              slideTo(begin: initPositionTop,end: top3);
            }

          }
        }
        break;
      case SlideDirection.Down:
        ///原理同上
        if(details.velocity.pixelsPerSecond.dy.abs() > thresholdV){

          switch(drawerLvl){
            case DrawerLvl.LVL1:
              slideTo(begin: initPositionTop,end: top2);
              break;
            case DrawerLvl.LVL2:
              slideTo(begin: initPositionTop,end: top3);
              break;
            case DrawerLvl.LVL3:
              //todo nothing
              break;
          }
        }else{
          if(initPositionTop >= top1 && initPositionTop <= top2){
            ///在1澜倦、2級之間

            if(initPositionTop <= threshold1To2){
              ///小于二分之一屏幕高度 滾向top1

              slideTo(begin: initPositionTop,end:top1);
            }else{
              ///滑向top2

              slideTo(begin: initPositionTop,end: top2);
            }
          }else if(initPositionTop >= top2 && initPositionTop <= top3){
            ///2-3之間
            if(initPositionTop <= threshold2To3){
              ///滑向2
              slideTo(begin: initPositionTop,end: top2);
            }else{
              ///滑向3
              slideTo(begin: initPositionTop,end: top3);
            }

          }
        }
        break;
    }
  }
復(fù)制代碼

在補(bǔ)全滑動(dòng)這里,我們交給animationController來處理:

    ///begin基本是手指離屏的位置杰妓,end則是目標(biāo)等級的top值
  slideTo({double begin,double end})async{
    animation = Tween<double>(begin: begin,end:end ).animate(animationController);
    await animationController.forward();
  }
復(fù)制代碼

在動(dòng)畫的listener中藻治,我們刷新initPositionTop的值:

    animationController.addListener(() {
      if(animation == null) return;
      ///暫時(shí)不用管
      refreshExpandWidgetTop();
      setState(() {
        initPositionTop = animation.value;
      });

    });
復(fù)制代碼

至此我們就相對完善的完成了drawer的滑動(dòng)功能。

多功能widget 顯隱效果

繼續(xù)觀察drawer內(nèi)部的widget巷挥,我們可以看到在top1和top2之間滾動(dòng)時(shí)桩卵,內(nèi)部的多功能區(qū)域也會(huì)進(jìn)行相應(yīng)的縮進(jìn)和伸出,接下來我們實(shí)現(xiàn)這個(gè)倍宾。

UI布局

因?yàn)槲覀冎恍枰苿?dòng)擴(kuò)展區(qū)域雏节,就可以實(shí)現(xiàn)多功能區(qū)的滑出/收起 效果,所以我們可以用stack來完成基本的布局:

Stack(
                    children: <Widget>[
                      ///搜索
                      Container(
                        alignment: Alignment.center,
                        color: Colors.pink,
                        width: size.width,height: searchHeight - minHeight,
                        child: Text('我是搜索'),
                      ),
                      ///多功能區(qū)
                      Positioned(
                        top: searchHeight - minHeight,
                        child: Container(
                          alignment: Alignment.center,
                          color: Colors.white,
                          width: size.width,height: rowH * 3+20,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.spaceBetween,
                            children: <Widget>[
                              normalRow(),
                              normalRow(),
                              Container(
                                color: Colors.grey[300],
                                width: size.width,height: rowH,
                                alignment: Alignment.topCenter,
                                child: Text('常去的地方',style: TextStyle(fontSize: 18,color: Colors.black),),
                              )
                            ],
                          ),
                        ),
                      ),
                      ///擴(kuò)展區(qū)
                      Positioned(
                        top: expandPosTop + topArea,
                        child: Container(
                          color: Colors.lightGreen,
                          alignment: Alignment.topCenter,
                          width: size.width,height: drawerHeight - searchHeight -rowH,///這里需要在滾動(dòng)時(shí)向下滑動(dòng)
                          child: Text('我是擴(kuò)展區(qū)域'),
                        ),
                      ),
                    ],
                  ),
復(fù)制代碼

搜索區(qū)和多功能區(qū)高职,只需要調(diào)整top钩乍,使他們順序排列即可。

而擴(kuò)展區(qū)怔锌,我們需要在頁面初始是遮住一部分多功能區(qū)(只漏出一行圓)寥粹。

方便起見,將多功能的高度定位 rowH * 3;
復(fù)制代碼

那么擴(kuò)展區(qū)的top初始值就是多功能的top + rowH埃元,這里我們給擴(kuò)展區(qū)的top值定義一個(gè)變量:

expandPosTop = 多功能區(qū)的top + rowH
復(fù)制代碼

進(jìn)而排作,我們可以確定,expandPosTop的變化范圍是:

我們給這個(gè)變化值定義一個(gè)變量:topArea

topArea = [0 - rowH * 2]亚情;
復(fù)制代碼

最終擴(kuò)展區(qū)的代碼如下:

                      ///擴(kuò)展區(qū)域
                      Positioned(
                        top: expandPosTop + topArea,
                        child: Container(
                          color: Colors.lightGreen,
                          alignment: Alignment.topCenter,
                          width: size.width,height: drawerHeight - searchHeight -rowH,///這里需要在滾動(dòng)時(shí)向下滑動(dòng)
                          child: Text('我是擴(kuò)展區(qū)域'),
                        ),
                      ),
復(fù)制代碼

整體UI布局就完成了妄痪,我們接著實(shí)現(xiàn)滾動(dòng)功能。

擴(kuò)展區(qū)滑動(dòng)

我們在dragUpdate和動(dòng)畫的listener中見到過這個(gè)方法:

refreshExpandWidgetTop();//這里就是實(shí)現(xiàn)對應(yīng)功能的
復(fù)制代碼

這里我把說明寫在注釋里楞件,方便閱讀

  ///刷新 擴(kuò)展區(qū)域的 position top值
  ///這里的差值是 rowH * 2
  refreshExpandWidgetTop(){
    ///首先衫生,我們根據(jù)initPositionTop,和top2 - top1 之間的差值土浸,來計(jì)算滑動(dòng)進(jìn)度
    double progress = (initPositionTop-top2).abs() /(top2 - top1).abs();
    ///判斷是從top1滑向top2 還是反著
    if(drawerLvl == DrawerLvl.LVL2){
      ///lvl2 滑向 lvl3時(shí) 不做處理
      if(initPositionTop > top2) return;
      ///之后我們根據(jù)進(jìn)度罪针,來刷新topArea的值
      ///這個(gè)值總是會(huì)在 0 到 rowh*2 這個(gè)范圍內(nèi)變化,具體由滑動(dòng)方向來定
      topArea =   (progress * (rowH*2).clamp(0, rowH*2));
    }else if(drawerLvl == DrawerLvl.LVL1){
      ///lvl2 滑向 lvl3時(shí) 不做處理
      if(initPositionTop > top2) return;
      topArea = (progress) * (rowH*2).clamp(0, rowH*2);
    }
  }
復(fù)制代碼

當(dāng)我們在調(diào)用上述方法外面刷新時(shí)黄伊,就會(huì)看到多功能區(qū)域的收起/伸出的效果了(給加點(diǎn)陰影會(huì)更好看)泪酱,至此我們整個(gè)功能就實(shí)現(xiàn)了,如果對你有幫助點(diǎn)歌贊或和star吧还最。 :)

點(diǎn)關(guān)注墓阀,更多Android開發(fā)技能~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拓轻,隨后出現(xiàn)的幾起案子斯撮,更是在濱河造成了極大的恐慌,老刑警劉巖扶叉,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勿锅,死亡現(xiàn)場離奇詭異帕膜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)溢十,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進(jìn)店門垮刹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人张弛,你說我怎么就攤上這事荒典。” “怎么了乌庶?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長契耿。 經(jīng)常有香客問我瞒大,道長,這世上最難降的妖魔是什么搪桂? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任透敌,我火速辦了婚禮,結(jié)果婚禮上踢械,老公的妹妹穿的比我還像新娘酗电。我一直安慰自己,他們只是感情好内列,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布撵术。 她就那樣靜靜地躺著,像睡著了一般话瞧。 火紅的嫁衣襯著肌膚如雪嫩与。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天交排,我揣著相機(jī)與錄音划滋,去河邊找鬼。 笑死埃篓,一個(gè)胖子當(dāng)著我的面吹牛处坪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播架专,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼同窘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了部脚?” 一聲冷哼從身側(cè)響起塞椎,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎睛低,沒想到半個(gè)月后案狠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體服傍,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年骂铁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吹零。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拉庵,死狀恐怖灿椅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钞支,我是刑警寧澤茫蛹,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站烁挟,受9級特大地震影響婴洼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撼嗓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一柬采、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧且警,春花似錦粉捻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杏头,卻和暖如春树酪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背大州。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工续语, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厦画。 一個(gè)月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓疮茄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親根暑。 傳聞我的和親對象是個(gè)殘疾皇子力试,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評論 2 359