Flutter 布局

193 阅读7分钟

Flutter 中有两种布局模型:

  • 基于 RenderBox 的盒模型布局。
  • 基于 Sliver ( RenderSliver ) 按需加载列表布局。

布局流程基本如下:

  1. 上层组件向下层组件传递约束(constraints)条件。
  2. 下层组件确定自己的大小,然后告诉上层组件。**任何时候子组件都必须先遵守父组件的约束**
  3. 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)

约束布局

ContrainedBox
// constraints: const BoxConstraints(minHeight: 100, maxHeight: 200, minWidth: 10, maxWidth: 100),

//严格约束,最大宽高最小宽高相同固定宽高
// constraints: BoxConstraints.tightFor(width: 100, height: 100),

//松散约束,限制最大宽高
// constraints: BoxConstraints.loose(const Size(100, 10)), 
 
//限制最大宽高
//constraints: BoxConstraints.expand(width: 100, height: 100), 

//无限制
// constraints: BoxConstraints.expand(width: 200), 

ConstrainedBox(
  constraints: BoxConstraints.tight(const Size(100, 100)),
  child: Container(
    color: ColorsRedcolor
    width: 5,
    height: 10,
  ),
);
SizeBox

SizedBox用于给子元素指定固定的宽高,传递给子组件的是强约束,其本身也是对 ConstrainedBox 的定制,

SizedBox(
  width: 10,
  height: 30,
  child: Container(
    color: Colors.red,
    width: 100,
    height: 200,
 ),
)
UnconstrainedBox

由于任何时候子组件都必须遵守其父组件的约束,所以当有多重约束时,最小值取大值,最大值取小值。 当有多重约束时,可能需要用 UnConstrained 去除父类约束。

ConstrainedBox(

  constraints: const BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
  
  //“去除”父级限制
  child: UnconstrainedBox(
     child: ConstrainedBox(
       constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), //子
       child: Container(color: Colors.red),
     ),
  ),
)

线性布局

RowColumn都继承自Flex,并且属性相似。

RowColumn都只会在主轴方向占用尽可能大的空间,而纵轴的长度则取决于他们最大子元素的长度。

Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的RowColumn会占用尽可能大的空间,里面RowColumn所占用的空间为实际大小。

Row/Column
 Row(
    //主轴尺寸,max 表最大宽度,min 子组件宽度和
    //但当Flex 本身受到强约束时,该属性无效
    mainAxisSize: MainAxisSize.max, 
    
    //主轴方向子组件对齐方式
    mainAxisAlignment: MainAxisAlignment.start, 

    //纵轴方向子组件对齐方式
    crossAxisAlignment: CrossAxisAlignment.center, 
    
    children: [

        Text( " hello world ",

          style: TextStyle(fontSize: 30.0, backgroundColor: Colors.red)),

       Text(" I am Jack "),

    ],

)

  • 主轴对齐方式:MainAxisAlignment
  enum MainAxisAlignment { 
    start, // 主轴方向起点 (默认) 
    end, // 主轴方向终点
    center, // 主轴方向居中 
    spaceBetween, //主轴方向,首尾组件分居两侧,其余组件均分空间
    spaceAround, //主轴方向,中间组件均分剩余空间,首尾组件只占一半 
    spaceEvenly, //主轴方向,均分剩余空间
  }

image.png

  • 交叉轴方向:crossAxisAlignment
enum CrossAxisAlignment {
  start, // 交叉轴方向起点 
  end, // 交叉轴方向终点 
  center, // 交叉轴方向中心 (默认)
  stretch, // ☆ 交叉轴方向为子级施加强约束
  baseline, // 交叉轴方向按基线对齐
}

image.png

弹性布局 EXPanded

弹性布局允许子组件按照一定比例来分配父容器空间,Flutter 中的弹性布局主要通过FlexExpanded来配合实现。

Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用RowColumn会方便一些,因为RowColumn都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用RowColumn

Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为  RowColumn 都继承自 Flex,所以 Expanded 也可以作为它们的孩子。

//`flex`参数为弹性系数,如果为 0`null`,则`child`是没有弹性的,即不会被扩伸占用的空间。
//如果大于0,所有的`Expanded`按照其 flex 的比例来分割主轴的全部空闲空间

const Expanded({
  int flex = 1, 
  required Widget child,
})

其中Spacer(flex: 2),Expanded的封装类

Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    
    Expanded(flex: 5,child: _stackView(entity)),
   
    SizedBox(width: 10),
   
    Spacer(flex: 2),
   
    Expanded(flex: 2,child: _imageView(entity.photo as String))
 ],
)

流式布局 Wrap

流式布局分为WrapFlow,但后者过于复杂,使用场景较少,这里只展示前者。

Wrap(

  direction:Axis.horizontal,//主轴方向 默认横向
  
  alignment: WrapAlignment.start, //主轴对方式

  crossAxisAlignment:WrapCrossAlignment.start,//辅轴对齐方式
  
  spacing: 8, //主轴方向间距
  
  runSpacing: 2, //纵轴方向间距
  
  
  children: const [

    Chip(label: Text("A"),
         avatar: CircleAvatar(backgroundColor: Colors.blue, 
           child: Text("Aaa")),
     ),
     Chip(label: Text("A"),
         avatar: CircleAvatar(backgroundColor: Colors.blue, 
           child: Text("Aaa")),
     ),
     Chip(label: Text("A"),
         avatar: CircleAvatar(backgroundColor: Colors.blue, 
           child: Text("Aaa")),
     ),
     Chip(label: Text("A"),
         avatar: CircleAvatar(backgroundColor: Colors.blue, 
           child: Text("Aaa")),
     ),
  ],
)

  • 间距 spacingrunspacing

