Flutter 布局

243 阅读8分钟

Text

Text继承自StatelessWidget,用于显示简单样式文本,它包含一些控制文本显示样式的一些属性。 下面是比较常用的属性: maxLines:最大显示行数 overflow:截断方式 textAlign:文本对齐方式 style:文本风格样式 color:文本颜色 fontSize:字体大小 fontFamily:使用自定义字体 background:背景绘制颜色 decoration:装饰下划线

Text(
            "simple text" * 20,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            textAlign: TextAlign.start,
            style: TextStyle(
                color: Colors.blue,
                fontSize: 18.0,
                height: 5,
                fontFamily: "FbtNumber",
                background: Paint()..color = Colors.yellow,
                decoration: TextDecoration.underline,
                decorationStyle: TextDecorationStyle.solid),
          ),

Button

ElevatedButton 即"漂浮"按钮,它默认带有阴影和灰色背景。按下后,阴影会变大。 onPressed 属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

 ElevatedButton(
            
            onPressed: () {
              Fluttertoast.showToast(msg: "ElevatedButton");
            },
            style: ButtonStyle(
                shape: MaterialStateProperty.resolveWith((states) {
                  if (states.contains(MaterialState.pressed)) {
                    return const RoundedRectangleBorder();
                  }
                  if (states.contains(MaterialState.disabled)) {
                    return const CircleBorder();
                  }
                  return const CircleBorder();
                }),
                splashFactory: NoSplash.splashFactory,
                foregroundColor: MaterialStateProperty.resolveWith((states) {
                  return Colors.white;
                }),
                backgroundColor: MaterialStateProperty.resolveWith((states) {
                  if (states.contains(MaterialState.pressed)) {
                    return Colors.red;
                  }
                  if (states.contains(MaterialState.disabled)) {
                    return Colors.black;
                  }
                  return Colors.green;
                })),
            child: const Text("ElevatedButton"),
          )
          
ElevatedButton.icon(
            icon: const Icon(
              Icons.add_a_photo,
              size: 60,
              color: Colors.red,
            ),
            label: const Text("camera"),
            onPressed: () {
              Fluttertoast.showToast(msg: "ElevatedButton");
            },
          ),

TextButton即文本按钮,默认背景透明并不带阴影。按下后,会有背景色

TextButton(
            child: const Text("TextButton"),
            onPressed: () {},
            onLongPress: () {
              Fluttertoast.showToast(msg: "TextButton onLongPress");
            },
            onFocusChange: (b) {
              Fluttertoast.showToast(
                  msg: "TextButton onFocusChange:" + b.toString());
            },
          ),

OutlineButton默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)

OutlinedButton(
            child: const Text("normal"),
            onPressed: () {},
          ),

IconButton 是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景

          IconButton(onPressed: () {}, icon: const Icon(Icons.add_a_photo)),

Image

从asset中加载图片

  1. 在工程根目录下创建一个images目录,并将图片 avatar.png 拷贝到该目录。
  2. pubspec.yaml中的flutter部分添加如下内容:
assets: - images/ic_xiecheng_logo.png

代码中加载本地图片

ClipOval(
                      child: Image.asset(
                        "images/ic_xiecheng_logo.png",
                        width: 80,
                      ),
                    )

从网络加载图片

Image提供了一个快捷的构造函数Image.network用于从网络加载、显示图片,loadingBuilder 用于加载中显示加载进度或者自定义占位Loading

src:网络图片地址

缩放模式 fit:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit中定义,它是一个枚举类型,有如下值:

  • fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
  • cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
  • contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
  • fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
  • fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
  • none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
Image.network(
              "https://devrel.andfun.cn/devrel/posts/2021/05/zC30Hx.png",
              loadingBuilder: (context, child, loadingProgress) {
                if (loadingProgress != null) {
                  return Center(
                    child: CircularProgressIndicator(
                        value: loadingProgress.cumulativeBytesLoaded /
                            loadingProgress.expectedTotalBytes!),
                  );
                }
                return child;
              },
              fit: _selectedBoxFit,
              semanticLabel: "flutter image",
            )

ICON

在Flutter开发中,iconfont和图片相比有如下优势:

  1. 体积小:可以减小安装包大小。
  2. 矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
  3. 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
  4. 可以通过TextSpan和文本混用。

常用属性

