Flutter動畫之自定義動畫組件-FlutterLayout

前言:

本文將自定義一個FlutterWidget的動畫組件,Flutter有顫動的意思
在此之前會講一下AnimatedWidget與AnimatedBuilder是什么蜜另,如何使用
所以本文是一篇挺重要的文章,不僅是內容雌贱,還有思想和靈魂焰络。
今天也悟到了一段話分享給大家:

當你遇到一群共事之人,開始難免會覺得某某人高冷而帥氣梅鹦,某某人美麗而大方,某某人能力超級強  
作為普通人的你也許很想和他們結交但又很難進入他們的世界,于是你在角落靜靜凝望,細心觀察
隨著時間的流逝,也許偶爾的交談构捡,你會發(fā)現(xiàn)他們并非看上去的那么難以接近,于是開始和他們交流  
隨著關系的加深,也許某個傍晚浪讳,你們會走在回去的路上佛猛,訴說著人生,從此漸漸無話不說惑芭。
然后會發(fā)現(xiàn),這世間的隔閡也許只是自己為自己施加的屏障,這個屏障會為你抵御傷害,
但它同時也可能讓你失去一個對的人,一個未來的止步于陌生的知己继找。

學習亦如此遂跟,一個框架就是那個高冷而帥氣公子,一個類就是那個美麗而大方姑娘婴渡,結合上面再看看幻锁。  
有時候錯過了,也就錯過了,你不可能認識所有的人边臼,但你可以用真誠選擇一位知己哄尔。
認識的人當然越多越好,但知己硼瓣,寧缺毋濫究飞。 ----XXX,你現(xiàn)在還好嗎? 
                                                    (張風捷特烈 2019.7.19 字)

首先置谦,留圖鎮(zhèn)樓

image
image

1.AnimatedWidget與AnimatedBuilder

1.1:前情回顧

現(xiàn)在回到昨天的最后一個組件,這樣寫不夠優(yōu)雅,什么東西都在一塊
Flutter中提供了AnimatedWidget類可以讓動畫的組件更加簡潔

image
class FlutterText extends StatefulWidget {
  var str;
  var style;

  FlutterText(this.str, this.style);
  _FlutterTextState createState() => _FlutterTextState();
}

class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    
    animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {
          setState(() {});
        }
      });
    controller.forward();
  }

  Widget build(BuildContext context) {
    var result = Transform(
      transform: Matrix4.rotationZ(animation.value * pi / 180),
      alignment: Alignment.center,
      child: Text(
        widget.str,
        style: widget.style,
      ),
    );
    return result;
  }
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

2.使用AnimatedWidget抽離組件

AnimatedWidget也不是什么神奇的東西,它的優(yōu)勢在于:
將組件的創(chuàng)建邏輯單獨封裝在一個類中,而且不用再調用setState方法堂鲤,也能自動更新信息

image
class FlutterText extends StatefulWidget {
  var str;
  var style;
  FlutterText(this.str, this.style);
  _FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller);
    controller.forward();
  }
  Widget build(BuildContext context) {
    return AnimateWidget(animation: animation);
  }
  
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimateWidget extends AnimatedWidget{
  AnimateWidget({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    var result = Transform(
      transform: Matrix4.rotationZ(animation.value * pi / 180),
      alignment: Alignment.center,
      child: Text(
        "捷",
        style: TextStyle(fontSize: 50),
      ),
    );
    return result;
  }
}

可以看出代碼明確了很多,AnimateWidget專門負責Widget的構建
FlutterText只注重Animation構成,分工明確媒峡,易于復用瘟栖、維護和拓展


3.使用AnimatedBuilder抽離動畫

AnimatedWidget不挺好的嗎,又來一個AnimatedBuilder什么鬼
AnimateWidget負責組件的抽離,可以看出組件中雜糅了動畫邏輯
而AnimatedBuilder恰好相反,它不在意組件是什么谅阿,只是將動畫抽離達到復用簡單
這樣針對不同的組件半哟,都可以產(chǎn)生同樣的動畫效果酬滤,比如傳入一個Image

image
class FlutterText extends StatefulWidget {
  final Widget child;
  FlutterText({this.child});
  _FlutterTextState createState() => _FlutterTextState();
}

class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);

    animation = TweenSequence<double>([ //使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return FlutterAnim(animation: animation,child: widget.child,);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

class FlutterAnim extends StatelessWidget {
  FlutterAnim({this.child, this.animation});
  final Widget child;
  final Animation<double> animation;
  Widget build(BuildContext context) {
    var result = AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Transform(
                transform: Matrix4.rotationZ(animation.value * pi / 180),
                alignment: Alignment.center,
                child: this.child);
          },
    );
    return Center(child: result,);
  }
}

