文章

04.Flutter容器类组件

04.Flutter容器类组件

容器类 Widget 和布局类 Widget 都作用于其子 Widget,不同的是:

  • 布局类 Widget 一般都需要接收一个 widget 数组(children),他们直接或间接继承自(或包含)MultiChildRenderObjectWidget ;而容器类 Widget 一般只需要接收一个子 Widget(child),他们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
  • 布局类 Widget 是按照一定的排列方式来对其子 Widget 进行排列;而容器类 Widget 一般只是包装其子 Widget,对其添加一些修饰(补白或背景色等)、变换 (旋转或剪裁等)、或限制 (大小等)。

Padding 填充

Padding 可以给其子节点添加填充(留白),和边距效果类似。Padding 定义:

1
2
3
4
5
Padding({
  // ...
  EdgeInsetsGeometry padding,
  Widget child,
})
  • EdgeInsetsGeometry 是一个抽象类,开发中,我们一般都使用 EdgeInsets 类,它是 EdgeInsetsGeometry 的一个子类,定义了一些设置填充的便捷方法

EdgeInsets

  • fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充。
  • all(double value) : 所有方向均使用相同数值的填充。
  • only({left, top, right ,bottom }):可以设置具体某个方向的填充 (可以同时指定多个方向)。
  • symmetric({ vertical, horizontal }):用于设置对称方向的填充,vertical 指 top 和 bottom,horizontal 指 left 和 right

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Padding(
    //上下左右各添加16像素补白
    padding: EdgeInsets.all(16),
    child: Column(
      //显式指定对齐方式为左对齐,排除对齐干扰
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Padding(
          //左边添加8像素补白
          padding: EdgeInsets.only(left: 8),
          child: Text("Hello world"),
        ),
        Padding(
          //上下各添加8像素补白
          padding: EdgeInsets.symmetric(vertical: 8),
          child: Text("I am Jack"),
        ),
        Padding(
          // 分别指定四个方向的补白
          padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
          child: Text("Your friend"),
        )
      ],
    ),
  );

ik5a4

DecoratedBox 装饰

DecoratedBox 可以在其子组件绘制前 (或后) 绘制一些装饰(Decoration),如背景、边框、渐变等。DecoratedBox 定义如下:

1
2
3
4
5
const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget? child
}
  • decoration:代表将要绘制的装饰,它的类型为 Decoration。Decoration 是一个抽象类,它定义了一个接口 createBoxPainter(),子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。
  • position:此属性决定在哪里绘制 Decoration,它接收 DecorationPosition 的枚举类型,该枚举类有两个值:
    • background:在子组件之后绘制,即背景装饰。
    • foreground:在子组件之上绘制,即前景

通常会直接使用 BoxDecoration 类,它是一个 Decoration 的子类,实现了常用的装饰元素的绘制

1
2
3
4
5
6
7
8
9
10
BoxDecoration({
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 DecoratedBox(
   decoration: BoxDecoration(
     gradient: LinearGradient(colors:[Colors.red,Colors.orange.shade700]), //背景渐变
     borderRadius: BorderRadius.circular(3.0), //3像素圆角
     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),),
  )
)

me1el

Transform 变换

Transform 可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。

Matrix4

Matrix4 是一个 4D 矩阵,通过它我们可以实现各种矩阵操作,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
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!'),
    ),
  ),
)

184k7

平移

Transform.translate 接收一个 offset 参数,可以在绘制时沿 x、y 轴对子组件平移指定的距离。

1
2
3
4
5
6
7
8
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  //默认原点为左上角,左移20像素,向上平移5像素  
  child: Transform.translate(
    offset: Offset(-20.0, -5.0),
    child: Text("Hello world"),
  ),
)

9scgi

旋转

Transform.rotate 可以对子组件进行旋转变换:

1
2
3
4
5
6
7
8
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.rotate(
    //旋转90度
    angle: pi/2 ,
    child: Text("Hello world"),
  ),
)

hnvmg

缩放

Transform.scale 可以对子组件进行缩小或放大

1
2
3
4
5
6
7
DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.scale(
    scale: 1.5, //放大到1.5倍
    child: Text("Hello world")
  )
);

hz1lq

Transform 注意事项

  • Transform 的变换是应用在绘制阶段,而并不是应用在布局 (layout) 阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。

示例说明:

1
2
3
4
5
6
7
8
9
10
11
12
 Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration:BoxDecoration(color: Colors.red),
      child: Transform.scale(scale: 1.5,
          child: Text("Hello world")
      )
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
)

