笔记:Flutter动画记录

一、带动画刷新的小部件

比如AnimatedContainer,这一类小部件都以Animated开头,示例如下:

AnimatedContainer(
          duration: const Duration(seconds: 3),
          width: 300,
          height: 300,
          color: Colors.blue,
        )

将上面的颜色换成其他颜色然后热更新一下,就会有颜色过渡动画。这一类的动画小部件只对自己的属性起到动画效果,他不能控制自己的child也执行动画。
差值器:curve
默认是Curves.linear。他是水平匀速运动。Curves里面还有很多,可以都看看。做一个匀速下落盒子的动画,如下:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: AnimatedPadding(
          curve: Curves.linear,
          duration: Duration(
            seconds: 2
          ),
          padding: EdgeInsets.only(top: 0),
          child: Container(
            width: 300,
            height: 300,
            color: Colors.blue,
          ),
        ),
      ),
    );
  }

二、两种小部件来回切换动画

上面的动画提到了child切换时,没有动画,那么我们要做到这样的功能需要AnimatedSwitcher来实现。如下:

AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: CircularProgressIndicator(),),//Image.network("https://www.baidu.com/img/flexible/logo/pc/result.png")
          )

当吧AnimatedSwitcher的child从Center(child: CircularProgressIndicator(),)换成Image,就会有过渡动画。
值得一提的是,如果是Text只是换掉里面的文案,那么不会有动画。比如:

AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: Text("HI",style: TextStyle(fontSize: 41),),),
          )
//把文案换成hello。
AnimatedSwitcher(
            duration: Duration(seconds: 2),
            child: Center(child: Text("HELLO",style: TextStyle(fontSize: 41),),),
          )

为什么没有动画?因为只是改文案,flutter不会认为是换了小部件,所以AnimatedSwitcher也不会执行动画,那么要想执行动画就要让flutter觉得小部件改变了。就要用到key。
在Flutter中,当重新build时,会更新widget和element。
Flutter的UI是通过构建widget树来实现的,widget是不可变的,当需要更新UI时,会创建一个新的widget,并使用diff算法比较新旧widget的差异,然后更新element树。如果widget树中的某个widget的属性发生了变化,或者父级widget调用了setState方法,那么Flutter会重新build该widget及其子树。
在重新build时,Flutter会根据widget的差异更新element树。如果新旧widget的类型相同,Flutter会复用element,只更新相关的属性值。如果新旧widget的类型不同,则会销毁旧的element,并创建一个新的element。如果当类型相同,key不同,flutter也会重新构建widget。
所以把上面的代码改成下面的加个key就行:

AnimatedSwitcher(
            duration: const Duration(seconds: 2),
            child: Center(child: Text(key: UniqueKey(),"HI",style: const TextStyle(fontSize: 41),),),
          )
//把文案换成hello。
AnimatedSwitcher(
            duration: const Duration(seconds: 2),
            child: Center(child: Text(key: UniqueKey(),"HELLO",style: const TextStyle(fontSize: 41),),),
          )

当然AnimatedSwitcher还可以选择动画类型,用transitionBuilder属性即可。默认用的是fade也就是透明度变化效果。如下:

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return FadeTransition(opacity: animation,child: child,)
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

我们也能改成放大缩小动画或者选择,如下:改成放大缩小

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return ScaleTransition(scale: animation,child: child,)
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

那么如果我们既需要透明动画也需要放大缩小动画怎么办?那就套娃。如下:

AnimatedSwitcher(
            transitionBuilder: (child,animation){
              return ScaleTransition(scale: animation,
                child: FadeTransition(
                opacity: animation,
                  child: child,
              ),);
            },
            duration:  Duration(seconds: 2),
            child: Center(child: Text(key: ValueKey("Hi"),"Hi",style:  TextStyle(fontSize: 41),),),
          )

三、补间动画

