Flutter第三章(Paddiing,Row,Column,Expanded,Stack,AspectRatio,Card,Wrap)

版权声明:本文为作者原创书籍。转载请注明作者和出处,未经授权,严禁私自转载,侵权必究!!!

情感语录:侠之大者为国为民,如果做不了侠者也别轻易做小人,多一些包容,其实很多事没你想像地那么糟糕!

哈喽,大家好,欢迎来到本章节,上一章节我们讲了Image组件ListView组件GridView组件 还记得吗?知识点回顾 戳这里 Fultter基础第二章在前两章节的捯饬下我相信你已经爱上了Flutter或者逐渐开始喜欢了上了,学习都是枯燥的,贵在坚持,加油!!!!

本章简要:

1、内距组件Paddiing
2、水平布局组件Row
3、垂直布局组件Column
4、权重布局组件Expanded
5、层叠 ( 帧 )组件Stack
6、纵横比组件AspectRatio
7、卡片组件Card
8、包裹组件Wrap

一、Paddiing组件

在 Android 原生控件中都有 padding 属性,但是 Flutter 中很多 Widget 是没有 padding 属性。这个时候我们可以用 Padding 组件处理容器与子元素之间的间距。

属性                 说明

padding             padding值, EdgeInsetss 设置填充的值

child               子组件

二、Row组件

Row水平布局组件其实理解也很简单,你可以看做是Android原生中LinearLayout布局控件属性为水平方向,或者说是一个横向的ListView控件,说是横向的ListView控件到更为贴切,因为在Flutter中Row的子元素接收的是一个List<Widget>,这也使得Row控件变得更加灵活易用,开发中RowColumn使用频率是很高的。

属性                     说明

mainAxisAlignment        主轴的排序方式

crossAxisAlignment       次轴的排序方式

children                 组件子元素

三、Column组件

Column组件的用法其实和Row是一模一样的,只是一个是控制水平方向的,一个是垂直方向,会了Row组件的使用,Column自然也就会使用了。

属性                     说明

mainAxisAlignment        主轴的排序方式

crossAxisAlignment       次轴的排序方式

children                 组件子元素

四、Expanded组件

Expanded 可以用在 Row 和 Column 布局中,其的目的就是控制子元素之间的权重比,这和在原生中LinearLayout的权重使用是一个道理。

属性                说明

flex                元素站整个父 Row /Column 的比例

child               子元素

这上面的4个组件的属性并未全部列出,只是给出了常用的属性,在学习的同时,还是建议结合源码查看,只有对源码熟悉了学的才快嘛,通过上面的简单介绍,也对这4个组件有了简单的了解,下面我现将这学到的这几个组件进行下练习,毕竟光说不练那都是假把式嘛,还是要实操的O(∩_∩)O

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
          appBar: AppBar(
            title: Text("呆萌"),
          ),
          body: ViewLayout()),
    );
  }
}

class ViewLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Text(
          "这是Paddiing的简单应用",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),

        SizedBox(
          height: 10,
        ),
        //用容器包装下 方便观察
        Container(
          color: Colors.deepPurple,
          height: 60,
          child: Padding(
            //可以通过fromLTRB分别设置左上右下的内距,也可以用all统一设置
            padding: EdgeInsets.fromLTRB(0, 10, 0, 10),
            child: Image.network(
                'http://qukufile2.qianqian.com/data2/pic/246708144/246708144.jpg',
                fit: BoxFit.fitHeight),
          ),
        ),
        SizedBox(
          height: 10,
        ),
        Text(
          "这是Row应用(横向布局)",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 10,
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          //spaceEvenly 主轴的排列方式最为常见
          crossAxisAlignment: CrossAxisAlignment.start,
          //用的比较少
          children: <Widget>[
            IconWidget(Icons.search, color: Colors.blue),
            IconWidget(Icons.home, color: Colors.orange),
            IconWidget(Icons.select_all, color: Colors.red),
          ],
        ),

        SizedBox(
          height: 5,
        ),
        Text(
          "这是Column应用(垂直布局)",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 5,
        ),
        Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          //spaceEvenly 主轴的排列方式最为常见
          crossAxisAlignment: CrossAxisAlignment.center,
          //用的比较少
          children: <Widget>[
            IconWidget(Icons.search, color: Colors.blue),
            SizedBox(
              height: 5,
            ),
            IconWidget(Icons.home, color: Colors.orange),
            SizedBox(
              height: 5,
            ),
            IconWidget(Icons.select_all, color: Colors.red),
          ],
        ),
        SizedBox(
          height: 5,
        ),
        Text(
          "这是Expanded应用权重(1:2:1)",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 5,
        ),

        Row(
          children: <Widget>[
            Expanded(
                flex: 1, child: IconWidget(Icons.search, color: Colors.blue)),
            Expanded(
              flex: 2,
              child: IconWidget(Icons.home, color: Colors.orange),
            ),
            Expanded(
              flex: 1,
              child: IconWidget(Icons.select_all, color: Colors.red),
            ),
          ],
        ),
        SizedBox(
          height: 10,
        ),
        Text(
          "这是Expanded应用权重(1:2)",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 10,
        ),

        Row(
          children: <Widget>[
            Expanded(
                flex: 1, child: IconWidget(Icons.search, color: Colors.blue)),
            Expanded(
              flex: 2,
              child: IconWidget(Icons.home, color: Colors.orange),
            ),
          ],
        ),
      ],
    );
  }
}

//封装一个简单的Icon 方便调度
class IconWidget extends StatelessWidget {
  double size = 32.0;
  Color color = Colors.red;
  IconData icon;

  IconWidget(this.icon, {this.color, this.size});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 50.0,
      width: 50.0,
      color: this.color,
      child:
          Center(child: Icon(this.icon, size: this.size, color: Colors.white)),
    );
  }
}

上面代码的布局效果如下:


实例1.png

读者在练习的时候尽量多手动写下,多尝试,比如我这里并没去细讲 ColumnRow中的MainAxisAlignment的属性以及效果,希望你在练习的时候能自己尝试感受下。

五、Stack组件

Stack 表示堆的意思,就是在该组件下放入的组件,会一层一层的累积,这样说理解起来可能有点抽象。想像一下,这就好比修房子,Stack就是房子的地基,而新建的第一层盖在地基上,第二层则盖在第一层上。有过Android原生开发的同学应该很好能理解,它就相当于原生中的FrameLayout控件。在Flutter中我们可以用 Stack 或者 Stack 结合 Align 或者 Stack 结合 Positiond 来实现页面的定位布局,Stack是定位发生的容器,只有在 Stack中,绝对定位的 Widget 才会生效

属性                说明

alignment           配置所有子元素的显示位置

children            子组件

其中关键的属性就是 children,除了几个样式控制的参数之外,通过 children 可以传入一个List<Widget>,可以使列表用在 Stack 中进行绝对定位。

5.1 Align组件

Stack 组件中结合 Align 组件可以控制每个子元素的显示位置,既实现绝对定位

属性                说明

alignment           配置所有子元素的显示位置

child               子组件

无论是单独使用Stack或者结合Align 使用,其中对元素位置控制都是通过 alignment来指定的,下面我们来结合它的源码分析下:

///
/// The [x] and [y] arguments must not be null.
const Alignment(this.x, this.y)
: assert(x != null),
assert(y != null);

/// The distance fraction in the horizontal direction.
///
/// A value of -1.0 corresponds to the leftmost edge. A value of 1.0
/// corresponds to the rightmost edge. Values are not limited to that range;
/// values less than -1.0 represent positions to the left of the left edge,
/// and values greater than 1.0 represent positions to the right of the right
/// edge.
final double x;

