Flutter(四)布局类组件

时间:2022-09-02 00:55:34

布局类组件简介

布局类组件就是指直接或间接继承(包含)SingleChildRenderObjectWidgetMultiChildRenderObjectWidget的Widget

它们一般都会有一个child或children属性用于接收子 Widget。

继承关系 Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget

RenderObjectWidget类中定义了创建、更新RenderObject的方法,子类必须实现他们

RenderObject:渲染UI界面的

布局原理与约束

BoxConstraints 是盒模型布局过程中父渲染对象传递给子渲染对象的约束信息,包含最大宽高信息,子组件大小需要在约束的范围内

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

ConstrainedBox用于对子组件添加额外的约束

Widget redBox = DecoratedBox(
  decoration: BoxDecoration(color: Colors.red),
);


ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //宽度尽可能大
    minHeight: 50.0 //最小高度为50像素
  ),
  child: Container(
    height: 5.0, 
    child: redBox ,
  ),
)

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

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

任何时候子组件都必须遵守其父组件的约束,如果我们需要自定义约束必须通过UnconstrainedBox来 “去除” 父元素的限制

线性布局(Row和Column)

Row和Column都继承自Flex,类似Android 中的LinearLayout

线性布局有主轴和纵轴之分,
如果布局沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向
如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向

Row:水平
Column:垂直

//Row和Column的属性
    Key? key,
    MainAxisAlignment//主轴对齐方式
    //默认MainAxisSize.max:占满宽度,类似match_parent
    //MainAxisSize.min:子组件的宽度总和,类似wrap_content
    MainAxisSize
    CrossAxisAlignment //纵轴对齐方式
    TextDirection //从左往右还是从右往左排列,默认从左往右
    VerticalDirection//纵轴对齐方向
    TextBaseline//对齐文本的水平线
    List<Widget> children = const <Widget>[] //子组件数组

如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间里面Row或Column所占用的空间为实际大小,如果要让里面的Column占满外部Column,可以使用Expanded 组件

弹性布局

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

Flex({
  ...
  required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})
const Expanded({
  int flex = 1, //比例系数,类似LinearLayout中的layout_weight
  required Widget child,
})

流式布局(Wrap、Flow)

Row 和 Colum ,如果子 widget 超出屏幕范围,则会报溢出错误
流式布局含义:超出屏幕自动换行
Flutter中通过Wrap和Flow来支持流式布局

Wrap({
  ...
  spacing//主轴方向子widget的间距
  runSpacing//纵轴方向子widget的间距
  runAlignment//纵轴方向的对齐方式
})

Flow:Flow很少用,因为过于复杂,需要自己实现子 widget 的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景

Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小

层叠布局(Stack、Positioned)

类似Android 中的 Frame,Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位

Stack({
  this.alignment = AlignmentDirectional.topStart,//对齐方式
  this.textDirection,//子组件排列顺序,默认从左往右
  //此参数用于确定没有定位的子组件如何去适应Stack的大小。
  //StackFit.loose表示使用子组件的大小,
  //StackFit.expand表示扩伸到Stack的大小。
  this.fit = StackFit.loose,
  //此属性决定对超出Stack显示空间的部分如何剪裁,
  //Clip枚举类中定义了剪裁的方式,Clip.hardEdge 表示直接剪裁...
  this.clipBehavior = Clip.hardEdge,
  List<Widget> children = const <Widget>[],
})
//left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离
//width和height用于指定需要定位元素的宽度和高度
const Positioned({
  Key? key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  required Widget child,
})

对齐与相对定位(Align)

如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些

Align({
  Key key,
  this.alignment = Alignment.center,//子组件在父组件中的起始位置
  this.widthFactor,//会分别乘以子元素的宽、高,最终的结果就是Align 组件的宽高
  this.heightFactor,//会分别乘以子元素的宽、高,最终的结果就是Align 组件的宽高
  Widget child,
})
//因为FlutterLogo的宽高为 60,则Align的最终宽高都为2*60=120。
Align(
  widthFactor: 2,
  heightFactor: 2,
  alignment: Alignment.topRight,
  child: FlutterLogo(
    size: 60,
  ),
),

Alignment可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:

(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)

FractionalOffset 继承自 Alignment,它和 Alignment唯一的区别就是坐标原点不同!FractionalOffset 的坐标原点为矩形的左侧顶点,FractionalOffset的坐标转换公式为:

实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

Align和Stack对比

Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点;而Align则需要先通过alignment 参数来确定坐标原点.
Stack可以有多个子元素,并且子元素可以堆叠,而Align只能有一个子元素,不存在堆叠

Center组件

Center继承自Align,它比Align只少了一个alignment 参数

Center(
    widthFactor: 1,
    heightFactor: 1,
    child: Text("xxx"),
  ),

LayoutBuilder、AfterLayout

LayoutBuilder

通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。
它主要有两个使用场景:

  • 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。
  • LayoutBuilder 可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时 LayoutBuilder 就很有用

为了便于排错,我们封装一个能打印父组件传递给子组件约束的组件:

class LayoutLogPrint<T> extends StatelessWidget {
  const LayoutLogPrint({
    Key? key,
    this.tag,
    required this.child,
  }) : super(key: key);

  final Widget child;
  final T? tag; //指定日志tag

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (_, constraints) {
      // assert在编译release版本时会被去除
      assert(() {
        print('${tag ?? key ?? child}: $constraints');
        return true;
      }());
      return child;
    });
  }
}

我们就可以使用 LayoutLogPrint 组件树中任意位置的约束信息,比如:

LayoutLogPrint(child:Text("xx"))

控制台输出:

flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)

可以看到 Text(“xx”) 的显示空间最大宽度为 428,最大高度为 823 。
我们的大前提是盒模型布局,如果是Sliver 布局,可以使用 SliverLayoutBuiler 来打印。

AfterLayout

  • 布局一结束就去获取大小和位置信息,笔者封装了一个 AfterLayout 组件,它可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。
AfterLayout(
  callback: (RenderAfterLayout ral) {
    print(ral.size); //子组件的大小
    print(ral.offset);// 子组件在屏幕中坐标
  },
  child: Text('flutter@wendux'),
),

控制台输出:

flutter: Size(105.0, 17.0)
flutter: Offset(42.5, 290.0)

Text 文本的实际长度是 105,高度是 17,它的起始位置坐标是(42.5, 290.0)

  • 获取组件相对于某个父组件的坐标
    RenderAfterLayout 类继承自 RenderBox,RenderBox 有一个 localToGlobal 方法,它可以将坐标转化为相对与指定的祖先节点的坐标,比如下面代码可以打印出 Text(‘A’) 在 父 Container 中的坐标
Builder(builder: (context) {
  return Container(
    color: Colors.grey.shade200,
    alignment: Alignment.center,
    width: 100,
    height: 100,
    child: AfterLayout(
      callback: (RenderAfterLayout ral) {
        Offset offset = ral.localToGlobal(
          Offset.zero,
          // 传一个父级元素
          ancestor: context.findRenderObject(),
        );
        print('A 在 Container 中占用的空间范围为:${offset & ral.size}');
      },
      child: Text('A'),
    ),
  );
}),

通过观察 LayoutBuilder 的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的build 和 layout 是可以交错执行的,并不是严格的按照先 build 再 layout 的顺序

至此,布局类组件完结。