和安卓一样,flutter也有补间动画,其实可以理解成,一个属性设置在一个返回变化,然后一变化就给小部件赋值,达到动画效果。比如下面的透明度动画。如下:

Scaffold(
      body: SafeArea(
        child: Center(
          child: TweenAnimationBuilder(
            duration: Duration(seconds: 2),
            builder: (BuildContext context,  value,  child) {
              return Opacity(
                opacity: value,
                child: Container(
                  width: 300,
                  height: 300,
                  color: Colors.blue,
                ),
              );
            },
            tween: Tween(begin: 1.0,end: 0.0),
          ),
        ),
      ),
    )

上面的代码可以实现AnimatedOpacity的动画效果,而且好掌握。就是代码长了些。有没有发现builder里面的child没有用到?这个是优化用的。还没到时机,暂时不记录。
补间动画经常和Transform一起连用。下面来一个按按钮让小部件来回放大缩小的动画。如下:

class Test1State extends State<Test1> {
  var _begin = true;
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: TweenAnimationBuilder(
            duration: Duration(seconds: 2),
            builder: (BuildContext context,  value,  child) {
              return Container(
                width: 300,
                height: 300,
                color: Colors.blue,
                child: Center(
                  child: Transform.scale(
                    scale: value,
                    child: Text("HI",style: TextStyle(fontSize: 50),),
                  ),
                ),
              );
            },
            tween: Tween(begin: 1.0,end: _begin ? 2.0 : 1.0),

          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        setState(() {
          _begin = !_begin;
        });
      },),
    );
  }
}

Transform还有旋转,平移等,都可以看看。

四、显示动画
显示动画带控制器,能更好的方便我们停止开始等。下面做一个不停旋转的图标动画,如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Container(
            width: 300,
            height: 300,
            color: Colors.blue,
            child: Center(
              child: RotationTransition(
                turns: _controller!!,
                child: Icon(Icons.refresh,color: Colors.black, size: 50,),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        if(_start){
          _controller?.stop();
        }else{
          _controller?.repeat();
        }
        setState(() {
          _start = !_start;
        });
      },),
    );
  }
}

AnimationController必传的一个参数是vsync,这是个屏幕刷新时通知其他页面的回调,我们这里with了一个SingleTickerProviderStateMixin,单个的屏幕刷新回调。因为我们就一个动画需要。这样屏幕每一次刷新都会通知我们的AnimationController,然后AnimationController根据我们传的值和时间,计算当前需要的旋转角度。repeat代表0到1重复执行。forward是0到1执行一次。那我要是指定3到5执行呢?那就设置区间

//设置lowerBound和upperBound
@override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 3.0,upperBound: 5.0);
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }
........
//forward用执行一次
floatingActionButton: FloatingActionButton(onPressed: (){
        if(_start){
          _controller?.stop();
        }else{
          _controller?.forward();
        }
        setState(() {
          _start = !_start;
        });
},),

除了RotationTransition还有FadeTransition和ScaleTransition、SlideTransition等。下面用ScaleTransition做一个放大缩小发大缩小重复循环的动画。

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: ScaleTransition(
            scale: _controller!!,
            child: Container(
              width: 300,
              height: 300,
              color: Colors.blue,
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        _controller?.repeat(reverse: true);
      },),
    );
  }
}

值得一提的是reverse,如果没有设置,那么动画只会放大,不会从大变小。设置了reverse就会有放大缩小再放大再缩小的动画。

再来一个来回移动的动画:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin{
  AnimationController? _controller = null;
  bool _start = false;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
    _controller?.addListener(() {
      print("value = ${_controller?.value}");
    });
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SlideTransition(
            position: _controller!.drive(Tween(begin: Offset(0,0),end: Offset(1,0))),
            child: Container(
              width: 300,
              height: 300,
              color: Colors.blue,
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(onPressed: (){
        _controller?.repeat(reverse: true);
      },),
    );
  }
}

