阅读 2515

Flutter布局——Flex、FittedBox、Stack、Container布局教程【超详细】

布局

Flex布局

Flutter中的Flex布局和Web的CSS中的Flex布局类似。

在Flutter 中用于控制Flex布局的有Row,Column,Expanded,Flexible,Spacer,Flex这些控件。

row水平布局

属性名 类型 默认值 说明
mainAxisAlignment MainAxisAlignment MainAxisAlignment.start 主轴的排列方式
mainAxisSize MainAxisSize MainAxisSize.max 主轴占空间的大小
crossAxisAlignment CrossAxisAlignment CrossAxisAlignment.center 次轴的排列方式
textDirection TextDirection null 确定children在水平方向的摆放顺序
verticalDirection VerticalDirection VerticalDirection.down 确定children在垂直方向的摆放顺序
textBaseline TextBaseline null 文字基准线对齐

我们首先创建三个大小不一的Container


class LyoutRowDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("水平布局"),
        ),
        body: Container(
          child: Row(
            children: <Widget>[
              Container(
                height: 100,
                width: 50,
                color: Colors.redAccent,
              ),
              Container(
                height: 50,
                width: 50,
                color: Colors.blueAccent,
              ),
              Container(
                color: Colors.black,
                height: 75,
                width: 75,
              )
            ],
          ),
        ));
  }
}
复制代码

主轴排列方式MainAxisAlignment

子元素children的排列方式由这两个属性决定textDirection和verticalDirection。textDirection决定水平方向的排列方式TextDirection.ltr从左往右排列(把左当作起始位置),TextDirection.rtl从右往左排列(把右当作起始位置)。verticalDirection时水平方向的排列方式。当值为down(向下排列)时起始位置在顶部。当值为up(向上排列)是起始位置在底部。

  • start (默认值) 根据textDirection属性排列方向。

    将children放置在主轴的起点

    textDirection属性值为rtl时右图

  • end

    根据textDirection属性排列方向。

    将children放置在主轴的末尾

    textDirection属性值为rtl时右图

  • center

    根据textDirection属性排列方向。

    将children放置在主轴的中心

    textDirection属性值为rtl时右图

  • spaceBetween

    根据textDirection属性排列方向。

    将主轴方向上的空白区域均分,使得children之间的空白区域相等,首尾child都靠近首尾,没有间隙

    textDirection属性值为rtl时右图

  • spaceAround

    根据textDirection属性排列方向。

    将主轴方向上的空白区域均分,使得children之间的空白区域相等,但是首尾空白区域为一半

    textDirection属性值为rtl时右图

  • spaceEvenly

    根据textDirection属性排列方向

    将主轴方向上的空白区域均分,使得children之间的空白区域相等,包括首尾空白区域

    textDirection属性值为rtl时右图

交叉轴的排列方式crossAxisAlignment

都以 mainAxisAlignment: MainAxisAlignment.start为例

  • start

    将子元素在交叉轴上起点对齐。设置verticalDirection为VerticalDirection.up右图。

  • end

    将子元素在交叉轴上末尾对齐。设置verticalDirection为VerticalDirection.up右图。

  • center

    将子元素在交叉轴上居中对齐。设置verticalDirection无改变。

  • strentch

    将子元素在交叉轴上拉伸

  • baseline

    基准线对其适用于文字,我们首先创建文字的start。使用baseline。必须设置textBaseline属性。不同文字的文字基准线。主要是字母文字(如:英语)和表意文字(如:汉语)的基准线是不同的。必须告诉Flutter你使用的文字。

    Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      textBaseline: TextBaseline.alphabetic,
      children: [
        Text(
          'Flutter',
          style: TextStyle(
            color: Colors.yellow,
            fontSize: 30.0
          ),
        ),
        Text(
          'Flutter',
          style: TextStyle(
              color: Colors.blue,
              fontSize: 20.0
          ),
        ),
      ],
    );
    复制代码

设置对齐方式为基准线

主轴占用的空间mainAxisSize

mainAxisSize只有两个值一个是min一个是max。默认是max。

当值为min时候。主轴缩紧,变为最小。