/// The distance fraction in the vertical direction.
///
/// A value of -1.0 corresponds to the topmost edge. A value of 1.0
/// corresponds to the bottommost edge. Values are not limited to that range;
/// values less than -1.0 represent positions above the top, and values
/// greater than 1.0 represent positions below the bottom.
final double y;

@override
double get _x => x;

@override
double get _start => 0.0;

@override
double get _y => y;

/// The top left corner.
static const Alignment topLeft = Alignment(-1.0, -1.0);

/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0, -1.0);

/// The top right corner.
static const Alignment topRight = Alignment(1.0, -1.0);

/// The center point along the left edge.
static const Alignment centerLeft = Alignment(-1.0, 0.0);

/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0, 0.0);

/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0, 0.0);

/// The bottom left corner.
static const Alignment bottomLeft = Alignment(-1.0, 1.0);

/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0, 1.0);

/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0, 1.0);

这段源码一目了然,比较简单,首先Alignment是一个没有无参构造函数的类,创建该对象时必须传入非null 且是double类型的xy值,否则报错。接下来是对 xy值的描述,我们可以简单的理解为x(水平方向)的位置在当前容器的可见区域从左到右是(-1到1之间),超出该值范围,既超出了该容器的边,既不可以见,y(垂直方向)同理,只是从上到下而已。最后源码中帮我列出了9种常用的位置常量方便我们使用。

5.2 Positioned组件

Stack 组件中结合 Positioned 组件也可以控制每个子元素的显示位置,在绝对定位中Positioned可能更适合我们的开发习惯:

属性                说明

top                 子元素距离顶部的距离

bottom              子元素距离底部的距离

left                子元素距离左侧距离

right               子元素距离右侧距离

child               子组件

width               子元素的宽度

height              子元素的高度

下面我们从源码中去看下注释文档。

/// Creates a widget that controls where a child of a [Stack] is positioned.
///
/// Only two out of the three horizontal values ([left], [right],
/// [width]), and only two out of the three vertical values ([top],
/// [bottom], [height]), can be set. In each case, at least one of
/// the three must be null.
///
/// See also:
///
///  * [Positioned.directional], which specifies the widget's horizontal
///    position using `start` and `end` rather than `left` and `right`.
///  * [PositionedDirectional], which is similar to [Positioned.directional]
///    but adapts to the ambient [Directionality].
const Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
@required Widget child,
}) : assert(left == null || right == null || width == null),
assert(top == null || bottom == null || height == null),
super(key: key, child: child);

大致可以理解是在使用width的时候不能同时使用leftright,既不能同时出现这三个属性,否则报错,如果在使用width的同时又使用了leftright,既分别表示子元素从左边或者右边开始对齐移动leftright个单位值。同理heighttopbottom三者的使用亦是如此。

六、Card组件

Card 是卡片组件块,内容可以由大多数类型的 Widget 构成,Card 具有圆角和阴影,这让它看起来有立体感。

属性                     说明

margin                   外边距

child                    子组件

Shape                    Card 的阴影效果,默认的阴影效果为圆角的长方形边。

七、AspectRatio组件

AspectRatio 的作用是根据设置调整子元素 child 的宽高比, 它首先会在布局限制条件允许的范围内尽可能的扩展,widget 的高度是由宽度和比率决定的,类似于 BoxFit 中的 contain,按照固定比率去尽量占满区域。如果在满足所有限制条件过后无法找到一个可行的尺寸,AspectRatio 最终将会去优先适应布局限制条件,而忽略所设置的比率。

属性                           说明

aspectRatio                    宽高比,最终可能不会根据这个值去布局,
                               具体则要看综合因素,外层是否允许按照这
                               种比率进行布局,这只是一个参考值
                       
child                          子组件

该组件在理解上可能比较难,下面我们来看一个实例:

     new Container(
         height: 100,
         child: AspectRatio(
         aspectRatio: 3.0 / 1.0,
         child: Container(
          color: Colors.red,
      ),
    ))

