Flutter 常用 Widget 详解

Flutter 中拥有 30 多种预定义的布局 widget,常用的有 Container、Padding、Center、Flex、Row、Colum、ListView、GridView。按照《Flutter 技术入门与实战》上面来说的话,大概分为四类

基础布局组件:Container(容器布局),Center(居中布局),Padding(填充布局),Align(对齐布局),Colum(垂直布局),Row(水平布局),Expanded(配合 Colum,Row 使用),FittedBox(缩放布局),Stack(堆叠布局),overflowBox(溢出父视图容器)。 宽高尺寸处理:SizedBox(设置具体尺寸),ConstrainedBox(限定最大最小宽高布局),LimitedBox(限定最大宽高布局),AspectRatio(调整宽高比),FractionallySizedBox(百分比布局) 列表和表格处理:ListView(列表),GridView(网格),Table(表格) 其它布局处理:Transform(矩阵转换),Baseline(基准线布局),Offstage(控制是否显示组件),Wrap(按宽高自动换行布局)

| MaterialApp

  • title : 在任务管理窗口中所显示的应用名字
  • theme : 应用各种 UI 所使用的主题颜色
  • color : 应用的主要颜色值(primary color),也就是安卓任务管理窗口中所显示的应用颜色
  • home : 应用默认所显示的界面 Widget
  • routes : 应用的顶级导航表格,这个是多页面应用用来控制页面跳转的,类似于网页的网址
  • initialRoute :第一个显示的路由名字,默认值为 Window.defaultRouteName
  • onGenerateRoute : 生成路由的回调函数,当导航的命名路由的时候,会使用这个来生成界面
  • onLocaleChanged : 当系统修改语言的时候,会触发 å 这个回调
  • navigatorObservers : 应用 Navigator 的监听器
  • debugShowMaterialGrid : 是否显示 纸墨设计 基础布局网格,用来调试 UI 的工具
  • showPerformanceOverlay : 显示性能标签,https://flutter.io/debugging/#performanceoverlay

| Row & Column

Row 和 Column 都是 Flex 的子类,只是 direction 参数不同。

Row({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline textBaseline,
  List<Widget> children = const <Widget>[],
})
Column({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline textBaseline,
  List<Widget> children = const <Widget>[],
})

mainAxisSize 主轴方向占用空间,只有两个属性 max 和 min;

在主轴(水平)方向占用的空间,默认是 MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row 的宽度始终等于水平方向的最大宽度;而 MainAxisSize.min 表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则 Row 的实际宽度等于所有子组件占用的的水平空间

mainAxisAlignment 设置主轴方向上排列展示

MainAxisAlignment.start : 默认值,将子控件放在主轴的开始位置 Column 靠上,Row 靠左; MainAxisAlignment.end : 将子控件放在主轴的结束位置,Column 靠下,Row 靠右; MainAxisAlignment.center : 将子控件放在主轴的中间位置 MainAxisAlignment.spaceBetween : 将主轴空白位置进行均分,排列子元素,手尾没有空隙 MainAxisAlignment.spaceAround : 将主轴空白区域均分,使中间各个子控件间距相等,首尾子控件间距为中间子控件间距的一半 MainAxisAlignment.spaceEvenly :将主轴空白区域均分,使各个子控件间距相等

crossAxisAlignment 设置交叉轴方向上排列展示

CrossAxisAlignment.end : Column 中会使子控件向右对齐,Row 中会使子控件向下对齐 CrossAxisAlignment.center : 默认值,子控件居中 CrossAxisAlignment.start : Column 中会使子控件向左对齐,Row 中会使子控件向上对齐 CrossAxisAlignment.stretch : Column 中会使子控件宽度调到最大,Row 则使子控件高度调到最大 CrossAxisAlignment.baseline : 按文本水平线对齐。与 TextBaseline 搭配使用

verticalDirection 文字的垂直布局方向,只有两个属性 down 和 up。VerticalDirection.down : 向下

TextDirection 文字方向从左往右(TextDirection.ltr);从右往左(TextDirection.rtl)

TextBaseline TextBaseline.alphabetic    用于对齐字母字符底部的水平线 TextBaseline.ideographic   用于对齐表意字符的水平线。

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flex > Row

| Expanded & Flexible

Expanded 组件可以使 Row、Column、Fiex 等子组件在其主轴上方向展开并填充可用的空间,这里注意:Expanded 组件必须用在 Row、Column、Fiex 内,并且从 Expanded 到封装它的 Row、Column、Flex 的路径必须只包括 StatelessWidgets 或者 StatefulWidgets(不能是其他类型的组件,像 RenderObjectWidget,它是渲染对象,不再改变尺寸,因此 Expanded 不能放进 RenderObjectWidget)。 注意一点:在 Row 中使用 Expanded 的时候,无法指定 Expanded 中的子组件的宽度 width,但可以指定其高度 height。同理,在 Column 中使用 Expanded 的时候,无法指定 Expanded 中的子组件的高度 height,可以指定宽度 width。