icon:icon资源 size:icon大小 color:icon颜色

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

Radio和RadioListTile

RadioListTitle:当有多个Radio组件,其中一个为选中状态,其他自动变成未选中状态。

value: 按钮所代表的值,例如 BoxFit.contain。 groupValue:此组单选按钮的当前选定值。 groupValue与value进行匹配,匹配成功即可更新选中状态。 onChanged:更新选中状态回调函数,会把value值回调给开发者。

Column(
              children: [
                RadioListTile<BoxFit>(
                    value: BoxFit.contain,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.contain"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.contain;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fill,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fill"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fill;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.cover,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.cover"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.cover;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fitHeight,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fitHeight"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fitHeight;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fitWidth,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fitWidth"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fitWidth;
                      });
                    })
              ],
            ),

线性布局(Row和Column)

所谓线性布局,即指沿水平或垂直方向排列子组件。Flutter 中通过RowColumn来实现线性布局,类似于Android 中的LinearLayout控件。 对于线性布局,有主轴和纵轴之分,如果布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向。在线性布局中,有两个定义对齐方式的枚举类MainAxisAlignmentCrossAxisAlignment,分别代表主轴对齐和纵轴对齐。

Row

Row可以沿水平方向排列其子widget。

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

  • mainAxisAlignment:表示子组件在Row所占用的水平空间内对齐方式,如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子组件的宽度等于Row的宽度。只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义,MainAxisAlignment.start表示沿textDirection的初始方向对齐,如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐。而MainAxisAlignment.endMainAxisAlignment.start正好相反;MainAxisAlignment.center表示居中对齐。读者可以这么理解:textDirectionmainAxisAlignment的参考系。

  • crossAxisAlignment:表示子组件在纵轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含startend、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.downcrossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.endcrossAxisAlignment.start正好相反;

  • children :子组件数组。

Row(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
                color: Colors.pink,
                width: 60,
                child: Text('测试 $title  1Widget')),
            const SizedBox(width: 18),
            Container(
                color: Colors.yellow,
                width: 60,
                child: Text('测试 $title 2Widget')),
            const SizedBox(width: 18),
            Container(
                color: Colors.green,
                width: 60,
                height: 200,
                child: Text('测试 $title  3Widget')),
            const SizedBox(width: 18),
            Container(color: Colors.green, width: 100, child: const Text('测试')),
            const SizedBox(width: 18),
          ],
        )

Column

Column可以在垂直方向排列其子组件。参数和Row一样,不同的是布局方向为垂直,主轴纵轴正好相反, RowColumn都只会在主轴方向占用尽可能大的空间,而纵轴的长度则取决于他们最大子元素的长度

Container(
        color: color,
        child: Column(
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
                color: Colors.pink,
                height: 100,
                child: Text('测试 $title  1Widget')),
            const SizedBox(height: 18),
            Container(
                color: Colors.yellow,
                height: 100,
                child: Text('测试 $title 2Widget')),
            const SizedBox(height: 18),
            Container(
                padding: const EdgeInsets.only(right: 100),
                color: Colors.green,
                height: 100,
                child: Text('测试 $title  3Widget')),
            const SizedBox(height: 18),
            Container(
                color: Colors.green, height: 100, child: const Text('测试')),
            const SizedBox(height: 18),
          ],
        ),
      ),

弹性布局(Flex)

弹性布局允许子组件按照一定比例来分配父容器空间。弹性布局的概念在其他UI系统中也都存在,如 H5 中的弹性盒子布局,Android中 的FlexboxLayout等。Flutter 中的弹性布局主要通过FlexExpanded来配合实现。

Wrap

WrapFlex(包括RowColumn)除了超出显示范围后Wrap会折行外,其他行为基本相同。下面我们看一下Wrap特有的几个属性:

  • spacing:主轴方向子widget的间距
  • runSpacing:纵轴方向的间距
  • runAlignment:纵轴方向的对齐方式
Wrap(
            spacing: 8,
            runSpacing: 8,
            children: _layoutWidgetList.map(
              (e) {
                Color itemColor = Colors.accents[
                    _layoutWidgetList.indexOf(e) % Colors.accents.length];

                return GestureDetector(
                  child: Container(
                    decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(8),
                      color: itemColor,
                    ),
                    padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
                    margin: const EdgeInsets.fromLTRB(8, 4, 8, 4),
                    child: Text(
                      e,
                      style: const TextStyle(
                          color: Colors.black87, fontWeight: FontWeight.bold),
                    ),
                  ),
                  onTap: () {
                    _jumpTestWidget(e, itemColor);
                  },
                );
              },
            ).toList(),
          )