5gscy

由于第一个 Text 应用变换 (放大) 后,其在绘制时会放大,但其占用的空间依然为红色部分,所以第二个 Text 会紧挨着红色部分,最终就会出现文字重合。

  • 由于矩阵变化只会作用在绘制阶段,所以在某些场景下,在 UI 需要变化时,可以直接通过矩阵变化来达到视觉上的 UI 改变,而不需要去重新触发 build 流程,这样会节省 layout 的开销,所以性能会比较好。如之前介绍的 Flow 组件,它内部就是用矩阵变换来更新 UI,除此之外,Flutter 的动画组件中也大量使用了 Transform 以提高性能。

RotatedBox

RotatedBox 和 Transform.rotate 功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox 的变换是在 layout 阶段,会影响在子组件的位置和大小。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      //将Transform.rotate换成RotatedBox  
      child: RotatedBox(
        quarterTurns: 1, //旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
),

ah5x0

由于 RotatedBox 是作用于 layout 阶段,所以子组件会旋转 90 度(而不只是绘制的内容),decoration 会作用到子组件所占用的实际空间上,所以最终就是上图的效果

Container 容器组件

什么是 Container?

Container 是一个组合类容器,它本身不对应具体的 RenderObject;它是 DecoratedBox、ConstrainedBox、Transform、Padding、Align 等组件组合的一个多功能容器,所以我们只需通过一个 Container 组件可以实现同时需要装饰、变换、限制的场景
可以得出几个信息,它是一个组合的 widget,内部有绘制 widget、定位 widget、尺寸 widget。后续看到的不少 widget,都是通过一些更基础的 widget 组合而成的。

Container 的行为

由于 Container 组合了一系列的 widget,这些 widget 都有自己的布局行为,因此 Container 的布局行为有时候是比较复杂的。
一般情况下,Container 会遵循如下顺序去尝试布局:

  • 对齐(alignment);
  • 调节自身尺寸适合子节点;
  • 采用 width、height 以及 constraints 布局;
  • 扩展自身去适应父节点;
  • 调节自身到足够小。

进一步说:

  • 如果没有子节点、没有设置 width、height 以及 constraints,并且父节点没有设置 unbounded 的限制,Container 会将自身调整到足够小。
  • 如果没有子节点、对齐方式(alignment),但是提供了 width、height 或者 constraints,那么 Container 会根据自身以及父节点的限制,将自身调节到足够小。
  • 如果没有子节点、width、height、constraints 以及 alignment,但是父节点提供了 bounded 限制,那么 Container 会按照父节点的限制,将自身调整到足够大。
  • 如果有 alignment,父节点提供了 unbounded 限制,那么 Container 将会调节自身尺寸来包住 child;
  • 如果有 alignment,并且父节点提供了 bounded 限制,那么 Container 会将自身调整的足够大(在父节点的范围内),然后将 child 根据 alignment 调整位置;
  • 含有 child,但是没有 width、height、constraints 以及 alignment,Container 会将父节点的 constraints 传递给 child,并且根据 child 调整自身。

另外,margin 以及 padding 属性也会影响到布局。

Container 属性

Container 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,
  ...
})
  • key:Container 唯一标识符,用于查找更新。
  • 容器的大小可以通过 width、height 属性来指定,也可以通过 constraints 来指定;如果它们同时存在时,width、height 优先。实际上 Container 内部会根据 width、height 来生成一个 constraints
    • width:container 的宽度,设置为 double.infinity 可以强制在宽度上撑满;如果不设置,则根据 child 和父节点两者一起布局。
    • height:container 的高度,设置为 double.infinity 可以强制在高度上撑满。
    • constraints:添加到 child 上额外的约束条件。
  • color 和 decoration 是互斥的,如果同时设置它们则会报错!实际上,当指定 color 时,Container 内会自动创建一个 decoration
    • color:用来设置 container 背景色,如果 foregroundDecoration 设置的话,可能会遮盖 color 效果。
    • decoration:绘制在 child 后面的装饰,设置了 decoration 的话,就不能设置 color 属性,否则会报错,此时应该在 decoration 中进行颜色的设置。
    • foregroundDecoration:绘制在 child 前面的装饰。
  • alignment
    这个属性针对的是 Container 内 child 的对齐方式,也就是容器子内容的对齐方式,并不是容器本身的对齐方式。