Flexible 是一个控制 Row、Column、Flex 等子组件如何布局的组件。但不强制子组件填充可用空间。

Row(
  children: <Widget>[
    Container( /// 此组件在主轴方向占据48.0逻辑像素
      width: 48.0
    ),
    Expanded(
      child: Container() /// 此组件会填满Row在主轴方向的剩余空间,撑开Row
    )
  ]

// 使用Expanded控件,将一行的宽度分成四个等分,第一、三个child占1/4的区域,第二个child占1/2区域,由flex属性控制。
class MyColumn extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return new Column(
      children: <Widget>[
        new Expanded(
          child: new Container(
            color: Colors.red,
            padding: EdgeInsets.all(5.0),
          ),
          flex: 1,
        ),
        new Expanded(
          child: new Container(
            color: Colors.yellow,
            padding: EdgeInsets.all(5.0),
          ),
          flex: 2,
        ),
        new Expanded(
          child: new Container(
            color: Colors.blue,
            padding: EdgeInsets.all(5.0),
          ),
          flex: 1,
        )
      ],
    );
  }
}

| Container

Container 提供的属性:

  • key:Container 唯一标识符,用于查找更新。
  • alignment:控制 child 的对齐方式
  • padding:decoration 内部的空白区域,如果有 child 的话,child 位于 padding 内部。
  • color:用来设置 container 背景色,如果 foregroundDecoration 设置的话,可能会遮盖 color 效果。
  • decoration:绘制在 child 后面的装饰。container 背景色 color 和 decoration 不能同时设置。
  • foregroundDecoration:绘制在 child 前面的装饰。
  • width:container 的宽度,设置为 double.infinity 可以强制在宽度上撑满
  • height:container 的高度,设置为 double.infinity 可以强制在高度上撑满
  • constraints:你可以使用 BoxConstraints 添加到 child 上额外的约束条件
  • margin:围绕在 decoration 和 child 之外的空白区域。
  • transform:设置 container 的变换矩阵,类型为 Matrix4。
  • child:container 中的内容 widget。
Container(
  width: 100,
  height: 110,
  margin: EdgeInsets.fromLTRB(5, 5, 5, 30),
  padding: EdgeInsets.fromLTRB(20, 10, 20, 10),
  child: Text("22")
  alignment: Alignment.center,
  transform: Matrix4.rotationZ(.2), //卡片倾斜变换
  decoration: BoxDecoration(
    color: Colors.white,
    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
      )
    ],
    border: Border.all(color: Color(0xFFFF0000), width: 0.5), // 设置边框
    borderRadius: BorderRadius.circular((5.0)), // 设置圆角
    image: DecorationImage( // 设置背景图片
        image: AssetImage('images/blockBg/bg4.png'),
        fit: BoxFit.cover)),
)

Container 的绘制的过程如下:

  1. 首先会绘制 transform 效果;
  2. 接着绘制 decoration;
  3. 然后绘制 child;
  4. 最后绘制 foregroundDecoration。

Container 自身尺寸的调节分两种情况:

  1. Container 在没有子节点(children)的时候,会试图去变得足够大。除非 constraints 是 unbounded 限制,在这种情况下,Container 会试图去变得足够小。
  2. 带子节点的 Container,会根据子节点尺寸调节自身尺寸,但是 Container 构造器中如果包含了 width、height 以及 constraints,则会按照构造器中的参数来进行尺寸的调节。

尺寸限制

ConstrainedBox 用于对子组件添加额外的约束。例如,如果你想让子组件的最小高度是 80 像素,你可以使用 const BoxConstraints(minHeight: 80.0)作为子组件的约束。

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, // 宽度尽可能大
    minHeight: 50.0 // 最小高度为50像素
  ),
  child: Container(
      height: 5.0, // 即使将Container的高度设置为5像素,但是最终却是50像素,这正是ConstrainedBox的最小高度限制生效了
      child: redBox
  ),
)

BoxConstraints 用于设置限制条件,它的定义如下:

const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

BoxConstraints 还定义了一些便捷的构造函数,用于快速生成特定限制规则的 BoxConstraints,如 BoxConstraints.tight(Size size)它可以生成给定大小的限制。

SizedBox 用于给子元素指定固定的宽高

SizedBox((width: 80.0), (height: 80.0), (child: redBox));
// 实际上SizedBox只是ConstrainedBox的一个定制,上面代码等价于:
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox,
)
// 也等价于:
BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)

多重限制