SlideTransition需要position,而position是一系列的Offset。_controller.drive加上Tween就能生成一系列Offset。当然Tween还有另外的写法。如下:

position: Tween(begin: Offset(0,0),end: Offset(1,0)).animate(_controller!),

这样写有好处,他可以叠加其他的Tween。如下:

position: Tween(begin: Offset(0, 0), end: Offset(1, 0))!
                .chain(CurveTween(curve: Curves.elasticInOut))
                .animate(_controller!),

这样我们就能做一个间隔动画,比如一个小部件执行完动画再执行其他小部件,一个个得来。如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  AnimationController? _controller = null;
  bool _start = false;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SliverBox(_controller!,Colors.blue[100]!,0.0,0.2),
              SliverBox(_controller!,Colors.blue[200]!,0.2,0.4),
              SliverBox(_controller!,Colors.blue[300]!,0.4,0.6),
              SliverBox(_controller!,Colors.blue[400]!,0.6,0.8),
              SliverBox(_controller!,Colors.blue[500]!,0.8,1.0),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat(reverse: true);
        },
      ),
    );
  }
}

class SliverBox extends StatelessWidget {
  AnimationController _controller;
  Color color;
  double startInterval;
  double endInterval;
  SliverBox(this._controller,this.color,this.startInterval,this.endInterval,{Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: Tween(begin: Offset(0, 0), end: Offset(0.1, 0))
          .chain(CurveTween(curve: Interval(startInterval, endInterval,curve: Curves.bounceOut)))
          .animate(_controller!),
      child: Container(
        width: 280,
        height: 100,
        color: color,
      ),
    );
  }
}

五、方便扩展的AnimatedBuilder

我们做一个透明度加高度变化的动画,如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  AnimationController? _controller = null;
  bool _start = false;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
  }

  @override
  void dispose() {
    super.dispose();
    _controller?.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: AnimatedBuilder(
            builder: (BuildContext context, Widget? child) {
              return Opacity(
                opacity: _controller!.value,
                child: Container(
                  width: 200,
                  height: 100 + (100 * _controller!.value),
                  color: Colors.blue,
                  child: Center(
                    child: Text(
                      "HI",
                      style: TextStyle(fontSize: 40),
                    ),
                  ),
                ),
              );
            }, animation: _controller!,
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat();
        },
      ),
    );
  }
}

可以看到我们的透明度增加,高度也随之增加。那么上次我们提到了AnimatedBuilder里面的builder有一个child参数,他是干嘛的?其实他是flutter为了增加效率做的参数,你看啊,我们这代码里面,Opacity和Container都有变化,一个跟着动画改变透明度,提个改变高度。唯独中间的文字没有变。所以我们可以把这个文字放到AnimatedBuilder的child中,这样的话,每一帧动画回调的builder都会把这个child返回,我们有直接复用即可。可以看出flutter对性能优化的注重。优化代码如下:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: AnimatedBuilder(
            builder: (BuildContext context, Widget? child) {
              return Opacity(
                opacity: _controller!.value,
                child: Container(
                  width: 200,
                  height: 100 + (100 * _controller!.value),
                  color: Colors.blue,
                  child: child,
                ),
              );
            },
            animation: _controller!,
            child: Center(   //这个child将会在build回调中复用。
              child: Text(
                "HI",
                style: TextStyle(fontSize: 40),
              ),
            ),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller?.repeat();
        },
      ),
    );
  }
}

六、hero动画

android有一个两个页面的跳转动画,那flutter也有,它就是hero动画。如下:

class HeroAnimationRoute extends StatelessWidget {
  const HeroAnimationRoute({Key? key}) : super(key: key);
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        padding: EdgeInsets.only(top: 100),
        alignment: Alignment.topCenter,
        child: Column(
          children: <Widget>[
            InkWell(
              child: Hero(
                tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
                child: ClipOval(
                  child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31',
                    width: 100.0,
                  ),
                ),
              ),
              onTap: () {
                //打开B路由
                Navigator.push(context, PageRouteBuilder(
                  pageBuilder: (
                      BuildContext context,
                      animation,
                      secondaryAnimation,
                      ) {
                    return FadeTransition(
                      opacity: animation,
                      child: Scaffold(
                        appBar: AppBar(
                          title: Text("原图"),
                        ),
                        body: HeroAnimationRouteB(),
                      ),
                    );
                  },
                ));
              },
            ),
            Padding(
              padding: const EdgeInsets.only(top: 8.0),
              child: Text("点击头像"),
            )
          ],
        ),
      ),
    );
  }
}
 
 
class HeroAnimationRouteB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
        tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
        child: Image.network('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F511%2F101611154647%2F111016154647-10-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1644588215&t=9a40338757751be9d2684f0d3c80ae31'),
      ),
    );
  }
}

Hero中的tag必须保持一致。这样,跳转页面就不会看起来生硬了。

七、Paint与动画结合

像android一样,flutter也有自己的画布、画笔。如果要在flutter中画画,需要用到

Container(
    width: double.infinity,
    height: double.infinity,
    child: CustomPaint(
        painter: MyPaint(),
    )
)
......
class MyPaint extends CustomPainter{
  MyPaint(this.whitePaint,this._snows);
  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(Offset(0, 0), 50, Paint());
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

}

这样就能在屏幕上画一个圆。
那么我们把这个圆当做是雪花,再在底部画一个雪人,那么结合anmation的事实刷新,就能让雪花动起来。如下:

class Test1State extends State<Test1> with SingleTickerProviderStateMixin {
  late AnimationController _controller ;
  late Paint whitePaint;//雪人和雪花都是白色,所以定义白色画笔
  late List<Snow> _snows = List.generate(100, (index) => Snow());//一共一百个雪花

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 4));
    whitePaint = Paint();
    whitePaint.color = Colors.white;
  }

  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(//宽高都最大
        width: double.infinity,
        height: double.infinity,
        decoration: const BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.blue,Colors.white],
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
            )
        ),
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            return CustomPaint(//设置自定义画布
              painter: MyPaint(whitePaint,_snows),//传入画笔和雪花到自定义画布
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _controller.repeat();//点击循环播放动画
        },
      ),
    );
  }
}

class MyPaint extends CustomPainter{
  Paint whitePaint;
  List<Snow> _snows;

  MyPaint(this.whitePaint,this._snows);

  @override
  void paint(Canvas canvas, Size size) {
    print("width = ${size.width}");
    canvas.drawCircle(Offset(size.width/2, size.height - 200), 50, whitePaint);//雪人头
    canvas.drawOval(Rect.fromCenter(center: Offset(size.width/2,size.height - 75), width: 140, height: 200), whitePaint);//雪人身体
    _snows.forEach((element) {//循环画出雪花
      canvas.drawCircle(Offset(element.x, element.y), element.radio, whitePaint);
      element.start();//开始下落,并且下落完成,重新设置随机数。
    });
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;//这是判断paint方法要不要执行,如果返回false,paint方法不会执行,返回true就会,我们这里必须让他刷新。
  }

}

class Snow {
  double x = Random().nextDouble() * 360;//横坐标
  double y = Random().nextDouble() * 720;//纵坐标
  double radio = Random().nextDouble() * 2 + 4;// 最小3最大5
  double speed = Random().nextDouble() * 2 + 2;// 最小2最大4

  start(){
    y = y + speed;
    if(y > 720){//如果雪花落地了,那么重新生成位置、大小、速度等
      x = Random().nextDouble() * 360;
      y = 0;
      radio = Random().nextDouble() * 2 + 4;// 最小3最大5
      speed = Random().nextDouble() * 2 + 2;// 最小2最大4
    }

  }
}

八、第三方动画框架
和安卓一样,flutter也支持lottie还支持Flare等。可以去插件库看看。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容