如果 container 或者 container 父节点尺寸大于 child 的尺寸,这个属性设置会起作用

  • bottomCenter: 下部居中对齐。
  • botomLeft: 下部左对齐。
  • bottomRight:下部右对齐。
  • center:纵横双向居中对齐。
  • centerLeft:纵向居中横向居左对齐。
  • centerRight:纵向居中横向居右对齐。
  • topLeft:顶部左侧对齐。
  • topCenter:顶部居中对齐。
  • topRight: 顶部居左对齐。
  • padding 内边距
    decoration 内部的空白区域,如果有 child 的话,child 位于 padding 内部。padding 与 margin 的不同之处在于,padding 是包含在 content 内,而 margin 则是外部边界,设置点击事件的话,padding 区域会响应,而 margin 区域不会响应。
1
padding : const EdgeInsets.fromLTRB(10.0,30.0,0.0,0.0),
  • margin 属性
    margin 是外边距,指的是 container 和外部元素的距离 ;围绕在 decoration 和 child 之外的空白区域,不属于内容区域
  • transform 设置 container 的变换矩阵,类型为 Matrix4
  • decoration
    decoration 是 container 的修饰器,主要的功能是设置背景和边框,用 BoxDecoration

设置边框 border:Border.all(width:2.0,color:Colors.red)

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Container(
  margin: EdgeInsets.only(top: 50.0, left: 120.0),
  constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0),//卡片大小
  decoration: BoxDecoration(  //背景装饰
    gradient: RadialGradient( //背景径向渐变
      colors: [Colors.red, Colors.orange],
      center: Alignment.topLeft,
      radius: .98,
    ),
    boxShadow: [
      //卡片阴影
      BoxShadow(
        color: Colors.black54,
        offset: Offset(2.0, 2.0),
        blurRadius: 4.0,
      )
    ],
  ),
  transform: Matrix4.rotationZ(.2),//卡片倾斜变换
  alignment: Alignment.center, //卡片内文字居中
  child: Text(
    //卡片文字
    "5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),
  ),
 )

6mgc5

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '这是title',
      home: Scaffold(

        appBar: AppBar(
          title: Text("这是AppBar的title"),
        ),

        body: Center(
            child: Container(
              child: Text("Hello 演示Container"),
              alignment: Alignment.bottomRight,
              // Container中子控件的对齐方式
              width: 500,
              height: 380,
              // color: Colors.blueGrey, // 不能和decoration同时存在
              padding: EdgeInsets.fromLTRB(0, 0, 50, 10),
    //              const EdgeInsets.only(left: 0, top: 0, right: 10, bottom: 90),
              margin: EdgeInsets.all(50),
              decoration: BoxDecoration(
                  gradient: const LinearGradient(colors: [Colors.blueGrey, Colors.greenAccent, Colors.purple]),
                  border: Border.all(width: 2,color: Colors.black87),
              ),
        )
        ),
      ),
    );
  }
}

bz68l

Clip 裁剪

Flutter 中提供了一些剪裁组件,用于对组件进行剪裁。

剪裁 Widget默认行为
ClipOval子组件为正方形时剪裁成内贴圆形;为矩形时,剪裁成内贴椭圆
ClipRRect将子组件剪裁为圆角矩形
ClipRect默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)
ClipPath按照自定义的路径剪裁

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class ClipTestRoute extends StatelessWidget {
  const ClipTestRoute({super.key});

  @override
  Widget build(BuildContext context) {
    // 头像
    Widget avatar = Image.asset("images/logo.png", width: 100.0);
    return Center(
      child: Column(
        children: <Widget>[
          const Text('不裁剪'),
          avatar, //不剪裁
          const Text('裁剪为圆形'),
          ClipOval(child: avatar), //剪裁为圆形
          const Text('裁剪为圆角矩形'),
          ClipRRect(
            //剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ),

          const Text('溢出部分裁剪'),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Align(
                alignment: Alignment.topLeft,
                widthFactor: .5, //宽度设为原来宽度一半,另一半会溢出
                child: avatar,
              ),
              const Text(
                "你好世界123456789",
                style: TextStyle(color: Colors.green),
              )
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(
                //将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5, //宽度设为原来宽度一半
                  child: avatar,
                ),
              ),
              const Text("你好世界123456789", style: TextStyle(color: Colors.green))
            ],
          ),
        ],
      ),
    );
  }
}

64z4q