多重限制时,对于 minWidth 和 minHeight 来说,是取父子中相应数值较大的。实际上,只有这样才能保证父限制与子限制不冲突。

// 最终显示效果是宽90,高60
ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    )
)
  1. Container 会遵循如下顺序去尝试布局
    • 对齐方式
    • 调整自身的尺寸来适应子节点
    • 采用 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 调整自身。
  1. margin,padding 也会影响布局

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > Container

| FittedBox

使用场景:FittedBox 在目前的项目中还未用到过。对于需要缩放调整位置处理的,一般都是图片。

const FittedBox({
Key key,
this.fit: BoxFit.contain,
this.alignment: Alignment.center,
Widget child,
})

参数含义:

fit:缩放的方式,默认的属性是 BoxFit.contain,child 在 FittedBox 范围内,尽可能的大,但是不超出其尺寸。这里注意一点,contain 是保持着 child 宽高比的大前提下,尽可能的填满,一般情况下,宽度或者高度达到最大值时,就会停止缩放。

BoxFit.none BoxFit.fill BoxFit.cover BoxFit.fitHeight BoxFit.fitWidth BoxFit.scaleDown

alignment:对齐方式,默认的属性是 Alignment.center,居中显示 child。

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > FittedBox

| Padding

Padding 作为一个基础 Widget 被使用的频率还是蛮高的,它的功能非常的单一,就是给子节点设置 padding 属性,你可以把它理解为前端的 css padding 属性。

Container(
  color: Colors.blue,
  width: 100.0,
  height: 100.0,
  child: new Padding(
    padding: EdgeInsets.all(8.0),
    child: new Card(
      child: new Text('icepy'),
    ),
  ),
)

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Padding

SingleChildRenderObjectWidget : SingleChildRenderObjectWidget 是 RenderObjectWidgets 的一个子类,用于限制只能有一个子节点。它只提供 child 的存储,而不提供实际的更新逻辑。

RenderObjectWidgets : RenderObjectWidgets 为 RenderObjectElement 提供配置,而 RenderObjectElement 包含着(wrap)RenderObject,RenderObject 则是在应用中提供实际的绘制(rendering)的元素。

| Align

设置 child 的对齐方式。当设置了 widthFactor 或 heightFactor 时,Align 会根据 factor 属性来调整自身宽高,比如当 widthFactor 是 3 的时候,Align 会将自身的 width 调整为 child 的 3 倍。

参数含义:

Alignment 实际上是包含了两个属性的,其中第一个参数,-1.0 是左边对齐,1.0 是右边对齐,第二个参数,-1.0 是顶部对齐,1.0 是底部对齐。 widthFactor:宽度因子,如果设置的话,Align 的宽度就是 child 的宽度乘以这个值,不能为负数。 heightFactor:高度因子,如果设置的话,Align 的高度就是 child 的高度乘以这个值,不能为负数。

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

// 居右高于底部1/4处.
static const Alignment rightHalfBottom = alignment: const Alignment(1.0, 0.5),

Container(
  width: 200,
  height: 50,
  color: Colors.green,
  child: new Align(
    alignment: Alignment.center,
    child: new Text('icepy'),
  ),
)

Container(
  color: Colors.green,
  child: new Align(
    widthFactor: 2.0,
    heightFactor: 2.0,
    child: new Text('icepy'),
  ),
)

| Center

A widget that centers its child within itself.

| Stack 和 IndexedStack

Stack 可以类比 CSS 中的 absolute(绝对布局),一般情况这个 Widget 只会用于显示比如图片之上的一些信息。它的布局行为对于 child Widget 来说只分 positioned 还是 non-positioned ,处理方式不同。

  • 如果是 positioned 的 child Widget 它的布局行为会根据设置的 top bottom right left 来确定
  • 如果是 non-positioned 的 child Widget 它的布局行为会根据 Stack 的 aligment 对齐方式来处理
  • 对于 child Widget 的叠加处理是 List 第一个 child Widget 在最下层,后面的依次在前一个 child 的上面,last child Widget 在最上层,位置的顺序非常类似 CSS 中的 z-index。如果想调整显示的顺序,则可以通过摆放 child 的顺序来进行。

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Stack