如图:即使这时mainAxisAlignment:为MainAxisAlignment.spaceAround。主轴的长度仍为最小状态

Column垂直布局

垂直布局与水平布局的属性和方法一致,唯一需要注意的是textDirection(水平排列方式)和verticalDirection(垂直排列方式)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: "垂直布局",
        home: Scaffold(
            appBar: AppBar(title: Text("垂直布局")),
            body: Center(
              child: Column(
			   mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.end,
                children: <Widget>[
                  Text("1",style: TextStyle(fontSize: 100),),
                  Expanded(
                    child: Text("2"),
                  ),
                  Text("33333"),
                  Text("4")
                ],
              ),
            )));
  }
}

复制代码

为了便于观看我们将1扩大

Flex布局

我们查看以下源码便于理解

可以看到RowColumn都继承与Flex

class Row extends Flex 
class Column extends Flex   
复制代码

Row的构造函数可选命名参数(即{}包裹的参数)有8个。

传入父级的super构造函数却有9个,多出了direction: Axis.horizontal

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>[],
  }) : super(
    children: children,
    key: key,
    direction: Axis.horizontal,
    mainAxisAlignment: mainAxisAlignment,
    mainAxisSize: mainAxisSize,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: textDirection,
    verticalDirection: verticalDirection,
    textBaseline: textBaseline,
  );
}
复制代码

再Flex中属性direction实际上时主轴的方向。且被@required标注。它是必选的。

Flex({
    Key key,
    @required this.direction,
    this.mainAxisAlignment = MainAxisAlignment.start,
    this.mainAxisSize = MainAxisSize.max,
    this.crossAxisAlignment = CrossAxisAlignment.center,
    this.textDirection,
    this.verticalDirection = VerticalDirection.down,
    this.textBaseline,
    List<Widget> children = const <Widget>[],
  }) : assert(direction != null),
       assert(mainAxisAlignment != null),
       assert(mainAxisSize != null),
       assert(crossAxisAlignment != null),
       assert(verticalDirection != null),
       assert(crossAxisAlignment != CrossAxisAlignment.baseline || textBaseline != null),
       super(key: key, children: children);
复制代码

Expanded

上面的例子我们遇到从未见过的一个widget。很容易就可以看出。Expanded会忽略子元素的大小并强制自动扩展使主轴填充父级可用的空白区域。Expanded必须是Row、Column、Flex的children。

下面两个例子便于更好理解:

//当子元素只有一个Expanded时
class LyoutExpanded extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Expanded"),
      ),
      body: Center(
        child: Container(
          child: Column(
            children: <Widget>[
              Expanded(
                child: Container(
                  color: Colors.blue,
                  width: 100,//这个被忽略掉了
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

复制代码

class LyoutExpanded extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Expanded"),
      ),
      body: Center(
        child: Container(
          height: 50,//填充使主轴扩展至父级高度
          child: Column(
            children: <Widget>[
           Expanded(
                child: Container(
                  color: Colors.blue,
                  height: 100,//
                  width: 100,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码

Expanden还有一个属性为flex默认值为1。代表权重。当子元素有多个Expanden。按照权重值占据父级的高度(row时为水平宽度)

<Widget>[
    Expanded(
        flex: 2,
        child: Container(
            color: Colors.blue,
            width: 100,
        ),
    ),
    Expanded(
        child: Container(
            color: Colors.red,
            width: 100,
        ),
    ),
    Expanded(
        child: Container(
            color: Colors.yellow,
            width: 100,
        ),
    ),
],
复制代码

我们可以看到蓝色的块时红和黄的二倍。

Flexible

Flexible组件可以使Row、Column、Flex等子组件在主轴方向有填充可用空间的能力(例如,Row在水平方向,Column在垂直方向),但是它与Expanded组件不同,它不强制子组件填充可用空间。同样Flexible组件必须是Row、Column、Flex等组件的children。

Flexiible 有属性fit当属性值FlexFit.tight时。Flexible和Expanded没有区别。

从源码可以看出Expanded继承与Flexible

且引用 上级构造函数传入fit的值为FlexFit.tight。

class Expanded extends Flexible
//省区其他源码
    

const Expanded({
    Key key,
    int flex = 1,
    @required Widget child,
  }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}
    
复制代码

在Column的children中传入。属性fit:为FlexFit.tight,按照权值填充空白区域。

<Widget>[
    Flexible(
        fit: FlexFit.tight,
        child: Container(
            color: Colors.blue,
            height: 100,
            width: 100,
        ),
    ),
    Flexible(
        fit: FlexFit.tight,
        flex: 2,
        child: Container(
            color: Colors.yellow,
            height: 100,
            width: 100,
        ),
    ),
],
复制代码

一旦fit的值为FlexFit.loose(默认值)先按flex的值分主轴确定占主轴的大小,再按child调整元素的高度。不强制拉伸

例如第一个为tight强制扩展。第二个为loose不强制扩展。

<Widget>[
    Flexible(
        fit: FlexFit.tight,
        child: Container(
            color: Colors.blue,
            height: 100,
            width: 100,
        ),
    ),
    Flexible(
        fit: FlexFit.loose,
        flex: 2,
        child: Container(
            color: Colors.yellow,
            height: 100,
            width: 100,
        ),
    ),
],
复制代码

因为第二个为loose所以黄颜色的Container高度为100。而蓝色Container的 fit值为 tight,他的大小和上一个例子一致。

Spacer

Spacer创建一个可以调整的空白区域,可用于调整Flex容器(Row或者Colum)中widget之间的间距。

一旦children里面包含了Spacer。mainAxisAlignment的属性值将起不到作用。Spacer已经占据了所有额外的空间,因此没有剩余的空间可以重新分配。

Row(
    mainAxisAlignment: MainAxisAlignment.end,//起不到作用
    children: <Widget>[
        Container(
            height: 100,
            width: 50,
            color: Colors.redAccent,
        ),
        Spacer(),
        Container(
            height: 50,
            width: 50,
            color: Colors.blueAccent,
        ),
        Container(
            color: Colors.black,
            height: 75,
            width: 75,
        )
    ],
),
复制代码

缩放布局

绝大多数flutter的widget是盒子。可以将他们叠放,堆叠,嵌套。

我们可以层级嵌套盒子,但是如果一个盒子不适合(大小不适合)另一个盒子。该如何解决?

为了解决这个问题Flutter提供了FittedBox,这个和移动端的ImageView类似。

它实现的功能是使子元素缩放(fit)或者调整位置(alignment)

  • BoxFit.contain(默认值)等比例扩大或缩小,但内容不会超过容器范围

  • BoxFit.cover按照比例逐步扩大至充满容器,内容有能会超过容器范围。当child比例与容器不同时,要么高度溢出容器,要么宽度溢出容器。

  • BoxFit.fill不保留比例强制拉伸(缩小)填充容器。

  • BoxFit.fitHeight保持比例确保高度在容器中显示完整。

  • BoxFit.fitWidth保持比例确保宽度在容器中显示完整。

  • BoxFit.none将child对齐在目标框内(默认剧中),并丢弃位于框外的部分,源文件不放大也不缩小。

  • BoxFit.scaleDown将child对齐在目标框内(默认剧中),当child大于容器,则与contain一致。如果child小于容器,则与none一致

为了便于理解我们可以找一个图片进行测试

class Lyoutfitdemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("data")),
      body: Container(
        height: 250,
        width: 250,
        color: Colors.indigo[200],
        child: FittedBox(
          child: Image.asset('images/fittedbox.png')),
      ),
    );
  }
}
复制代码
  1. BoxFit.contain

  1. BoxFit.cover

  1. BoxFit.fill

  1. BoxFit.fitHeight

5.BoxFit.fitWidth

  1. BoxFit.none

  1. BoxFit.scaleDown

备注:不用创建FittedBox,Image含有属性fit。值效果和FittedBox一致。

堆叠布局

Stack与web绝对定位布局模型类似。Stack不是按行或者列来布局的,而是按照特定顺序和位置堆叠。可以使用 PositionedAlign作为Stack的的定位。

示例:使用层叠布局实现一个 图片渐变效果

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "渐变状态栏",
      home: Scaffold(
        body: Container(
          height: 400,
          child: Stack(
            fit: StackFit.expand,
            children: <Widget>[
              Image.asset(
                'images/Stack.png',
                fit: BoxFit.cover,
              ),
          
              const DecoratedBox(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment(0.0, -1.0),
                    end: Alignment(0.0, -0.4),
                    colors: <Color>[Color(0x90000000), Color(0x00000000)],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
复制代码

图片渐变效果

这是两个控件堆叠在一起的例子。当然子部件可以出现在父级内的任何位置。通过两种widget实现对位置的控制。

Align布局

Align为对齐部件,设置child的在父级的(如container、stack等)对齐方式,并根据child的尺寸调整自身的尺寸。

本文的父级容器选用stack

Alignment

其中有这几种属性

topLeft(左上),topCenter(顶部中央),topRight(右上),centerLeft,center,centerRight,bottomLeft,bottomCenter,bottomRight

class LayoutAlignDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("align部件"),
        ),
        body: Stack(
          children: <Widget>[
            Align(
              alignment: Alignment.topLeft,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
          ],
        ));
  }
}
复制代码

Alignment.lerp(Alignment a, Alignment b, double t)

lerp方法有三个参数,前两个参数为Alignment类型,第三个参数为小数。

当t为0时,这个方法返回的值为a。

当t为1时,返回b的值。

当t为0.5时,这个widget位置就位于a和b指定的位置中间。

所以这个t为一个偏移量。指定为a到b之间的偏移。

//在Stack children中添加一个align
Align(
  alignment: Alignment.lerp(Alignment.bottomCenter, Alignment.center,0.5),//位于Alignment.bottomCenter和Alignment.center的中央
  child: Container(
  width: 100,
  height: 100,
  color: Colors.yellow,
   ),
),
复制代码

偏移量对齐

上面介绍了相对于对其方式a和b的偏移对其。

FractionalOffset 是另外一个偏移方法,它是相对于父部件左上角的偏移。

创建偏移量FractionalOffset (dx,dy)。dx和dy的取值都是0~1。左上的位置为dx和dy都为0。

Align(
  alignment: FractionalOffset(0, 0.5),
  child: Container(
    width: 100,
    height: 100,
    color: Colors.red,
  ),
),
复制代码

另外在源码中:

class FractionalOffset extends Alignment
复制代码

FractionalOffset继承于Alignment所以Alignment的属性都可以使用,这样我们要使得widget位于左上也可以用使用FractionalOffset.topLeft


class LayoutAlignDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("align部件"),
        ),
        body: Stack(
          children: <Widget>[
            Align(
              alignment: FractionalOffset(0, 0),
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
            Align(
              alignment: FractionalOffset(0, 0.5),
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
            Align(
              alignment: FractionalOffset(0, 1),
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ),
            ),
            Align(
              alignment: Alignment.lerp(Alignment.bottomCenter, Alignment.center,0.5),
              child: Container(
                width: 100,
                height: 100,
                color: Colors.yellow,
              ),
            ),
            Align(
              alignment: FractionalOffset.topRight,
              child: Container(
                width: 100,
                height: 100,
                color: Colors.yellow,
              ),
            ),
          ],
        ));
  }
}

复制代码

与之相类似的还有Alignment(dx, dy),以父级容器的中心为坐标系原点。dx的取值范围为-1~1,dy的取值范围为-1~1。设置子元素的位置。

Positioned

Positioned 部件可以控制Stack中子元素的位置。Positioned 与Align不同的是Position必须是Stack的Children。

Position有topbottomleftrightheightwidth

topbottomleftright这些量都是子元素边界与父级某一边界的距离。

也就是说当一个高度和长度固定容器,一旦我们确定left或right的一个量和bottom或top的一个量,其位置就可以确定。

部分代码

Stack(
    children: <Widget>[
        Positioned(
            top: 100,
            right: 100,
            width: 100,
            height: 100,
            child: Container(
                color: Colors.red,
            ),
        ),
    ],
)
复制代码

源码分析

 const Positioned({
    Key key,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    @required Widget child,
  }) : assert(left == null || right == null || width == null),
       assert(top == null || bottom == null || height == null),
       super(key: key, child: child);
复制代码

在水平方向上如果assert(false)会抛出错误

而括号里的值为left == null || right == null || width == null

left ,right ,width 至少有一个值为null这个程序才不会报错。

也就是说当width未指定(为空)使left和right是可以同时存在的。这时的容器的宽度会以据边界的距离(left和right)自动调整。

Stack(
    children: <Widget>[
        Positioned(
            top: 100,
            right: 100,
            left: 100,
            // width: 100,
            height: 100,
            child: Container(
                color: Colors.red,
            ),
        ),
    ],
)
复制代码

IndexedStack

IndexedStack继承于Stack

class IndexedStack extends Stack
复制代码

他和Stack不同的是有一个index的属性,表示只显示第几个元素。

IndexedStack(
    index: 1,//只显示第二个
    children: <Widget>[
        Positioned(
            top: 100,
            right: 100,
            left: 100,
            height: 100,
            child: Container(
                color: Colors.red,
            ),
        ),  
        Positioned(
            top: 500,
            right: 100,
            left: 100,
            height: 100,
            child: Container(
                color: Colors.red,
            ),
        ),
    ],
)
复制代码

容器布局

Container可以创建一个矩形元素。可以用BoxDecoration进行装饰。背景,边框,阴影。

class LyoutContainerdemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Container容器"),
      ),
      body: Container(
        height: 200,
        width: 200,
        child: Text("这是一个Container容器"),
        decoration: BoxDecoration(
          color: Colors.red[200],
          // shape: BoxShape.circle, //形状
          border: Border.all(width: 10.0),
          boxShadow: [
            BoxShadow(
              offset: Offset(0.0, 100.0), //模糊偏移量
              color: Color.fromRGBO(16, 20, 188, 1.0), //颜色
              blurRadius: 10, //模糊
              spreadRadius: -9.0, //在应用模糊之前阴影的膨胀量
            ),
          ],
        ),
      ),
    );
  }
}

复制代码

为了演示功能,这个图片效果做的比较夸张。

附上一个好看的效果

相似于Css的盒子布局,我们也可以通过Margin给盒子设置外边距。

Container(
    margin: EdgeInsets.all(50.0),//外边距50.0
    height: 200,
    width: 200,
    decoration: BoxDecoration(
        color: Colors.red[600],
        border: Border.all(width: 2.0),
        boxShadow: [
            BoxShadow(
                offset: Offset(2.0, 9.0), //偏移量
                color: Colors.red[200], //颜色
                blurRadius: 10, //模糊
                spreadRadius: -1.0, //在应用模糊之前阴影的膨胀量
            ),
        ],
    ),
),
复制代码

Padding布局

用于处理容器与子元素之间的距离。与Padding对应的是margin属性。margin用于处理与其他组件之间的距离。Padding部件和容器内的pading属性的效果实际上是一致的,当同时出现,真实的padding将是两者相加。

class LayoutPaddingDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Padding 布局"),
      ),
      body: Container(
        padding: EdgeInsets.all(20.0),//#1这两处的属性效果是一致的
        decoration: BoxDecoration(
            color: Colors.yellow,
            border: Border.all(color: Colors.white, width: 8.0)),//白色边框
        child: Padding(
          child: Container(
            decoration: BoxDecoration(
            color: Colors.red,
            border: Border.all(color: Colors.white, width: 8.0)),//白色边框
          ),
          padding: EdgeInsets.all(10.0),//#1这两处的属性效果是一致的
        ),
      ),
    );
  }
}

复制代码

写在后面

Flutter中涉及到布局的Widget有30多种,一样的效果的ui,实现的途径有很多中。本篇就重点涉及几个常用的部件。

参考链接

youtu.be/T4Uehk3_wlY

medium.com/jlouage/flu…

api.flutter.dev/index.html

flutter.dev/docs

关注下面的标签,发现更多相似文章
评论