四、容器组件
定义
看了这么久,我div呢?布局控件相对使用比较麻烦,容器组件感觉更贴近一些。
容器类Widget和布局类Widget都作用于其子Widget,不同的是:
- 布局类Widget一般都需要接收一个widget数组(children),他们直接或间接继承自(或包含)MultiChildRenderObjectWidget ;而容器类Widget一般只需要接收一个子Widget(child),他们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
- 布局类Widget是按照一定的排列方式来对其子Widget进行排列;而容器类Widget一般只是包装其子Widget,对其添加一些修饰(补白或背景色等)、变换(旋转或剪裁等)、或限制(大小等)。
容器(Container)
它就是我们的div吧...
它结合了绘制、定位和大小调整为一体。可以用它来设置背景颜色、形状、边框等。如果没有子组件,Container会自动填充父组件;如果设置了子组件,Container可以根据子组件的大小调整自身的大小。
Container({
  this.alignment,
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰
  Decoration foregroundDecoration, //前景装饰
  double width,//容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件
  this.margin,//容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
  ...
})
注意:
- 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定;如果它们同时存在时,width、height优先。实际上Container内部会根据width、height来生成一个constraints。
- color和- decoration是互斥的,如果同时设置它们则会报错!实际上,当指定- color时,- Container内会自动创建一个- decoration。
实现一个圆角带阴影的卡片示例:

Container(
  margin: const EdgeInsets.only(top: 50.0, left: 120.0),
  constraints: const BoxConstraints.tightFor(width: 200.0, height: 150.0), //卡片大小
  // 前景装饰
  // foregroundDecoration: BoxDecoration( // 前景装饰会把背景装饰和子组件都遮住
  //   color: Colors.blue,
  // ),
  //背景装饰
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(10.0), //圆角
    gradient: const RadialGradient( // 背景径向渐变
      colors: [Colors.red, Colors.orange],
      center: Alignment.topLeft,
      radius: .98, // 半径
    ),
    boxShadow: const [ // 卡片阴影
      BoxShadow(
        color: Colors.black54,
        offset: Offset(2.0, 2.0),
        blurRadius: 4.0,
      )
    ],
  ),
  transform: Matrix4.rotationZ(.2), //卡片倾斜变换
  alignment: Alignment.center, //卡片内文字居中
  child: const Text(
    //卡片文字
    "5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),
  ),
)
示例
只有左上右上有圆角
Container(
  width: 200,
  height: 100,
  decoration: BoxDecoration(
    color: Color(0xff0075e0),
    borderRadius: BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)),
  ),
  // color: Color(0xff0075e0),
)
渐变背景色
Container(
  width: double.infinity,
  height: 30,
  // color: Colors.brown, // 如果有decoration属性,则不可直接设置Color属性,需移至decoration
  decoration: const BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.centerLeft,
      end: Alignment.centerRight,
      colors: [Color(0xfffbe8a3),Color(0xfffbe8a3), Color(0xfffef3d6),], // 渐变色
      stops: [0.0, 0.7, 1.0], // 控制颜色的过渡点
    ),
    borderRadius: BorderRadius.all(Radius.circular(1)), // 圆角
  ),
  child: Row(
  children: [
    const SizedBox(width: 10,),
    Image.asset(
      'images/home/laba.png',
      width: 18, 
      height: 18, 
    ),
    const SizedBox(width: 10,),
    Expanded(child: CircularListMarquee()),
  ]
  ),
)
填充(Padding)
和css一样,内边距。Padding组件用于给其子组件添加填充空间。它只接受一个子组件,并且可以指定上下左右的填充量。
Padding(
  padding: EdgeInsets.all(20.0),
  child: Text('Hello, Flutter!'),
)
EdgeInsets可以设置空间的内边距:
- fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充。注意与css的- top right bottom left顺序不同
- all(double value): 所有方向均使用相同数值的填充。
- only({left, top, right ,bottom }):可以设置具体某个方向的填充(可以同时指定多个方向)。
- symmetric({ vertical, horizontal }):用于设置对称方向的填充,- vertical指- top和- bottom,- horizontal指- left和- right。
样式(DecoratedBox)
可以在其子 widget 绘制前或绘制后绘制一个装饰(Decoration)。这个装饰可以包括背景色、边框、边距、圆角、渐变、背景图像等。DecoratedBox 对于添加简单的装饰效果非常有用,而无需创建一个完整的 Container widget,这使得布局更加轻量级。
const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget? child
})
主要属性:
- decoration:这是- DecoratedBox的核心属性,类型为- Decoration。这个属性定义了如何绘制装饰,比如颜色、形状、阴影等。- BoxDecoration是- Decoration的一个常用子类,它提供了设置背景色、图片、边框、圆角等的能力。
- position:这个属性决定装饰是绘制在子 widget 的背景之下还是前景之上,取值为- DecorationPosition.background或- DecorationPosition.foreground。
- child:- DecoratedBox的子 widget,它位于装饰之上或之下,取决于- position的值。
BoxDecoration
BoxDecoration({
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})
| 属性 | 类型 | 描述 | 
|---|---|---|
| color | Color | 背景颜色。注意,如果你同时使用了 image,color就会作为图片的背景色。 | 
| image | DecorationImage | 背景图像。可以设置图片的填充方式、对齐方式等。 | 
| border | Border | 边框。可以单独设置每一边的样式,如颜色、宽度。 | 
| borderRadius | BorderRadius | 边框圆角。可以分别设置每个角的圆角大小。 | 
| boxShadow | List<BoxShadow> | 阴影列表。可以设置多个阴影,每个阴影可以有不同的颜色、偏移、模糊程度等。 | 
| gradient | Gradient | 渐变。可以设置线性渐变( LinearGradient)、径向渐变(RadialGradient)或扫描式渐变(SweepGradient)。 | 
| shape | BoxShape | 形状。可以是 BoxShape.rectangle(矩形,默认值)或BoxShape.circle(圆形)。 | 
| backgroundBlendMode | BlendMode | 背景混合模式。用于与背景色混合,创建不同的视觉效果。 | 
示例:
DecoratedBox(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      // 线性渐变
      colors: [Colors.red, Colors.orange],
    ),
    borderRadius: BorderRadius.circular(10.0), // 圆角
    boxShadow: [
      // 阴影
      BoxShadow(
        color: Colors.black54,
        offset: Offset(2.0, 2.0),
        blurRadius: 4.0,
      )
    ],
  ),
  child: Padding(
    padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
    child: Text('Login', style: TextStyle(color: Colors.white)),
  ),
)