可滚动组件简介

SingleChildScrollView

SingleChildScrollView类似于Android中的ScrollView,它只能接收一个子组件 通常SingleChildScrollView只应在期望的内容不会超过屏幕太多时使用,这是因为SingleChildScrollView不支持基于 Sliver 的延迟加载模型,所以如果预计视口可能包含超出屏幕尺寸太多的内容时,那么使用SingleChildScrollView将会非常昂贵(性能差),此时应该使用一些支持Sliver延迟加载的可滚动组件,如ListView

SingleChildScrollView(
            child: Column(
              children: [
                RadioListTile<BoxFit>(
                    value: BoxFit.contain,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.contain"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.contain;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fill,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fill"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fill;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.cover,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.cover"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.cover;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fitHeight,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fitHeight"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fitHeight;
                      });
                    }),
                RadioListTile<BoxFit>(
                    value: BoxFit.fitWidth,
                    groupValue: _selectedBoxFit,
                    title: const Text("BoxFit.fitWidth"),
                    onChanged: (BoxFit? state) {
                      setState(() {
                        _selectedBoxFit = state ?? BoxFit.fitWidth;
                      });
                    })
              ],
            ),
          )

ListView

默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder 按需动态构建列表项。

注意:虽然这种方式将所有children一次性传递给 ListView,但子组件)仍然是在需要时才会加载(build(如有)、布局、绘制),也就是说通过默认构造函数构建的 ListView 也是基于 Sliver 的列表懒加载模型。

    Scaffold(
        appBar: AppBar(
          backgroundColor: color,
          title: Text('测试 $title Widget'),
        ),
        body: Container(
          alignment: Alignment.center,
          color: color,
          child: _list.isEmpty
              ? const CircularProgressIndicator()
              : ListView(
                  children: _list
                      .map((e) => ListTile(
                            title: Text(e.id.toString()),
                          ))
                      .toList(),
                ),
        ));

ListView.builder

`ListView.builder`适合列表项比较多或者列表项不确定的情况
-   `itemBuilder`:它是列表项的构建器,类型为`IndexedWidgetBuilder`
,返回值为一个widget。当列表滚动到具体的`index`位置时,会调用该构建器构建列表项。
-   `itemCount`:列表项的数量,如果为`null`,则为无限列表。

     return Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.white,
          title: Text('测试 $title Widget'),
        ),
        floatingActionButton: FloatingActionButton(
          child: Text("add"),
          onPressed: () {
            setState(() {
              _list.addAll(Colors.accents
                  .map((e) => TestCard(e, id: Colors.accents.indexOf(e)))
                  .toList());
            });
          },
        ),
        body: Container(
            alignment: Alignment.center,
            color: Colors.white,
            child: _list.isEmpty
                ? const CircularProgressIndicator()
                : ListView.builder(
                    itemBuilder: (context, index) {
                      return Container(
                        color: _list[index].color,
                        child: ListTile(
                          title: Text(_list[index].id.toString()),
                        ),
                      );
                    },
                    itemCount: _list.length)));
## ListView.separated
`ListView.separated`可以在生成的列表项之间添加一个分割组件
    return Scaffold(
        appBar: AppBar(
          backgroundColor: Colors.white,
          title: Text('测试 $title Widget'),
        ),
        floatingActionButton: FloatingActionButton(
          child: Text("add"),
          onPressed: () {
            setState(() {
              _list.addAll(Colors.accents
                  .map((e) => TestCard(e, id: Colors.accents.indexOf(e)))
                  .toList());
            });
          },
        ),
        body: Container(
            alignment: Alignment.center,
            color: Colors.white,
            child: _list.isEmpty
                ? const CircularProgressIndicator()
                : ListView.separated(
                    itemBuilder: (context, index) {
                      return Container(
                        color: _list[index].color,
                        child: ListTile(
                          title: Text(_list[index].id.toString()),
                        ),
                      );
                    },
                    separatorBuilder: (context, index) {
                      return SizedBox(height: 12);
                    },
                    itemCount: _list.length)));

参考地址:book.flutterchina.club/