---->[使用]----
var child = Image(
  image: AssetImage("images/icon_head.png"),
);
var scaffold = Scaffold(
  body: Center(child: FlutterText(child: child),),
);

var app = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home:scaffold,
);

void main() => runApp(app);

可以看到,現(xiàn)在不止針對于文字,對于所有的Widget都有效寓涨,實現(xiàn)了功能的更高層抽象盯串。


2.組件之所為組件

2.1:組件是什么

模塊化的思想大家應該都聽過,為了讓已有代碼更好復用,將項目拆成不同模塊
組件也是這樣戒良,對于一個頁面,便是組件的組合体捏,可以拆裝,拼湊和批量生成
在代碼中我們可以很輕易的將多個控件批量動效糯崎。比如一段話的每個字都有效果:

image
_formChild(String str) {
  var li = <Widget>[];
  for (var i = 0; i < str.length; i++) {
    li.add(FlutterText(child: Text(str[i],style: TextStyle(fontSize: 30),),
    ));
  }
  return li;
}

var textZone=Row(children:_formChild("代碼,改變生活"),mainAxisSize: MainAxisSize.min,);

使用_formChild批量生成單個文字,每個文字都加有抖動的光環(huán),所以呈現(xiàn)每個字都抖動的效果


2.2:FlutterText的修改與封裝

現(xiàn)在類名叫FlutterText有點不妥了几缭,它包含一個孩子,可以讓其中的孩子抖動,改名:FlutterLayout
那現(xiàn)在想讓每個文字都抖一下沃呢,每次都寫這么多也不爽,所以可以單獨封裝一下
這里FlutterText繼承自Text,并定義所有屬性年栓。在build方法里生成剛才的帶有顫動效果的組件

image
class FlutterText extends Text {

  const FlutterText(
    this.data, {
    Key key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
  }) : super(data);

  final String data;
  final TextStyle style;
  final StrutStyle strutStyle;
  final TextAlign textAlign;
  final TextDirection textDirection;
  final Locale locale;
  final bool softWrap;
  final TextOverflow overflow;
  final double textScaleFactor;
  final int maxLines;
  final String semanticsLabel;
  final TextWidthBasis textWidthBasis;

  @override
  Widget build(BuildContext context) {
    var textZone = Row(
      children: _formChild(data),
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
    );

    return textZone;
  }

  _formChild(String str) {
    var li = <Widget>[];
    for (var i = 0; i < str.length; i++) {
      li.add(FlutterLayout(
        child: Text(
          str[i],
          style: style,
          strutStyle: strutStyle,
          textAlign: textAlign,
          textDirection: textDirection,
          locale: locale,
          softWrap: softWrap,
          overflow: overflow,
          textScaleFactor: textScaleFactor,
          maxLines: maxLines,
          semanticsLabel: semanticsLabel,
          textWidthBasis: textWidthBasis,
        ),
      ));
    }
    return li;
  }
}


2.3:FlutterText的使用

你可以完全當它是一個Text來用,只不過有個抖動的效果

image
var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var text = FlutterText("代碼,改變生活", style: TextStyle(
    color: Colors.blue,
    fontSize: 30,
    letterSpacing: 3
),);

var scaffold = Scaffold(
  body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[child, text],
  ),),
);

var app = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: scaffold,
);

void main() => runApp(app);

這樣一個抖動的Text就完成了,本文結束了嗎?不薄霜,才剛剛開始某抓。


2.升級FlutterLayout的功能

image
2.1.抖動樣式:RockMode

分上下抖動,左右抖動惰瓜,搖擺抖動搪缨,隨機抖動

enum RockMode {
  random, //隨機
  up_down, //上下
  left_right, //左右
  lean //傾斜
}

2.2.定義配置參數(shù):AnimConfig
class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  AnimConfig({this.duration, this.offset, this.mode});
}

2.3.FlutterLayout具體實現(xiàn)

這里只是把常量配置參數(shù)化,在生成_formTransform的時候根據(jù)模式來生成