变换(Transform)
类似css中的transform。 Transform控件可以对widget进行旋转、缩放、平移和倾斜等变换,而不改变widget本身的布局属性(如大小和位置)。Transform直接作用于绘制阶段,因此不会影响布局,这意味着即使变换后的widget视觉上超出了原始布局空间,也不会影响其他widget的布局。
Container(
  color: Colors.black,
  child: Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
)
主要属性:
- transform:这是- Transformwidget 的核心属性,它接受一个- Matrix4对象,定义了如何变换widget。- Matrix4是一个4x4矩阵,用于描述3D变换。
- alignment:定义变换的原点,默认是中心点- Alignment.center。变换(如旋转和缩放)是相对于这个点进行的。
- origin:一个- Offset类型的点,也可以用来定义变换的原点。如果同时指定了- origin和- alignment,则- origin优先。
- child:要变换的子widget。
基本示例:
DecoratedBox( // 旋转
  decoration:BoxDecoration(color: Colors.blue),
  child: Transform.rotate(
    //旋转90度
    angle:math.pi/2 ,
    child: Text("Hello world"),
  ),
),
SizedBox(height: 50,),
DecoratedBox( // 缩放
  decoration:BoxDecoration(color: Colors.green),
  child: Transform.scale(
    scale: 1.5, //放大到1.5倍
    child: Text("Hello world")
  )
),
SizedBox(height: 50,),
DecoratedBox( // 平移
  decoration:BoxDecoration(color: Colors.red),
  //默认原点为左上角,左移20像素,向上平移5像素  
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
),
SizedBox(height: 80,),
Container( // 斜切
  color: Colors.black,
  child: Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.orange,
      child: const Text('Apartment for rent!'),
    ),
  ),
)

RotatedBox
RotatedBox和Transform.rotate功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox的变换是在layout阶段,会影响在子组件的位置和大小
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  //默认原点为左上角,左移20像素,向上平移5像素  
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  //将Transform.rotate换成RotatedBox  
  child: RotatedBox(
    quarterTurns: 1, //旋转90度(1/4圈)
    child: Text("Hello world"),
  ),
),
 由于
