Flutter 布局组件8 —— 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 继承关系

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

1.4 示例代码

下面是一个简单的例子,一个有 3 个子部件的 Stack,默认情况下它们位于 Stack 的右上角,可以看到它们相互重叠:

Stack(
  children: <Widget>[
    Container(
      width: 290,
      height: 190,
      color: Colors.green,
    ),
    Container(
      width: 250,
      height: 170,
      color: Colors.red,
    ),
    Container(
      width: 220,
      height: 150,
      color: Colors.yellow,
    ),
  ],
)

基本上,Stack大小尽可能小,并尝试大于其所有子小部件(除了定位或变换等子小部件,请参阅更多溢出属性)。
让我们看一个例子:一个带有最大尺寸子部件的Stack。

示例代码如下:

Stack(
  children: <Widget>[
    Container( // First child (child 1)
      width: double.infinity,
      height: double.infinity,
      color: Colors.green,
      margin: EdgeInsets.all(20)
    ),
    Container(
      width: 250,
      height: 170,
      color: Colors.red,
    ),
    Container(
      width: 220,
      height: 150,
      color: Colors.yellow,
    ),
  ],
)

如果要对齐(堆栈的)子小部件的位置,请将其放在 Align 中。

注意:当没有指定widthFactorheightFactor,但是指定了child时,Align会尽量大。

示例代码:

Stack(
  children: <Widget>[
    Container(
        width: double.infinity,
        height: double.infinity,
        color: Colors.green,
        margin: EdgeInsets.all(20)
    ),
    Align (
      alignment: Alignment.centerRight,
      child: Container(
        width: 250,
        height: 170,
        color: Colors.red,
      ),
    ),
    Container(
      width: 220,
      height: 150,
      color: Colors.yellow,
    ),
  ],
)

Flutter Align
Flutter Positioned
还可以通过将子小部件(Stack)放置在一个定位中来对齐它的Positioned

children
children - 是 Stack 的子部件列表。

List<Widget> children: const <Widget>[]

alignment
alignment属性用于对齐定位以外的子小部件。它的默认值是 AlignmentDirectional.topStart

AlignmentGeometry alignment: AlignmentDirectional.topStart

下面的示例显示 Stack 的对齐属性对 Positioned 的子小部件没有影响。

示例代码:

SizedBox (
   width: double.infinity,
   height: double.infinity,
   child: Stack(
     alignment: Alignment.centerLeft,
     children: <Widget>[
       Container(
         width: 290,
         height: 190,
         color: Colors.green,
       ),
       Container(
         width: 220,
         height: 130,
         color: Colors.yellow,
       ),
       Positioned (
         bottom: 10,
         right: 10,
         child: Container(
           width: 150,
           height: 90,
           color: Colors.red,
         ),
       ),
     ],
   )
)

overflow
overflow 属性用于指定 Stack 如何处理从中溢出的那些子小部件。处理方法可以是剪掉溢出,或者让溢出显示出来。溢出的默认值是Overflow.clip

Overflow overflow: Overflow.clip

// Enum:
Overflow.clip
Overflow.visible

测试结果表明 overflow:Overflow.visible 只适用于某些类型的widget(例如PositionedTransform)。

示例代码:

Container (
    width: 250,
    height: 250,
    color: Colors.blueGrey,
    margin: EdgeInsets.all(20),
    child: Stack (
      overflow: Overflow.visible,
      children: <Widget>[
        Positioned(
          top: 50,
          left: 50,
          child: Container(
            width: 290,
            height: 100,
            color: Colors.green,
          ),
        ),
        Positioned(
          top: 70,
          left: 70,
          child: Container(
            width: 120,
            height: 230,
            color: Colors.yellow,
          ),
        )
      ],
    )
)

示例:一个 Transform 沿 Y 轴倾斜,它可以从 Stack 溢出。显示沿 Y 轴的溢出,同时剪切 X 轴的溢出。

示例代码:

Container (
    width: 250,
    height: 250,
    margin: EdgeInsets.all(20),
    color: Colors.blueGrey,
    child: Stack (
      overflow: Overflow.visible,
      children: <Widget>[
        Container(
          height: 100,
          width: 300,
          color: Colors.green,
        ),
        Transform(
          alignment: FractionalOffset.topLeft,
          transform: Matrix4.skewY(0.7), // skew will not go out of y bounds
          child: Container(
            height: 100,
            width: 300,
            color: Colors.red,
          ),
        ),
      ],
    )
)

1.5 源码解析

构造函数如下:

Stack({
  Key key,
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

1.5.1 属性解析

alignment:对齐方式,默认是左上角(topStart);
textDirection:文本的方向,绝大部分不需要处理;
overflow:超过的部分是否裁剪掉(clipped);

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

其中StackFit有如下几种:

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

1.5.2 源码

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

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

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

if (childParentData.left != null && childParentData.right != null)
  childConstraints = childConstraints.tighten(width: size.width - childParentData.right - childParentData.left);
else if (childParentData.width != null)
  childConstraints = childConstraints.tighten(width: childParentData.width);

if (childParentData.top != null && childParentData.bottom != null)
  childConstraints = childConstraints.tighten(height: size.height - childParentData.bottom - childParentData.top);
else if (childParentData.height != null)
  childConstraints = childConstraints.tighten(height: childParentData.height);

child.layout(childConstraints, parentUsesSize: true);

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

double x;
if (childParentData.left != null) {
  x = childParentData.left;
} else if (childParentData.right != null) {
  x = size.width - childParentData.right - child.size.width;
} else {
  x = _resolvedAlignment.alongOffset(size - child.size).dx;
}

if (x < 0.0 || x + child.size.width > size.width)
  _hasVisualOverflow = true;

double y;
if (childParentData.top != null) {
  y = childParentData.top;
} else if (childParentData.bottom != null) {
  y = size.height - childParentData.bottom - child.size.height;
} else {
  y = _resolvedAlignment.alongOffset(size - child.size).dy;
}

if (y < 0.0 || y + child.size.height > size.height)
  _hasVisualOverflow = true;

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的节点。

Container(
  color: Colors.yellow,
  child: IndexedStack(
    index: 1,
    alignment: const Alignment(0.6, 0.6),
    children: [
      CircleAvatar(
        backgroundImage: AssetImage('images/pic.jpg'),
        radius: 100.0,
      ),
      Container(
        decoration: BoxDecoration(
          color: Colors.black45,
        ),
        child: Text(
          'Mia B',
          style: TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
    ],
  ),
)

2.3 源码解析

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

@override
void paintStack(PaintingContext context, Offset offset) {
if (firstChild == null || index == null)
  return;
final RenderBox child = _childAtIndex();
final StackParentData childParentData = child.parentData;
context.paintChild(child, childParentData.offset + offset);
}

2.4 使用场景

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

3. GridView

A scrollable, 2D array of widgets.

3.1 简介

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

3.2 布局行为

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

3.3 继承关系

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

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

3.4 示例代码

GridView.count(
  crossAxisCount: 2,
  children: List.generate(
    100,
    (index) {
      return Center(
        child: Text(
          'Item $index',
          style: Theme.of(context).textTheme.headline,
        ),
      );
    },
  ),
);

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

3.5 源码解析

默认构造函数如下:

GridView({
  Key key,
  Axis scrollDirection = Axis.vertical,
  bool reverse = false,
  ScrollController controller,
  bool primary,
  ScrollPhysics physics,
  bool shrinkWrap = false,
  EdgeInsetsGeometry padding,
  @required this.gridDelegate,
  bool addAutomaticKeepAlives = true,
  bool addRepaintBoundaries = true,
  double cacheExtent,
  List<Widget> children = const <Widget>[],
})

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

GridView.builder
GridView.custom
GridView.count
GridView.extent

3.5.1 属性解析

scrollDirection:滚动的方向,有垂直和水平两种,默认为垂直方向(Axis.vertical)。
reverse:默认是从上或者左向下或者右滚动的,这个属性控制是否反向,默认值为false,不反向滚动。
controller:控制child滚动时候的位置。
primary:是否是与父节点的PrimaryScrollController所关联的主滚动视图。
physics:滚动的视图如何响应用户的输入。
shrinkWrap:滚动方向的滚动视图内容是否应该由正在查看的内容所决定。
padding:四周的空白区域。
gridDelegate:控制GridView中子节点布局的delegate。
cacheExtent:缓存区域。

3.5.2 源码

@override
Widget build(BuildContext context) {
  final List<Widget> slivers = buildSlivers(context);
  final AxisDirection axisDirection = getDirection(context);

  final ScrollController scrollController = primary
    ? PrimaryScrollController.of(context)
    : controller;
  final Scrollable scrollable = new Scrollable(
    axisDirection: axisDirection,
    controller: scrollController,
    physics: physics,
    viewportBuilder: (BuildContext context, ViewportOffset offset) {
      return buildViewport(context, offset, axisDirection, slivers);
    },
  );
  return primary && scrollController != null
    ? new PrimaryScrollController.none(child: scrollable)
    : scrollable;
}

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

3.6 使用场景

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

参考链接:

  1. Stack class
  2. IndexedStack class
  3. GridView class
  4. ScrollView class
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容