最后 2 个 Row,通过 Align 设置 widthFactor 为 0.5 后,图片的实际宽度等于 60×0.5,即原宽度一半,但此时图片溢出部分依然会显示,所以第一个 “ 你好世界 123456789” 会和图片的另一部分重合,为了剪裁掉溢出部分,我们在第二个 Row 中通过 ClipRect 将溢出部分剪裁掉了。

CustomClipper 自定义裁剪

剪裁子组件的特定区域

1
2
3
4
5
6
7
8
9
class MyClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) {
    return const Rect.fromLTWH(10, 10, 60.0, 80.0);
  }

  @override
  bool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
}
  • getClip() 用于获取剪裁区域的接口,以原组件左上角为原点裁剪
  • shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回 false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回 true 来重新执行剪裁

示例:

1
2
3
4
5
6
7
Widget avatar = Image.asset("images/logo.png", width: 100.0);
DecoratedBox(
  decoration: const BoxDecoration(color: Colors.red),
  child: ClipRect(
      clipper: MyClipper(), //使用自定义的clipper
      child: avatar),
);

t868x

ClipPath

ClipPath 可以按照自定义的路径实现剪裁,它需要自定义一个 CustomClipper<Path> 类型的 Clipper,定义方式和 MyClipper 类似,只不过 getClip 需要返回一个 Path

FittedBox 空间适配

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Widget demoWidget() {
  return Column(
    children: [
      Padding(
        padding: const EdgeInsets.symmetric(vertical: 30.0),
        child: Row(children: [Text('xx' * 30)]), //文本长度超出 Row 的最大宽度会溢出
      ),
      Container(
        width: 50,
        height: 50,
        color: Colors.red,
        child: Container(width: 50, height: 50, color: Colors.green),
      ),
      const Padding(
        padding: EdgeInsets.symmetric(vertical: 5.0),
      ),
      wContainer(BoxFit.none),
      const Text('Wendux'),
      wContainer(BoxFit.contain),
      const Text('Flutter中国'),
    ],
  );
}

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),
    ),
);

w6qts

  • 因为父 Container 要比子 Container 小,所以当没有指定任何适配方式时,子组件会按照其真实大小进行绘制,所以第一个蓝色区域会超出父组件的空间,因而看不到红色区域
  • 第二个我们指定了适配方式为 BoxFit.contain,含义是按照子组件的比例缩放,尽可能多的占据父组件空间,因为子组件的长宽并不相同,所以按照比例缩放适配父组件后,父组件能显示一部分。
  • Container 中有子组件的大小超过了自己,所以最终的效果就是绘制范围超出了 Container,但布局位置是正常的,所以就重叠了。如果我们不想让蓝色超出父组件布局范围,那么可以可以使用 ClipRect 对超出的部分剪裁掉即可
1
2
3
4
5
6
7
8
9
10
11
 ClipRect( // 将超出子组件布局范围的绘制内容剪裁掉
  child: Container(
    width: 50,
    height: 50,
    color: Colors.red,
    child: FittedBox(
      fit: boxFit,
      child: Container(width: 60, height: 70, color: Colors.blue),
    ),
  ),
);

Scaffold 页面骨架

Scaffold 是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
Scaffold 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Scaffold({
    super.key,
    this.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,
    this.floatingActionButtonAnimator,
    this.persistentFooterButtons,
    this.persistentFooterAlignment = AlignmentDirectional.centerEnd,
    this.drawer,
    this.onDrawerChanged,
    this.endDrawer,
    this.onEndDrawerChanged,
    this.bottomNavigationBar,
    this.bottomSheet,
    this.backgroundColor,
    this.resizeToAvoidBottomInset,
    this.primary = true,
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.drawerScrimColor,
    this.drawerEdgeDragWidth,
    this.drawerEnableOpenDragGesture = true,
    this.endDrawerEnableOpenDragGesture = true,
    this.restorationId,
})
  • appBar 顶部的 title bar
  • body 主体
  • floatingActionButton 右下角悬浮按钮
  • drawer 抽屉
  • bottomNavigationBar 底部导航栏

注意:Scaffold 不能作为一个 Widget 的根 View,否则报错;

Scaffold widgets require a Directionality widget ancestor.

AppBar

AppBar 是一个 Material 风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的 Tab 标题等。

1
2
3
4
5
6
7
8
9
10
11
12
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,
  // ...   //其他属性见源码注释
})
  • Scaffold 添加了 drawer,默认情况下 Scaffold 会自动将 AppBar 的 leading 设置为菜单按钮