由于RotatedBox是作用于layout阶段,所以子组件会旋转90度(而不只是绘制的内容),decoration会作用到子组件所占用的实际空间上,所以最终就是上图的效果
思考
使用Transform对其子组件先进行平移然后再旋转和先旋转再平移,两者最终的效果一样吗?
在使用 Transform 对其子组件进行变换时,先进行平移然后再旋转与先旋转再平移的效果一般是不一样的。这是因为变换(Transformations)是按照指定的顺序应用的,且变换的效果是相对于当前坐标系来说的。在变换的过程中,每一步变换都可能改变后续变换的基准坐标系。
先平移再旋转:
当你先对一个组件进行平移操作,你是在当前的坐标系中移动了组件的位置。接着当你对组件进行旋转时,旋转是相对于旋转中心(默认是组件的中心)在已平移的新位置上进行的。
先旋转再平移:
相反,如果你先旋转组件,组件将围绕旋转中心(或指定的旋转点)旋转。此时如果再对组件进行平移,平移将在已旋转的坐标系中进行,这意味着平移的方向可能与原始坐标系中的方向不同。
body: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    // 先平移再旋转
    Transform.translate(
      offset: Offset(50, 0), // 向右平移50个单位
      child: Transform.rotate(
        angle: math.pi / 2, // 旋转90度
        child: Container(
          color: Colors.red,
          width: 100,
          height: 100,
          child: Text('先平移再旋转'),
        ),
      ),
    ),
    SizedBox(height: 50), // 添加一些间隔
    // 先旋转再平移
    Transform.rotate(
      angle: math.pi / 2, // 旋转90度
      child: Transform.translate(
        offset: Offset(50, 0), // 向右平移50个单位
        child: Container(
          color: Colors.blue,
          width: 100,
          height: 100,
          child: Text('先旋转再平移'),
        ),
      ),
    ),
  ],
),
 同时可以看到,变换规则是父组件->子组件
同时可以看到,变换规则是父组件->子组件
裁剪(Clip) - overflow
剪裁(Clipping)是一种常用的UI效果,用于对widget进行形状裁剪或尺寸限制,使其显示区域被限制在特定的形状或路径内。Flutter提供了多种剪裁widget来实现不同的剪裁效果:
默认裁剪
| 剪裁Widget | 默认行为 | 
|---|---|
| ClipOval | 子组件为正方形时剪裁成内贴圆形;为矩形时,剪裁成内贴椭圆 | 
| ClipRRect | 将子组件剪裁为圆角矩形 | 
| ClipRect | 默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁) | 
| ClipPath | 按照自定义的路径剪裁 | 
Widget build(BuildContext context) {
    final Widget avatar = Image.asset("images/pyy.jpeg", width: 60.0, height: 60, fit: BoxFit.fill,); // 设置图像的裁剪效果可以调整fit属性
    return Scaffold(
        appBar: AppBar(
          title: const Text(
            '容器',
          ),
          centerTitle: true, // 文本居中
          backgroundColor: Colors.cyanAccent.shade700,
        ),
        body: Center(
          child: Column(
            children: <Widget>[
              avatar, //不剪裁
              ClipOval(child: avatar), //剪裁为圆形
              ClipRRect(
                //剪裁为圆角矩形
                borderRadius: BorderRadius.circular(5.0),
                child: avatar,
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Align(
                    alignment: Alignment.topLeft,
                    widthFactor: .5, // 宽度设为原来宽度一半,另一半会溢出
                    child: avatar,
                  ),
                  Text(
                    "你好世界",
                    style: TextStyle(color: Colors.green),
                  )
                ],
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  ClipRect(
                    //将溢出部分剪裁
                    child: Align(
                      alignment: Alignment.topLeft,
                      widthFactor: .5, //宽度设为原来宽度一半
                      child: avatar,
                    ),
                  ),
                  Text("你好世界", style: TextStyle(color: Colors.green))
                ],
              ),
            ],
          ),
        ));
  }

widthFactor是Align和其他一些布局widget的一个属性,用于确定子widget的宽度相对于其父widget的大小 最后的两个Row通过Align设置widthFactor为0.5后,图片的实际宽度等于60×0.5,即原宽度一半,但此时图片溢出部分依然会显示。 所以第一个“你好世界”会和图片的另一部分重合,为了剪裁掉溢出部分,我们在第二个Row中通过ClipRect将溢出部分剪裁掉了。
自定义裁剪(CustomClipper)
如果我们想剪裁子组件的特定区域,比如,在上面示例的图片中,如果我们只想截取图片中部40×30像素的范围应该怎么做?这时我们可以使用CustomClipper来自定义剪裁区域,实现代码如下:
class MyClipper extends CustomClipper<Rect> {
  