示例代码是定义了一个高度为100的区域,内部AspectRatio比率设置为3,最终AspectRatio的宽是300,高是100的一个区域。

八、Wrap组件

开篇第一个例子讲了Column 或者 Row + Expanded Widget 来实现 flex 布局,但是 Row + Expanded 的实现方式有个致命问题是无法自动换行,而真正的 flex 布局是有一个 wrap 属性的,对于一行无法铺开的场景非常实用。Wrap组件可以实现流布局,单行的 Wrap 跟 Row 表现几乎一致,单列的 Wrap 则跟 Row 表现几乎一致。但 Row 与 Column 都是单行单列的,Wrap 则突破了这个限制,mainAxis 上空间不足时,则向 crossAxis 上去扩展显示。

  /// Creates a wrap layout.
  ///
  /// By default, the wrap layout is horizontal and both the children and the
  /// runs are aligned to the start.
  ///
  /// The [textDirection] argument defaults to the ambient [Directionality], if
  /// any. If there is no ambient directionality, and a text direction is going
  /// to be necessary to decide which direction to lay the children in or to
  /// disambiguate `start` or `end` values for the main or cross axis
  /// directions, the [textDirection] must not be null.
  Wrap({
    Key key,
    this.direction = Axis.horizontal,
    this.alignment = WrapAlignment.start,
    this.spacing = 0.0,
    this.runAlignment = WrapAlignment.start,
    this.runSpacing = 0.0,
    this.crossAxisAlignment = WrapCrossAlignment.start,
    this.textDirection,
    this.verticalDirection = VerticalDirection.down,
    List<Widget> children = const <Widget>[],
  }) : super(key: key, children: children);

常用属性说明:

属性                 说明

direction            主轴的方向,默认水平

alignment            主轴的对其方式

spacing              主轴方向上的间距

textDirection        文本方向

verticalDirection    定义了children 摆放顺序,默认是 down,见Flex 相关属性介绍。

runAlignment         run 的对齐方式, 可以理解为新的行或者列,如果是水平方向布局的话,
                     run 可以理解为新的一行
                     
runSpacing           run 的间距

又讲了4个组件,加上开篇讲的4个组件本章节你已经掌握了8个常用的组件了,下面我们还是先对刚刚学到的4个组件来做个小的练习,加深下印象。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
          appBar: AppBar(
            title: Text("呆萌"),
          ),
          body: ViewLayout()),
    );
  }
}

class ViewLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Text(
          "这是Stack的简单应用",
          style: TextStyle(fontSize: 18, color: Colors.green),
        ),

        SizedBox(
          height: 5,
        ),
        //用容器包装下设置背景色 方便观察
        Container(
          color: Colors.deepPurple,
          height: 60,
          child: Stack(
            alignment: Alignment(-1, -1), // 将子元素定位在左上,或者使用常量 topLeft
            children: <Widget>[
              Container(
                width: 100,
                height: 40,
                color: Colors.red,
              ),
              Text('你说什么?', style: TextStyle(fontSize: 16, color: Colors.white))
            ],
          ),
        ),
        SizedBox(
          height: 5,
        ),
        Text(
          "这是Stack结合Align的应用",
          style: TextStyle(fontSize: 18, color: Colors.green),
        ),
        SizedBox(
          height: 5,
        ),
        Container(
          height: 100,
          width: 300,
          color: Colors.red,
          child: Stack(
            children: <Widget>[
              Align(
                alignment: Alignment(1, 0), //定位最右边,垂直居中
                child: Icon(Icons.home, size: 30, color: Colors.white),
              ),
              Align(
                alignment: Alignment.center, //定位在容器的中心位置
                child: Icon(Icons.search, size: 30, color: Colors.white),
              ),
              Align(
                alignment: Alignment.bottomLeft, //定位在容器的左下
                child: Icon(Icons.ac_unit, size: 30, color: Colors.white),
              )
            ],
          ),
        ),
        SizedBox(
          height: 5,
        ),
        Text(
          "这是Stack结合Positioned的应用",
          style: TextStyle(fontSize: 18, color: Colors.green),
        ),
        SizedBox(
          height: 5,
        ),

        //用容器包装下设置背景色 方便观察
        Container(
          color: Colors.deepPurple,
          height: 60,
          child: Stack(
            children: <Widget>[
              Positioned(
                right: 10, // 让子元素从右边开始对齐
                width: 120, //指定宽度为120个单位
                child: Icon(Icons.access_alarm, size: 30, color: Colors.white),
              ),
              Positioned(
                bottom: 0, // 让子元素从底部开始对齐
                left: 100,
                height: 50,
                child: Icon(Icons.memory, size: 30, color: Colors.white),
              ),
              Positioned(
                left: 5, // 让子元素从左边开始对齐
                width: 150,
                child: Text('你很帅,你造吗?',
                    style: TextStyle(fontSize: 16, color: Colors.white)),
              )
            ],
          ),
        ),
        SizedBox(
          height: 5,
        ),
        Text(
          "这是Card应用",
          style: TextStyle(fontSize: 18, color: Colors.green),
        ),
        SizedBox(
          height: 5,
        ),

        Card(
          margin: EdgeInsets.all(5),
          color: Colors.cyan,
          elevation: 10,
          //10个单位的阴影
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(14.0))),
          //设置圆角
          child: Column(
            children: <Widget>[
              ListTile(
                title: Text("Mr.Z", style: TextStyle(fontSize: 18)),
                subtitle: Text("工程师", style: TextStyle(fontSize: 14)),
              ),
              ListTile(
                title: Text("电话:xxxxx"),
              ),
            ],
          ),
        ),

        SizedBox(
          height: 5,
        ),
        Text(
          "这是AspectRatio应用",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 5,
        ),

        Container(
            height: 100,
            child: AspectRatio(
              aspectRatio: 3.0 / 1.0,
              child: Container(
                color: Colors.red,
              ),
            )),
        SizedBox(
          height: 5,
        ),
        Text(
          "这是Wrap应用",
          style: TextStyle(fontSize: 18, color: Colors.deepOrangeAccent),
        ),
        SizedBox(
          height: 5,
        ),
        Wrap(
           spacing: 10,
           runSpacing: 10,
           direction: Axis.horizontal,
           alignment:WrapAlignment.spaceEvenly,
           children: <Widget>[
            ButtonItem("盗墓笔记"),
            ButtonItem("鬼吹灯"),
            ButtonItem("桃花怪大战菊花侠"),
            ButtonItem("无主之城"),
            ButtonItem("琅琊榜"),
            ButtonItem("仙剑奇侠传"),
            ButtonItem("风云决"),
            ButtonItem("哪吒"),
            ButtonItem("玄门大师"),
            ButtonItem("废材兄弟"),
            ButtonItem("爱情公寓"),
          ],
        )
      ],
    );
  }
}

//封装一个简单的Button 方便调度
class ButtonItem extends StatelessWidget {
  final String text;

  const ButtonItem(this.text, {Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
        child: Text(this.text,style: TextStyle(color: Colors.red),),
        textColor: Theme.of(context).cardColor,
        onPressed: () {});
  }
}

上述练习代码效果大致如下:


实例2.gif

本章实战:

由于本章讲述的组件相对较多但并不复杂,本章节实战环节跳过,但希望你在学习的时候多结合源码手动练习下,虽然不难但本章节的东西在开发中是使用非常频繁的。

实例源码地址: https://github.com/zhengzaihong/flutter_learn

好了本章节就此结束,又到了说再见的时候了,如果你喜欢请留下你的小红星,你们的支持才是创作的动力,如有错误,请热心的你留言指正, 谢谢大家观看,下章再会 O(∩_∩)O

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

推荐阅读更多精彩内容