class FlutterLayout extends StatefulWidget {
  final Widget child;
  final AnimConfig config;
  FlutterLayout({this.child, this.config});
  _FlutterLayoutState createState() => _FlutterLayoutState();
}
class _FlutterLayoutState extends State<FlutterLayout>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    controller = AnimationController(
        duration: Duration(milliseconds: widget.config.duration), vsync: this);
    var dx = widget.config.offset;
    var sequence = TweenSequence<double>([
      //使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: dx), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: dx, end: -dx), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: -dx, end: dx), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: dx, end: 0), weight: 4),
    ]);
    animation = sequence.animate(controller)
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {}
      });
    controller.forward();
  }
  Widget build(BuildContext context) {
    return FlutterAnim(
        animation: animation, child: widget.child, config: widget.config);
  }
  dispose() {
    controller.dispose();
    super.dispose();
  }
}
class FlutterAnim extends StatelessWidget {
  FlutterAnim({this.child, this.animation, this.config});
  Random random = Random();
  final Widget child;
  final Animation<double> animation;
  final AnimConfig config;
  Widget build(BuildContext context) {
    var result = AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        return new Transform(
            transform: _formTransform(config),
            alignment: Alignment.center,
            child: this.child);
      },
    );
    return Center(
      child: result,
    );
  }
  _formTransform(AnimConfig config) {//分類獲取
    var result;
    switch (config.mode) {
      case RockMode.random:
        result = Matrix4.rotationZ(animation.value * pi / 180);
        break;
      case RockMode.up_down:
        result = Matrix4.translationValues(0, animation.value*pow(-1, random.nextInt(20)), 0);
        break;
      case RockMode.left_right:
        result = Matrix4.translationValues(animation.value*pow(-1, random.nextInt(20)), 0, 0);
        break;
      case RockMode.lean:
        result = Matrix4.rotationZ(animation.value * pi / 180);
        break;
    }
    return result;
  }
}

2.4.FlutterText的修改
class FlutterText extends Text {

  FlutterText(this.data, {
    //略同...
    this.config,
  }) : super(data);
  final AnimConfig config;
  Random random = Random();

  _formChild(String str) {
    var li = <Widget>[];
    for (var i = 0; i < str.length; i++) {
      li.add(FlutterLayout(
        config: AnimConfig(duration: config.duration,offset: config.offset,mode: _dealRandom()),
        child: Text(
            //略同...
        ),
      ));
    }
    return li;
  }

  RockMode _dealRandom() {
    var modes = [RockMode.lean, RockMode.up_down, RockMode.left_right];
    return modes[random.nextInt(3)];
  }
}

2.5:使用MultiShower測試一下

關于MultiShower鸵熟,可以看一下Flutter自定義組件-MultiShower副编,主要用于批量產(chǎn)生不同配置的同類組件

image
var configs=<AnimConfig>[
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.random),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
  AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];

var configsInfo=["random","up_down","left_right","lean"];

var show = MultiShower(configs,(config) =>FlutterText("代碼,改變生活",
  config:config,
  style: TextStyle(
      color: Colors.blue,
      fontSize: 30,
      letterSpacing: 3
  ),),infos: configsInfo,width: 250,color: Colors.transparent,);

var scaffold = Scaffold(
  body: Center(child: show,)
);

另外還有我們的FlutterLayout,可以包含任意組件,那Image來測試

image
var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var configs=<AnimConfig>[
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
  AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];

var configsInfo=["up_down","left_right","lean"];

var show = MultiShower(configs,(config) =>FlutterLayout(child: child,
  config:config,
 ),infos: configsInfo,width: 200,color: Colors.transparent,);

var scaffold = Scaffold(
  body: Center(child: show,)
);

好了流强,到這也差不多了痹届,你以為結束了,稍安勿躁打月,還有一點


3.增加運動曲線

可以用曲線補間來讓動畫的執(zhí)行不那么古板

image

3.1:代碼修改
class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  CurveTween curveTween;//運動曲線
  AnimConfig({this.duration, this.offset, this.mode,this.curveTween});
}

class _FlutterLayoutState extends State<FlutterLayout>
    with SingleTickerProviderStateMixin {

    var curveTween = widget.config.curveTween;
    animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {}
      });

3.2:MultiShower測試

Curves內置四十幾種曲線队腐,這里就隨便挑一些,你也可以用MultiShower自己玩一玩