  Rect getClip(Size size) => Rect.fromLTWH(100.0, 0, 300.0, 300.0); // left,top,width,height
  
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
- getClip()是用于获取剪裁区域的接口,由于图片大小是499x285,我们返回剪裁区域为- Rect.fromLTWH(100.0, 0, 300.0, 300.0),即图片中部40×30像素的范围。
- shouldReclip()接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回- false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回- true来重新执行剪裁。
final Widget avatar = Image.asset("images/pyy.jpeg", width: 375, height: 285, fit: BoxFit.cover,);
DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
    clipper: MyClipper(), //使用自定义的clipper
    child: avatar
  ),
),
avatar, //不剪裁
 可以看到我们的剪裁成功了,但是图片所占用的空间大小仍然是375×285(红色区域),这是因为组件大小是是在layout阶段确定的,而剪裁是在之后的绘制阶段进行的,所以不会影响组件的大小,这和
可以看到我们的剪裁成功了,但是图片所占用的空间大小仍然是375×285(红色区域),这是因为组件大小是是在layout阶段确定的,而剪裁是在之后的绘制阶段进行的,所以不会影响组件的大小,这和Transform原理是相似的。
ClipPath 可以按照自定义的路径实现剪裁,它需要自定义一个CustomClipper<Path> 类型的 Clipper,定义方式和 MyClipper 类似,只不过 getClip 需要返回一个 Path:
// 改写刚才的例子
class MyClipperPath extends CustomClipper<Path> {
  
