• Flutter 布局(八)- Stack、IndexedStack、GridView详解
    • 1. Stack
      • 1.1 简介
      • 1.2 布局行为
      • 1.3 继承关系
      • 1.4 示例代码
      • 1.5 源码解析
        • 1.5.1 属性解析
        • 1.5.2 源码
      • 1.6 使用场景
    • 2. IndexedStack
      • 2.1 简介
      • 2.2 例子
      • 2.3 源码解析
      • 2.4 使用场景
    • 3. GridView
      • 3.1 简介
      • 3.2 布局行为
      • 3.3 继承关系
      • 3.4 示例代码
      • 3.5 源码解析
        • 3.5.1 属性解析
        • 3.5.2 源码
      • 3.6 使用场景
    • 4. 后话
    • 5. 参考

    Flutter 布局(八)- Stack、IndexedStack、GridView详解

    本文主要介绍Flutter布局中的Stack、IndexedStack、GridView控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析。

    1. Stack

    A widget that positions its children relative to the edges of its box.

    1.1 简介

    Stack可以类比web中的absolute,绝对布局。绝对布局一般在移动端开发中用的较少,但是在某些场景下,还是有其作用。当然,能用Stack绝对布局完成的,用其他控件组合也都能实现。

    1.2 布局行为

    Stack的布局行为,根据child是positioned还是non-positioned来区分。

    • 对于positioned的子节点,它们的位置会根据所设置的top、bottom、right以及left属性来确定,这几个值都是相对于Stack的左上角;
    • 对于non-positioned的子节点,它们会根据Stack的aligment来设置位置。

    对于绘制child的顺序,则是第一个child被绘制在最底端,后面的依次在前一个child的上面,类似于web中的z-index。如果想调整显示的顺序,则可以通过摆放child的顺序来进行。

    1.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Stack

    1.4 示例代码

    1. Stack(
    2. alignment: const Alignment(0.6, 0.6),
    3. children: [
    4. CircleAvatar(
    5. backgroundImage: AssetImage('images/pic.jpg'),
    6. radius: 100.0,
    7. ),
    8. Container(
    9. decoration: BoxDecoration(
    10. color: Colors.black45,
    11. ),
    12. child: Text(
    13. 'Mia B',
    14. style: TextStyle(
    15. fontSize: 20.0,
    16. fontWeight: FontWeight.bold,
    17. color: Colors.white,
    18. ),
    19. ),
    20. ),
    21. ],
    22. );

    示例代码我就直接用的Building Layouts in Flutter中的例子,效果如下

    Stack例子

    1.5 源码解析

    构造函数如下:

    1. Stack({
    2. Key key,
    3. this.alignment = AlignmentDirectional.topStart,
    4. this.textDirection,
    5. this.fit = StackFit.loose,
    6. this.overflow = Overflow.clip,
    7. List<Widget> children = const <Widget>[],
    8. })

    1.5.1 属性解析

    alignment:对齐方式,默认是左上角(topStart)。

    textDirection:文本的方向,绝大部分不需要处理。

    fit:定义如何设置non-positioned节点尺寸,默认为loose。

    其中StackFit有如下几种:

    • loose:子节点宽松的取值,可以从min到max的尺寸;
    • expand:子节点尽可能的占用空间,取max尺寸;
    • passthrough:不改变子节点的约束条件。

    overflow:超过的部分是否裁剪掉(clipped)。

    1.5.2 源码

    Stack的布局代码有些长,在此分段进行讲解。

      1. 如果不包含子节点,则尺寸尽可能大。
    1. if (childCount == 0) {
    2. size = constraints.biggest;
    3. return;
    4. }
    • 2.根据fit属性,设置non-positioned子节点约束条件。
    1. switch (fit) {
    2. case StackFit.loose:
    3. nonPositionedConstraints = constraints.loosen();
    4. break;
    5. case StackFit.expand:
    6. nonPositionedConstraints = new BoxConstraints.tight(constraints.biggest);
    7. break;
    8. case StackFit.passthrough:
    9. nonPositionedConstraints = constraints;
    10. break;
    11. }
    • 3.对non-positioned子节点进行布局。
    1. RenderBox child = firstChild;
    2. while (child != null) {
    3. final StackParentData childParentData = child.parentData;
    4. if (!childParentData.isPositioned) {
    5. hasNonPositionedChildren = true;
    6. child.layout(nonPositionedConstraints, parentUsesSize: true);
    7. final Size childSize = child.size;
    8. width = math.max(width, childSize.width);
    9. height = math.max(height, childSize.height);
    10. }
    11. child = childParentData.nextSibling;
    12. }
    • 4.根据是否包含positioned子节点,对stack进行尺寸调整。
    1. if (hasNonPositionedChildren) {
    2. size = new Size(width, height);
    3. } else {
    4. size = constraints.biggest;
    5. }
    • 5.最后对子节点位置的调整,这个调整过程中,则根据alignment、positioned节点的绝对位置等信息,对子节点进行布局。

    第一步是根据positioned的绝对位置,计算出约束条件后进行布局。

    1. if (childParentData.left != null && childParentData.right != null)
    2. childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
    3. else if (childParentData.width != null)
    4. childConstraints = childConstraints.tighten(width: childParentData.width);
    5. if (childParentData.top != null && childParentData.bottom != null)
    6. childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
    7. else if (childParentData.height != null)
    8. childConstraints = childConstraints.tighten(height: childParentData.height);
    9. child.layout(childConstraints, parentUsesSize: true);

    第二步则是位置的调整,其中坐标的计算如下:

    1. double x;
    2. if (childParentData.left != null) {
    3. x = childParentData.left;
    4. } else if (childParentData.right != null) {
    5. x = size.width - childParentData.right - child.size.width;
    6. } else {
    7. x = _resolvedAlignment.alongOffset(size - child.size).dx;
    8. }
    9. if (x < 0.0 || x + child.size.width > size.width)
    10. _hasVisualOverflow = true;
    11. double y;
    12. if (childParentData.top != null) {
    13. y = childParentData.top;
    14. } else if (childParentData.bottom != null) {
    15. y = size.height - childParentData.bottom - child.size.height;
    16. } else {
    17. y = _resolvedAlignment.alongOffset(size - child.size).dy;
    18. }
    19. if (y < 0.0 || y + child.size.height > size.height)
    20. _hasVisualOverflow = true;
    21. childParentData.offset = new Offset(x, y);

    1.6 使用场景

    Stack的场景还是比较多的,对于需要叠加显示的布局,一般都可以使用Stack。有些场景下,也可以被其他控件替代,我们应该选择开销较小的控件去实现。

    2. IndexedStack

    A Stack that shows a single child from a list of children.

    2.1 简介

    IndexedStack继承自Stack,它的作用是显示第index个child,其他child都是不可见的。所以IndexedStack的尺寸永远是跟最大的子节点尺寸一致。

    2.2 例子

    在此还是将Stack的例子稍加改造,将index设置为1,也就是显示含文本的Container的节点。

    1. Container(
    2. color: Colors.yellow,
    3. child: IndexedStack(
    4. index: 1,
    5. alignment: const Alignment(0.6, 0.6),
    6. children: [
    7. CircleAvatar(
    8. backgroundImage: AssetImage('images/pic.jpg'),
    9. radius: 100.0,
    10. ),
    11. Container(
    12. decoration: BoxDecoration(
    13. color: Colors.black45,
    14. ),
    15. child: Text(
    16. 'Mia B',
    17. style: TextStyle(
    18. fontSize: 20.0,
    19. fontWeight: FontWeight.bold,
    20. color: Colors.white,
    21. ),
    22. ),
    23. ),
    24. ],
    25. ),
    26. )

    IndexedStack例子

    2.3 源码解析

    其绘制代码很简单,因为继承自Stack,布局方面表现基本一致,不同之处在于其绘制的时候,只是将第Index个child进行了绘制。

    1. @override
    2. void paintStack(PaintingContext context, Offset offset) {
    3. if (firstChild == null || index == null)
    4. return;
    5. final RenderBox child = _childAtIndex();
    6. final StackParentData childParentData = child.parentData;
    7. context.paintChild(child, childParentData.offset + offset);
    8. }

    2.4 使用场景

    如果需要展示一堆控件中的一个,可以使用IndexedStack。有一定的使用场景,但是也有控件可以实现其功能,只不过操作起来可能会复杂一些。

    3. GridView

    A scrollable, 2D array of widgets.

    3.1 简介

    GridView在移动端上非常的常见,就是一个滚动的多列列表,实际的使用场景也非常的多。

    3.2 布局行为

    GridView的布局行为不复杂,本身是尽量占满空间区域,布局行为上完全继承自ScrollView。

    3.3 继承关系

    1. Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView

    从继承关系看,GridView是在ScrollView的基础上封装而来的,这跟移动端的类似。

    3.4 示例代码

    1. GridView.count(
    2. crossAxisCount: 2,
    3. children: List.generate(
    4. 100,
    5. (index) {
    6. return Center(
    7. child: Text(
    8. 'Item $index',
    9. style: Theme.of(context).textTheme.headline,
    10. ),
    11. );
    12. },
    13. ),
    14. );

    示例代码直接用了Creating a Grid List中的例子,创建了一个2列总共100个子节点的列表。

    3.5 源码解析

    默认构造函数如下:

    1. GridView({
    2. Key key,
    3. Axis scrollDirection = Axis.vertical,
    4. bool reverse = false,
    5. ScrollController controller,
    6. bool primary,
    7. ScrollPhysics physics,
    8. bool shrinkWrap = false,
    9. EdgeInsetsGeometry padding,
    10. @required this.gridDelegate,
    11. bool addAutomaticKeepAlives = true,
    12. bool addRepaintBoundaries = true,
    13. double cacheExtent,
    14. List<Widget> children = const <Widget>[],
    15. })

    同时也提供了如下额外的四种构造方法,方便开发者使用。

    1. GridView.builder
    2. GridView.custom
    3. GridView.count
    4. GridView.extent

    3.5.1 属性解析

    scrollDirection:滚动的方向,有垂直和水平两种,默认为垂直方向(Axis.vertical)。

    reverse:默认是从上或者左向下或者右滚动的,这个属性控制是否反向,默认值为false,不反向滚动。

    controller:控制child滚动时候的位置。

    primary:是否是与父节点的PrimaryScrollController所关联的主滚动视图。

    physics:滚动的视图如何响应用户的输入。

    shrinkWrap:滚动方向的滚动视图内容是否应该由正在查看的内容所决定。

    padding:四周的空白区域。

    gridDelegate:控制GridView中子节点布局的delegate。

    cacheExtent:缓存区域。

    3.5.2 源码

    1. @override
    2. Widget build(BuildContext context) {
    3. final List<Widget> slivers = buildSlivers(context);
    4. final AxisDirection axisDirection = getDirection(context);
    5. final ScrollController scrollController = primary
    6. ? PrimaryScrollController.of(context)
    7. : controller;
    8. final Scrollable scrollable = new Scrollable(
    9. axisDirection: axisDirection,
    10. controller: scrollController,
    11. physics: physics,
    12. viewportBuilder: (BuildContext context, ViewportOffset offset) {
    13. return buildViewport(context, offset, axisDirection, slivers);
    14. },
    15. );
    16. return primary && scrollController != null
    17. ? new PrimaryScrollController.none(child: scrollable)
    18. : scrollable;
    19. }

    上面这段代码是ScrollView的build方法,GridView就是一个特殊的ScrollView。GridView本身代码没有什么,基本上都是ScrollView上的东西,主要会涉及到Scrollable、Sliver、Viewport等内容,这些内容比较多,因此源码就先略了,后面单独出一篇文章对ScrollView进行分析吧。

    3.6 使用场景

    使用场景很多,非常常见的控件。也有控件可以实现其功能,例如官方说的,GridView实际上是一个silvers只包含一个SilverGrid的CustomScrollView。

    4. 后话

    笔者建了一个Flutter学习相关的项目,Github地址,里面包含了笔者写的关于Flutter学习相关的一些文章,会定期更新,也会上传一些学习Demo,欢迎大家关注。

    5. 参考

    1. Stack class
    2. IndexedStack class
    3. GridView class
    4. ScrollView class