ww95s

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AppBar(
  title: const Text("App Name"),
  leading: Builder(builder: (context) {
    return IconButton(
      icon: const Icon(Icons.dashboard, color: Colors.blue), //自定义图标
      onPressed: () {
        // 打开抽屉菜单
        Scaffold.of(context).openDrawer();
      },
    );
  }),
  actions: <Widget>[
    //导航栏右侧菜单
    IconButton(
        icon: const Icon(
          Icons.share,
          color: Colors.blue,
        ),
        onPressed: () {}),
]);

wkmks

打开抽屉菜单的方法在 ScaffoldState 中,通过 Scaffold.of(context) 可以获取父级最近的 Scaffold 组件的 State 对象

Drawer 抽屉菜单

Scaffold 的 drawer 和 endDrawer 属性可以分别接受一个 Widget 来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class MyDrawer extends StatelessWidget {
  const MyDrawer({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        // 移除抽屉菜单顶部和avatar默认留白
        removeTop: true,
        removeLeft: false,
        removeRight: false,
        removeBottom: false,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            avatar(),
            menuList(),
          ],
        ),
      ),
    );
  }

  Widget avatar() {
    return Container(
        decoration: const BoxDecoration(
          color: Colors.blue,
        ),
        child: Padding(
          // 用户信息
          padding: const EdgeInsets.only(top: 38.0),
          child: Row(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 16.0),
                child: ClipOval(
                  child: Image.asset(
                    "images/logo.png",
                    width: 80,
                  ),
                ),
              ),
              const Text(
                "hacket",
                style: TextStyle(fontWeight: FontWeight.bold),
              )
            ],
          ),
        ));
  }

  Widget menuList() {
    return Expanded(
        // 菜单项目
        child: Container(
      decoration: const BoxDecoration(
        color: Colors.orange,
      ),
      child: ListView(
        children: const <Widget>[
          ListTile(
            leading: Icon(Icons.add),
            title: Text('Add account'),
          ),
          ListTile(
            leading: Icon(Icons.settings),
            title: Text('Manage accounts'),
          ),
        ],
      ),
    ));
  }
}

j72t8

FloatingActionButton

FloatingActionButton 是 Material 设计规范中的一种特殊 Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如本节示例中页面右下角的 “➕” 号按钮。我们可以通过 Scaffold 的 floatingActionButton 属性来设置一个 FloatingActionButton,同时通过 floatingActionButtonLocation 属性来指定其在页面中悬浮的位置

bottomNavigationBar

bottomNavigationBar 属性来设置底部导航

BottomNavigationBar

1
2
3
4
5
6
7
8
9
10
11
12
BottomNavigationBar(
    // 底部导航
    items: const <BottomNavigationBarItem>[
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
      BottomNavigationBarItem(
          icon: Icon(Icons.business), label: 'Business'),
      BottomNavigationBarItem(icon: Icon(Icons.school), label: 'School'),
    ],
    currentIndex: _selectedIndex,
    fixedColor: Colors.blue,
    onTap: _onItemTapped,
)

21pbz

  • 打洞的位置取决于 floatingActionButtonLocation
    • centerDocked 正中间
    • endDocked 右边
  • BottomAppBar 的 shape 属性决定洞的外形,CircularNotchedRectangle 实现了一个圆形的外形

BottomAppBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bottomNavigationBar: bottomAppBar(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: FloatingActionButton(
    //悬浮按钮
    onPressed: _onAdd,
    //悬浮按钮
    child: const Icon(Icons.add))

Widget bottomAppBar() {
    return BottomAppBar(
      color: Colors.white,
      shape: const CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          IconButton(icon: const Icon(Icons.home), onPressed: () {}),
          const SizedBox(), //中间位置空出
          IconButton(
            icon: const Icon(Icons.business),
            onPressed: () {},
          ),
        ], //均分底部导航栏横向空间
      ),
    );
}

p5h6s

Body 页面内容

body 属性,接收一个 Widget,我们可以传任意的 Widget。
可用 TabBarView,它是一个可以进行页面切换的组件,在多 Tab 的 App 中,一般都会将 TabBarView 作为 Scaffold 的 Body

Card 卡片布局