  Path getClip(Size size) {
    // 定义一个新的路径
    var path = Path();
    // 从左上角(100.0, 0)开始
    path.moveTo(100.0, 0);
    // 定义一个矩形路径,与原来的Rect.fromLTWH(100.0, 0, 300.0, 300.0)等价
    path.lineTo(100.0, 300.0); // 向下画线
    path.lineTo(400.0, 300.0); // 向右画线
    path.lineTo(400.0, 0); // 向上画线
    path.close(); // 闭合路径
    return path;
  }
  
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
空间适配(FittedBox)
子组件大小超出了父组件大小时,如果不经过处理的话 Flutter 中就会显示一个溢出警告并在控制台打印错误日志:
 当看到黄色和黑色条纹警告,这通常表示某个widget超出了其父widget的布局约束。在这个例子中,
当看到黄色和黑色条纹警告,这通常表示某个widget超出了其父widget的布局约束。在这个例子中,Row widget 包含一个文本(Text('xyz' * 50)),这个文本的内容超过了屏幕的宽度。
Row widget 和其他Flex类widget(如 Column 和 Flex)默认情况下不会对其子widget进行剪裁。这意味着如果子widget超出了 Row 的边界,它会在界面上显示为溢出内容的视觉提示,即那些黄色和黑色的条纹。
在这个特定的例子中,虽然 Container 设置了无限宽度的约束(BoxConstraints(minWidth: double.infinity)),但是 Row 仍然尝试让所有的子widget在水平方向上排列,而不考虑它们的总宽度是否超过屏幕宽度。因此,当文本内容超过屏幕宽度时,Row 的子widget(在这里是文本)就会溢出,并在界面上显示溢出的警告。
抛出需求: 如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小呢?还有一种情况,比如父组件的宽高固定,而 Text 文本较少,这时候我们想让文本放大以填充整个父组件空间该怎么做呢?实际上,这两个问题的本质就是:子组件如何适配父组件空间。
定义
FittedBox根据自身的大小和子 widget 的大小,对子 widget 进行缩放和定位,以确保子 widget 能够根据指定的适配方式完全填充 FittedBox 的空间。这对于解决不同屏幕尺寸和分辨率导致的布局问题特别有用,是实现空间适配的一种常见方法。
const FittedBox({
  Key? key,
  this.fit = BoxFit.contain, // 适配方式
  this.alignment = Alignment.center, //对齐方式
  this.clipBehavior = Clip.none, //是否剪裁
  Widget? child,
})
| BoxFit枚举值 | 描述 | 
|---|---|
| fill | 子widget会被拉伸来完全填充FittedBox的空间,可能会导致子widget的宽高比失衡。 | 
| contain | 保持子widget的宽高比,使得子widget尽可能大地填充,同时确保其完整显示在FittedBox内部。 | 
| cover | 保持子widget的宽高比,尽可能小地填充,确保FittedBox的每个部分都被子widget覆盖。 | 
| fitWidth | 保持子widget的宽高比,缩放子widget以填充FittedBox的宽度。 | 
| fitHeight | 保持子widget的宽高比,缩放子widget以填充FittedBox的高度。 | 
适配原理
- FittedBox 在布局子组件时会忽略其父组件传递的约束,可以允许子组件无限大,即FittedBox 传递给子组件的约束为(0<=width<=double.infinity, 0<= height <=double.infinity)。
- FittedBox 对子组件布局结束后就可以获得子组件真实的大小。
- FittedBox 知道子组件的真实大小也知道他父组件的约束,那么FittedBox 就可以通过指定的适配方式(BoxFit 枚举中指定),让起子组件在 FittedBox 父组件的约束范围内按照指定的方式显示。
示例
Widget wContainer(BoxFit boxFit) {
  return Container(
    width: 50,
    height: 50,
    color: Colors.red,
    child: FittedBox(
      fit: boxFit,
      // 子容器超过父容器大小
      child: Container(width: 60, height: 70, color: Colors.blue),
    ),
  );
}
  body: Center(
    child: Column(
      children: [
        wContainer(BoxFit.none),
        Text('上方区域有越界'),
        wContainer(BoxFit.contain),
        Text('正常展示'),
      ],
  ),
 因为父Container要比子Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制:
因为父Container要比子Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制:
- 第一个蓝色区域会超出父组件的空间,因而看不到红色区域
- 第二个我们指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分
把子组件的蓝色背景删除看下效果:
 在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,但FittedBox 自身还是要遵守其父组件传递的约束,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占50×50的大小,接下来文本会紧挨着Container进行布局,而此时Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了Container,但布局位置是正常的,所以就重叠了。 在未指定适配方式时,虽然 FittedBox 子组件的大小超过了 FittedBox 父 Container 的空间,但FittedBox 自身还是要遵守其父组件传递的约束,所以最终 FittedBox 的本身的大小是 50×50,这也是为什么蓝色会和下面文本重叠的原因,因为在布局空间内,父Container只占50×50的大小,接下来文本会紧挨着Container进行布局,而此时Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了Container,但布局位置是正常的,所以就重叠了。如果我们不想让蓝色超出父组件布局范围,那么可以使用 ClipRect 包裹wContainer中的Container对超出的部分剪裁掉即可 
页面框架(Scaffold)- 重要
Scaffold 是 Flutter 中的一个非常重要的布局组件,它提供了一个框架,用于实现基本的材料设计布局结构。Scaffold 提供了默认的导航栏、标题、包含主屏幕widget树的body属性、一个浮动操作按钮、抽屉菜单(通常用于创建导航抽屉)等等。
| 属性 | 类型 | 描述 | 
|---|---|---|
| appBar | PreferredSizeWidget | 显示在界面顶部的AppBar。 | 
| body | Widget | 当前界面所显示的主要内容Widget。 | 
| floatingActionButton | Widget | 浮动在body上的按钮,通常用于执行应用程序中的主要动作。 | 
| drawer | Widget | 抽屉菜单,通常是一个Drawer widget,从屏幕边缘滑入。 | 
| bottomNavigationBar | Widget | 底部导航条,可以有多个BottomNavigationBarItem组成。 | 
| backgroundColor | Color | Scaffold的背景颜色。 | 
import 'package:flutter/material.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Scaffold 示例'),
          actions: <Widget>[
            //导航栏右侧菜单
            IconButton(icon: Icon(Icons.share), onPressed: () {}),
          ],
        ),
        body: Center(
          child: Text('Hello, Scaffold!'),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // 浮动动作按钮的点击事件
            print('FloatingActionButton tapped');
          },
          child: Icon(Icons.add),
        ),
        bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: '首页',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: '学习',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: '学习',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: '学习',
            ),
          ],
          type: BottomNavigationBarType.fixed, // tabbar超过3个,不加该属性会展示异常
          currentIndex: 0,
          selectedItemColor: Theme.of(context).colorScheme.primary,
          // 切换页面
          onTap: (index) {
            print(index);
          },
        ),
        drawer: Drawer(
          child: ListView(
            padding: EdgeInsets.zero,
            children: <Widget>[
              DrawerHeader(
                child: Text('抽屉头部'),
                decoration: BoxDecoration(
                  color: Colors.blue,
                ),
              ),
              ListTile(
                title: Text('项 1'),
                onTap: () {
                  // 更新应用状态...
                  Navigator.pop(context); // 关闭抽屉菜单
                },
              ),
              ListTile(
                title: Text('项 2'),
                onTap: () {
                  // 更新应用状态...
                  Navigator.pop(context); // 关闭抽屉菜单
                },
              ),
              // ... 其他列表项
            ],
          ),
        ),
      ),
    );
  }
}

AppBar
AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:
AppBar({
  Key? key,
  this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  this.title,// 页面标题
  this.actions, // 导航栏右侧菜单
  this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle, //标题是否居中 
  this.backgroundColor,
  ...   //其他属性见源码注释
})