Flutter知识梳理 布局篇《一》

1,319 阅读7分钟

好记性不如烂笔头,缘由看了小册就忘,本篇属于笔记整理加自我思考,详情请购买大佬小册 Flutter 布局探索 - 薪火相传

布局中的紧约束

有一个问题困扰我许久,在Flutter中使用Stack定位组件,使用Positioned 的左右定位属性设置为 0,使Positioned 的孩子居中,但是Positionedchild,会被强行拉伸填满左右,自身设置的宽高变的不起作用。每次我只需要套个Center组件就能使其动态居中,我想Center组件还蛮神奇在定位里也能实现居中。后来发现套个 UnconstrainedBox组件也可以实现居中,嗯???


Stack(children:[
Positioned(
  left: 0,
  right: 0,
  child:UnconstrainedBox(
    child: Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(5.w),
        color: cardColors[index],
      ),
      padding: EdgeInsets.symmetric(vertical: 5.w, horizontal: 10.w),
      child: Text("工作",style: const TextStyle(color: Colors.white,fontSize: 16),),
    ),
  ),
]);

紧约束

什么是紧约束?

紧约束(tight constraints)是指对Widget进行布局时,父Widget给子Widget施加的大小限制。每个Widget在布局时都会遵循紧约束原则,即Widget必须完全适应父Widget给予的约束条件。

常见的有关约束组件的包括:

  1. BoxConstraints BoxConstraints定义了一个Widget可以渲染的最大和最小大小。每个Widget在布局时,Flutter会根据父Widget的BoxConstraints计算出子Widget的大小。如果没有指定任何约束,则Widget将尽可能变大。

  2. ConstrainedBox ConstrainedBox是一个Widget,它可以给子Widget添加额外的约束条件。通过它可以限制子Widget的最大最小宽高。

  3. UnconstrainedBox UnconstrainedBox的作用是暂时移除对子Widget施加的约束,让子Widget可以根据自身需求设置尺寸。

  4. AlignCenterPadding 这些Widget在布局时会改变子Widget的约束条件,使之居中对齐或添加间距等。

想要我的儿子听我话(约束),必须打破我父亲对我的紧约束,不然我的儿子只听我父亲的话(约束),不听我的。

约束对象 BoxConstraints

紧约束 本质是 BoxConstraints 类中的 tight 构造。通过 BoxConstraints 约束可以设置宽高的取值区间。 在布局时 BoxConstraints对象通常由父widget传递给子widget,用于控制子widget的大小和布局行为。子widget需要根据这些约束来调整自身尺寸,确保符合父widget的要求。

BoxConstraints是一个不可变的对象,用于表示父widget对子widget施加的约束条件。它主要包含以下属性:
  1. maxWidthmaxHeight: 子widget的最大宽度和高度限制。

  2. minWidthminHeight: 子widget的最小宽度和高度限制。

  3. hasBoundedWidthhasBoundedHeight: 布尔值,表示是否有确定的最大宽度或高度约束。

  4. isTight: 布尔值,如果所有约束都不为无限大(double.infinity),则为true。

  5. isNormalized: 布尔值,如果所有约束都在0到double.infinity范围内,则为true。

BoxConstraints 类的不同构造:

  1. BoxConstraints() 创建一个无约束的 BoxConstraints 对象,即最小尺寸为 0,最大尺寸为无限大。

    BoxConstraints()
    
  2. BoxConstraints.tight(Size size) 创建一个紧致约束,其最小和最大尺寸都等于指定的 size

    
    BoxConstraints.tight(Size(200.0, 100.0))
    
  3. BoxConstraints.loose(Size size) 创建一个宽松约束,其最小尺寸为 0,最大尺寸为指定的 size

    
    Copy code
    BoxConstraints.loose(Size(300.0, 150.0))
    
  4. BoxConstraints.expand() 创建一个无限大约束,即最小尺寸为 0,最大尺寸为无限大。

    
    BoxConstraints.expand()
    
  5. BoxConstraints(double minWidth, double maxWidth, double minHeight, double maxHeight) 使用指定的最小和最大宽高创建一个 BoxConstraints 对象。

    BoxConstraints(minWidth: 100.0, maxWidth: 300.0, minHeight: 50.0, maxHeight: 200.0)
    
  6. BoxConstraints.tightFor(width: double, height: double) 创建一个紧致约束,其最小和最大宽高都等于指定的值。

    BoxConstraints.tightFor(width: 150.0, height: 80.0)
    
  7. BoxConstraints.tightForFinite(width: double, height: double)tightFor 类似,但会确保最小和最大宽高都是有限的。

    BoxConstraints.tightForFinite(width: 200.0, height: 120.0)
    

 LayoutBuilder 组件

 LayoutBuilder 根据渲染时获取到的约束条件,获得到当前区域内的约束信息,动态地构建UI布局。

LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    // 根据constraints返回不同的布局
    if (constraints.maxWidth > 600) {
      return BigLayout();
    } else {
      return SmallLayout();      
    }
  },
)

Flex(指 Row 和 Column 组件) 、Wrap、Stack 组件下的约束

Flex 垂直布局和横向布局 ,Wrap流式布局,Stack定位布局

Flex