image.png

  • 主轴方向的对齐模式alignment
enum WrapAlignment { 
  start, // 主轴方向起点 (默认) 
  end, // 主轴方向终点
  center, // 主轴方向居中 
  spaceBetween, //主轴方向,首尾组件分居两侧,其余组件均分空间 
  spaceAround, //主轴方向,中间组件均分剩余空间,首尾组件只占一半 
  spaceEvenly, //主轴方向,均分剩余空间 
}

image.png

  • 交叉轴方向:crossAxisAlignment 和 runAlignment
enum WrapCrossAlignment { 
  start, //交叉轴方向起点 (默认)
  end, //交叉轴方向终点 
  center, //交叉轴方向中心 
}

image.png

  • 在 Wrap 区域中交叉轴方向的对齐方式,类型也是 WrapAlignment 枚举.

image.png

层叠布局 Stack、Positioned

相当于iOS/Android 中的Frame 布局通过确定位置来布局。

Position 只能用于 Stack 中,并且被 Positioned 嵌套的组件,可以无视 Stack 组件的约束。另外Positioned 还可以为子组件施加宽高进行强约束,其自身约束失效。

Stack(

   alignment: Alignment.topLeft, //指定未定位或部分定位widget的对齐方式

   fit: StackFit.loose, //没有定位的子组件如何去适应Stack的大小

   children: [

     Positioned(
        top: 0,
        left: 0,
        right: 0,
        child: Text(entity.title ?? " ",
           style: boldStyle,
           maxLines: 2, 
           overflow: TextOverflow.ellipsis),
     ),

   Positioned(
      bottom: 0,
      left: 0,
      child: Text(entity.createTime(), style: normalStyle),
   ),

    Positioned(
       right: 20,
       bottom: 0,
       child: Text(entity.clicked ?? "0", style: normalStyle),
    ),

   Positioned(
      right: 0,
      bottom: 0,
      child: Image.asset("assets/images/news/news_list_clicked@2x.png",
          width: 14,
          height: 14,
       ),
   ),

  ],
)
  • Alignment本身是一个类,可以进行构造 左上角是 (-1.0, -1.0) ,右下角是 (1.0, 1.0) 的坐标系,通过坐标来决定对齐的位置。 image.png
static const Alignment center = Alignment(0.0, 0.0); 
static const Alignment topLeft = Alignment(-1.0, -1.0);
static const Alignment bottomRight = Alignment(1.0, 1.0);
  • fit 属性可以对子级的约束情况进行设置
enum StackFit { 
    loose, 
    expand, 
    passthrough, 
}
[1]: loose 下, 宽高尽可能 [放松约束][2]: expand 下, 施加 [强约束],约束尺寸为自身受到约束的 [最大尺寸][3]: passthrough 下, 仅 [传递约束],把自身受到的约束原封不动的施加给子级。

相对定位 Align / Center

Center继承自Align,它比Align只少了一个alignment 参数;由于Align的构造函数中alignment 值为Alignment.center,所以我们可以认为Center组件其实是对齐方式确定(Alignment.center)的Align

//`FlutterLogo`的宽高为 60,则`Align`的最终宽高都为`2*60=120`
Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment.topRight,
  child: FlutterLogo(
    size: 60,
  ),
),

widthFactorheightFactornull时组件的宽高将会占用尽可能多的空间

DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    child: Text("xxx"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
  child: Center(
    widthFactor: 1,
    heightFactor: 1,
    child: Text("xxx"),
  ),
)

容器类组件

Padding
Padding(
  //上下左右各添加16像素补白
  padding: const EdgeInsets.all(16),
  
  child: Column(
  
     //显式指定对齐方式为左对齐,排除对齐干扰
     crossAxisAlignment: CrossAxisAlignment.start,
    
     mainAxisSize: MainAxisSize.min,
    
     children: const <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"),
          )
     ],
  ),
)

剪裁组件
剪裁Widget默认行为
ClipOval子组件为正方形时剪裁成内贴圆形;为矩形时,剪裁成内贴椭圆
ClipRRect将子组件剪裁为圆角矩形
ClipRect默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)
ClipPath按照自定义的路径剪裁
Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
Center(
   child: Column(
      children: <Widget>[
          avatar, //不剪裁
          
          ClipOval(child: avatar), //剪裁为圆形
          
          ClipRRect( //剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ), 
        ],
      ),
    )
DecoratedBox

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

const DecoratedBox({

  //抽象类
  Decoration decoration,
  
  //foreground background
  DecorationPosition position = DecorationPosition.background,
  
  Widget? child
})

BoxDecoration类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。

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

示例渐变按钮

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

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

其中属性margin的留白是在容器外部,而padding的留白是在容器内部

Container(
  margin: EdgeInsets.all(20.0), //容器外补白
  color: Colors.orange,
  child: Text("Hello world!"),
),
Container(
  padding: EdgeInsets.all(20.0), //容器内补白
  color: Colors.orange,
  child: Text("Hello world!"),
),