卡片式布局。这种布局类似 ViewList,但是列表会以物理卡片的形态进行展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class MyCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(
        children: <Widget>[
          ListTile(
            title: Text(
              '深圳南山',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            subtitle: Text('hacket:13510599881'),
            leading: Icon(
              Icons.account_box,
              color: Colors.lightBlue,
            ),
          ),
          Divider(),
          ListTile(
            title: Text(
              '北京市海淀区中国科技大学',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            subtitle: Text('胜宏宇:1513938888'),
            leading: Icon(
              Icons.account_box,
              color: Colors.lightBlue,
            ),
          ),
          Divider(),
          ListTile(
            title: Text(
              '河南省濮阳市百姓办公楼',
              style: TextStyle(fontWeight: FontWeight.w500),
            ),
            subtitle: Text('dasheng:1513938888'),
            leading: Icon(
              Icons.account_box,
              color: Colors.lightBlue,
            ),
          ),
          Divider(),
        ],
      ),
    );
  }
}

3qx14

Flutter 间隔问题

设置 margin

Container 组件的 margin 属性

1
2
3
4
5
6
7
8
9
10
11
Container(
  margin: EdgeInsets.symmetric(horizontal: 30, vertical: 20),
  color: Colors.orange,
  width: 150,
  height: 150,
),
Container(
    color: Colors.orange,
    width: 150,
    height: 150,
)

设置了其中一个 Container 组件水平方向上的 margin 为 30,垂直方向上的 margin 为 20。效果如下:
2ix1c

使用 Padding 组件

将 Container 组件放在 Padding 组件内,然后设置 Padding 组件的 padding 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
Container(
    color: Colors.blue,
    width: 150,
    height: 150,
),
Padding(
  padding: EdgeInsets.only(top: 20),
  child: Container(
    color: Colors.orange,
    width: 150,
    height: 150,
  ),
),

将第二个 Container 放在了 Padding 组件中,并设置了 Padding 组件的内上边距为 20。效果如下:
o27mc

使用 SizeBox 组件

设置 SizeBox 组件的 height 属性或 width 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Container(
  color: Colors.blue,
  width: 150,
  height: 150,
  child: Text(
    'data',
    style: TextStyle(fontSize: 16),
  ),
),
// 设置 SizeBox 
SizedBox(height: 20),

Container(
    color: Colors.orange,
    width: 150,
    height: 150,
)

在两个 Container 组件之间添加 SizeBox 组件,然后设置 SizeBox 的 height 属性,从而让两个 Container 之间具有垂直方向上间距。效果如下:
8p8lk

Row 子控件设置间距

使用 SizedBox 保持固定间距

1
2
3
4
5
6
7
Row(
  children: <Widget>[
    Text("1"),
    SizedBox(width: 50), // 50宽度
    Text("2"),
  ],
)

wdurv

使用 Spacer 填充尽可能大的空间

1
2
3
4
5
6
7
Row(
  children: <Widget>[
    Text("1"),
    Spacer(), // use Spacer
    Text("2"),
  ],
)

vy2dg

使用 mainAxisAlignment 对齐方式控制彼此间距

  • spaceEvenly 平均分布
1
2
3
4
5
6
7
Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly, //元素与空白互相间隔
  children: <Widget>[
    Text("1"),
    Text("2"),
  ],
)

rk3aj

使用 Wrap

指定 spacing

1
2
3
4
5
6
7
Wrap(
  spacing: 100, // set spacing here
  children: <Widget>[
    Text("1"),
    Text("2"),
  ],
)

fufbs

同样是使用 Wrap,设置 spaceAround

1
2
3
4
5
6
7
Wrap(
  alignment: WrapAlignment.spaceAround, // 空白包围住元素
  children: <Widget>[
    Text("1"),
    Text("2"),
  ],
)

4br4e

设置子控件分别左对齐和右对齐

  1. 使用 spaceBetween 对齐方式
1
2
3
4
5
6
7
new Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    new Text("left"),
    new Text("right")
  ]
);
  1. 中间使用 Expanded 自动扩展
1
2
3
4
5
6
7
Row(
  children: <Widget>[
    FlutterLogo(),//左对齐
    Expanded(child: SizedBox()),//自动扩展挤压
    FlutterLogo(),//右对齐
  ],
);
  1. 使用 Spacer 自动填充
1
2
3
4
5
6
7
Row(
  children: <Widget>[
    FlutterLogo(),
    Spacer(),
    FlutterLogo(),
  ],
);
  1. 使用 Flexible
1
2
3
4
5
6
7
Row(
  children: <Widget>[
    FlutterLogo(),
    Flexible(fit: FlexFit.tight, child: SizedBox()),
    FlutterLogo(),
  ],
);

dwbv7

本文由作者按照 CC BY 4.0 进行授权