Flex 轴向为水平时,不会限制孩子的宽度,(如 Row组件)Flex 轴向为垂直时,不会限制孩子的高度。(如 Column) 属性:

  1. direction: 主轴方向,取值为 Axis.horizontalAxis.vertical

  2. mainAxisAlignment: 主轴方向上的对齐方式,例如 MainAxisAlignment.start, MainAxisAlignment.center 等。

  3. crossAxisAlignment: 交叉轴方向上的对齐方式,例如 CrossAxisAlignment.start, CrossAxisAlignment.center 等。

  4. verticalDirection: 当 directionAxis.vertical 时,子组件在交叉轴方向上的排列顺序。

  5. textDirection: 水平方向的子组件排列顺序。

  6. children: Flex 的子组件列表。

Warp

主要属性:

  1. direction: 主轴方向,默认为Axis.horizontal,即水平排列。设置为Axis.vertical可以垂直排列。

  2. alignment: 主轴方向上的对齐方式。

  3. spacing: 主轴方向上相邻子组件之间的间距。

  4. runAlignment: 交叉轴方向上每行子组件的对齐方式。

  5. runSpacing: 交叉轴方向上相邻行之间的间距。

  6. textDirection: 水平排列时,文本方向。

  7. verticalDirection: 垂直排列时,每行中子组件的垂直顺序。

  8. clipBehavior: 裁剪行为,默认根据需要裁剪。

Wrap 施加约束的特点: 在 [主轴] 方向上 [放松约束],在 [交叉轴] 方向上 [无限约束]。

Stack

  • alignment:子组件在Stack布局中的对齐方式,如中心对齐、左对齐等。

  • overflow: 定义子组件超出Stack布局范围时的处理方式,如裁剪或不受限制。

  • children: Stack布局的子组件列表,按顺序从底到顶叠加。所受到的约束都是一致的。

  • fit: Stack组件的大小如何适应其子组件的大小,有四种模式可选。

StackFit.loose Stack 的大小足够容纳所有子组件,并且没有额外的空间。这是默认值。

StackFit.expand 将尽可能扩大以填充父级提供的约束空间,同时仍然包含所有子组件。如果子组件超出了父级约束,则会裁剪子组件。

StackFit.passthrough Stack 的大小与父级提供的约束空间完全一致,忽略所有子组件的大小。这可能会导致子组件被裁剪。

StackFit.overflow Stack 的大小由最大的子组件决定。如果子组件比父级约束更大,则 Stack 也会超出父级提供的约束空间。

Flexible 组件

确保行或列内的子Widget按比例占用剩余空间。

主要属性:

  1. fit: 控制子Widget如何适应Flexible本身的约束空间。 可选值

    • tight会尽量缩小至Widget以满足约束。
    • loose会尽量扩大子Widget填充剩余空间。
    • unbounded会完全忽略Flexible的约束,让子Widget自身决定大小。
  2. flex: 一个int值,用于确定这个Flexible相对于其它Flexible的flex比例。值越大,占用剩余空间的比例就越大。

Expanded 组件

Expanded 组件的等价于 FlexFit.tight 时的 Flexible 组件, Spacer组件占据剩余空间,是通过封装的Expanded实现的

布局案例

思路 :简简单单使用figma 画个图,仿造一下掘金主页布局。马卡龙配色吼吼!!

image.png

代码实现

整体是一个垂直布局,中间部分为纵向布局。由于我想固定每个小模块的比例所以简单写了设计图的值转换,能够在窗口大小改变保证模块的比例不变。

class SizeUtils {

 static double w(double w, double screenW) {
   return (w * screenW) / meter.width;
 }
 static double h( h, double screenH) {
   return (h * screenH) / meter.height;
 }
}

布局代码

import 'package:flutter/material.dart';
import '../../utils/size_util.dart';

class LayoutPage extends StatefulWidget {
  final BoxConstraints constraints;

  const LayoutPage({Key? key, required this.constraints}) : super(key: key);

  @override
  State<LayoutPage> createState() => _LayoutPageState();
}

class _LayoutPageState extends State<LayoutPage> {
  @override
  Widget build(BuildContext context) {
    double _width = widget.constraints.maxWidth;
    double _height = widget.constraints.maxHeight;

    return Container(
        //由子填充
        color: const Color(0xffB8FFCC),
        child: Column(
          children: [
            Container(
              width: double.infinity,
              height: SizeUtils.h(93, _height),
              color: const Color(0xff50EBCF),
            ),
            Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  width: SizeUtils.w(304, _width),
                  height:SizeUtils.h(670, _height),
                  color: const Color(0xffFFDCEE),
                ),
                SizedBox(width:SizeUtils.w(30, _width) ,),
                SizedBox(
                  height: SizeUtils.h(800, _height),
                  width: SizeUtils.w(600, _width),
                  child: ListView.builder(
                    itemBuilder: (context, _) {
                      return Container(
                        margin: EdgeInsets.only(top: SizeUtils.h(20, _height)),
                        color: const Color(0xffffe7b6),
                        width: double.infinity,
                        height: SizeUtils.h(60, _height),
                      );
                    },
                    itemCount: 30,
                  ),
                ),
                SizedBox(width:SizeUtils.w(60, _width) ,),
                Column(
                  children: [
                    Container(
                      height: SizeUtils.h(139, _height),
                      width: SizeUtils.w(349, _width),
                      color: const Color(0xffFFB8C5),
                    ),
                    SizedBox(height:SizeUtils.h(50, _height) ,),
                    Container(
                      height: SizeUtils.h(213, _height),
                      width: SizeUtils.w(349, _width),
                      color: const Color(0xffD2D4FD),
                    ),
                    SizedBox(height:SizeUtils.h(50, _height) ,),
                    Container(
                      height: SizeUtils.h(117, _height),
                      width: SizeUtils.w(349, _width),
                      color: const Color(0xffFAE396),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ));
  }
}

效果

buju.gif