Stack({
  Key key,
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

overflow:超过的部分是否裁剪掉(clipped) alignment:对齐方式,默认是左上角(topStart)。 textDirection:文本的方向,绝大部分不需要处理。 fit:定义如何设置 non-positioned 节点尺寸,默认为 loose。 其中 StackFit 有如下几种:

  • loose:子节点宽松的取值,可以从 min 到 max 的尺寸;
  • expand:子节点尽可能的占用空间,取 max 尺寸;
  • passthrough:不改变子节点的约束条件。
// non-positioned
body: new Container(
  width: 200.0,
  height: 200.0,
  color: Colors.blue,
  child: new Stack(
    alignment: Alignment(0, 0),
    children: <Widget>[
      new Text(
        'icepy'
      ),
    ],
  ),
)
// positioned
body: new Container(
  width: 200.0,
  height: 200.0,
  color: Colors.blue,
  child: new Stack(
    children: <Widget>[
      new Positioned(
        top: 10.0,
        right: 10.0,
        child: new Text(
          'icepy'
        ),
      )
    ],
  ),
)

IndexedStack 继承自 Stack,它的作用是显示第 index 个 child,其他 child 都是不可见的。所以 IndexedStack 的尺寸永远是跟最大的子节点尺寸一致。

// 构造函数
IndexedStack({
    Key key,
    AlignmentGeometry alignment = AlignmentDirectional.topStart,
    TextDirection textDirection,
    StackFit sizing = StackFit.loose,
    this.index = 0,
    List<Widget> children = const <Widget>[],
  }) : super(key: key, alignment: alignment, textDirection: textDirection, fit: sizing, children: children);
// IndexedStack
body: new Container(
  width: 200.0,
  height: 200.0,
  color: Colors.blue,
  child: new IndexedStack(
    index: 1,
    children: <Widget>[
      new Container(
        width: 20.0,
        height: 20.0,
        color: Colors.grey,
      ),
      new Positioned(
        top: 10.0,
        right: 10.0,
        child: new Container(
          width: 50.0,
          height: 50.0,
          color: Colors.red,
        ),
      )
    ],
  ),
)

| OverflowBox

继承关系 Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > OverflowBox

布局行为 当 OverflowBox 的最大尺寸大于 child 的时候,child 可以完整显示,当其小于 child 的时候,则以最大尺寸为基准,当然,这个尺寸都是可以突破父节点的。最后加上对齐方式,完成布局。

const OverflowBox({
    Key key,
    this.alignment = Alignment.center,
    this.minWidth,
    this.maxWidth,
    this.minHeight,
    this.maxHeight,
    Widget child,
  })

| Dialog

import 'package:flutter/material.dart';
class AVDialog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('AVAlertDialog'),
      ),
      body: Column(
        children:[
          new RaisedButton(
            child: new Text('click me'),
            color: Colors.green,
            onPressed: (){
              showDialog(
                context: context,
                child: new SimpleDialog( // 没有按钮
                  title: new Text('Hello icepy'),
                  children: <Widget>[
                    new Text('dialog children')
                  ],
                )
              );
            },
          ),
          new RaisedButton(
            child: new Text('click me'),
            color: Colors.green,
            onPressed: (){
              showDialog(
                context: context,
                child: new AlertDialog( // 带按钮
                  title: new Text('alert'),
                  actions: <Widget>[
                    new FlatButton(
                      child: new Text('确定'),
                      onPressed: (){
                        Navigator.pop(context);
                      },
                    )
                  ],
                )
              )
          }),
          new RaisedButton(
            child: new Text('click me'),
            color: Colors.green,
            onPressed: (){
              showDialog(
                context: context,
                child: new AboutDialog( // 不常用,只用于显示应用
                  applicationName: 'Flutter',
                  applicationVersion: 'v1.0.0',
                  applicationIcon: new Icon(
                    Icons.android,
                    color: Colors.green,
                  ),
                  children: <Widget>[
                    new Text(
                      '更新摘要 \n Update 1.0.0'
                    )
                  ],
                )
              )
        ]
      )
    );
  }
}

| Scaffold

它提供了诸如:抽屉(drawers)底部按钮(bottom sheets)和 底部通知(snack bars)。你可以认为它是一基本快速实现某些布局的容器 Widget。

import 'package:flutter/material.dart';
class AVScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar( // AppBar
        title: new Text('Home'),
      ),
      body: new Center(
        child: new RaisedButton(
          child: new Text('click me'),
          color: Colors.blue,
          onPressed: () {
            Scaffold.of(context).showSnackBar(new SnackBar( // snack bars 的效果
              content: new Text('SnackBar'),
            ));
          }
        )
      ),
      drawer: new Drawer( // Drawers 布局框架
        child: new ListView(
          padding: const EdgeInsets.only(),
          children: <Widget>[
            new ListTile(
              title: new Text('Drawer'),
              onTap: () => {},
            )
          ],
        ),
      ),
      bottomSheet: new Container( // bottom sheets ,这个效果其实和前端里的 position: fixed 很类似
        child: new Row(
          children: <Widget>[
            new Expanded(
              child: new TextField(
                controller: textCV,
              ),
            ),
            new RaisedButton(
              child: new Text('发表'),
              onPressed: () {
                setState(() {
                  data.add(textCV.text);
                });
              },
            )
          ],
        ),
      ),
    );
  }
}

更多参考

老孟教程