var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var configs = <CurveTween>[
  CurveTween(curve: Curves.bounceIn),
  CurveTween(curve: Curves.bounceInOut),
  CurveTween(curve: Curves.bounceOut),
  CurveTween(curve: Curves.decelerate),
  CurveTween(curve: Curves.ease),
  CurveTween(curve: Curves.easeIn),
  CurveTween(curve: Curves.easeInBack),
  CurveTween(curve: Curves.easeInCirc),
  CurveTween(curve: Curves.easeInCubic),
  CurveTween(curve: Curves.easeInExpo),
  CurveTween(curve: Curves.easeInOut),
  CurveTween(curve: Curves.easeInOutBack),
  CurveTween(curve: Curves.easeOut),
  CurveTween(curve: Curves.easeOutBack),
  CurveTween(curve: Curves.linear),
  CurveTween(curve: Curves.linearToEaseOut),
];

var configsInfo = <String>[
  "bounceIn","bounceInOut","bounceOut","decelerate",
  "ease","easeIn","easeInBack","easeInCirc","easeInCubic",
  "easeInExpo","easeInOut","easeInOutBack",
  "easeOut","easeOutBack",linear","linearToEaseOut",
];


var show = MultiShower(configs, (config) =>
    FlutterLayout(child: child,
      config: AnimConfig(
          duration: 2000, offset: 45, mode: RockMode.lean, curveTween: config),
    ), width: 60, color: Colors.transparent,infos: configsInfo,);

3.3:動畫完成的監(jiān)聽

定義一個FinishCallback回調作為配置參數(shù),在animation.addStatusListener里回調

class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  CurveTween curveTween;//運動曲線
  FinishCallback onFinish;
  AnimConfig({this.duration, this.offset, this.mode,this.curveTween,this.onFinish});
}

typedef FinishCallback = void Function();

---->[_FlutterLayoutState]----
animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
  ..addStatusListener((s) {
    if (s == AnimationStatus.completed) {
      if(widget.config.onFinish!=null)
        widget.config.onFinish();
    }
  });

好了奏篙,到這里柴淘,本文完結散花∶赝ǎ看到這的为严,贊點起來。


結語

本文到此接近尾聲了肺稀,如果想快速嘗鮮Flutter第股,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅话原。
另外本人有一個Flutter微信交流群夕吻,歡迎小伙伴加入诲锹,共同探討Flutter的問題,本人微信號:zdl1994328涉馅,期待與你的交流與切磋归园。

本文所有源碼見github/flutter_journey

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市稚矿,隨后出現(xiàn)的幾起案子蔓倍,更是在濱河造成了極大的恐慌,老刑警劉巖盐捷,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偶翅,死亡現(xiàn)場離奇詭異,居然都是意外死亡碉渡,警方通過查閱死者的電腦和手機聚谁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滞诺,“玉大人形导,你說我怎么就攤上這事∠芭” “怎么了朵耕?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淋叶。 經(jīng)常有香客問我阎曹,道長,這世上最難降的妖魔是什么煞檩? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任处嫌,我火速辦了婚禮,結果婚禮上斟湃,老公的妹妹穿的比我還像新娘熏迹。我一直安慰自己,他們只是感情好凝赛,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布注暗。 她就那樣靜靜地躺著,像睡著了一般墓猎。 火紅的嫁衣襯著肌膚如雪捆昏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天陶衅,我揣著相機與錄音屡立,去河邊找鬼直晨。 笑死搀军,一個胖子當著我的面吹牛膨俐,可吹牛的內容都是我干的。 我是一名探鬼主播罩句,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼焚刺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了门烂?” 一聲冷哼從身側響起乳愉,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屯远,沒想到半個月后蔓姚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡慨丐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年坡脐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片房揭。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡备闲,死狀恐怖,靈堂內的尸體忽然破棺而出捅暴,到底是詐尸還是另有隱情恬砂,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布蓬痒,位于F島的核電站泻骤,受9級特大地震影響,放射性物質發(fā)生泄漏梧奢。R本人自食惡果不足惜瞪讼,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粹断。 院中可真熱鬧符欠,春花似錦、人聲如沸瓶埋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽养筒。三九已至曾撤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晕粪,已是汗流浹背挤悉。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留巫湘,地道東北人装悲。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓昏鹃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诀诊。 傳聞我的和親對象是個殘疾皇子洞渤,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容