如何创建一个具有内容大小的Flutter GridView

822 阅读5分钟

每当您想在网格中布置项目时,FlutterGridView部件是第一个要做的。

GridView 提供这些属性来决定项目的布局方式。

  • crossAxisSpacing
  • mainAxisSpacing
  • crossAxisCount(当使用GridView.countSliverGridDelegateWithFixedCrossAxisCount)
  • maxCrossAxisExtent(当使用GridView.extentSliverGridDelegateWithMaxCrossAxisExtent)
  • 儿童长宽比(childAspectRatio

根据我们的要求,我们可以选择在十字轴上要多少个项目(使用crossAxisCount ),或者最大的十字轴范围(使用maxCrossAxisExtent )。

除此之外,childAspectRatio 可以用来指定网格中所有项目的宽度/高度比例。

注意childAspectRatio 对网格中的所有项目应用相同的比例,不管里面的内容有多大。

这对简单的布局来说效果很好。

但很快就会发现,GridView 不够灵活,无法表现复杂的、响应式的布局。

因此,在这篇文章中,我们将探讨FlutterGridView widget的局限性。

我们将学习如何使用flutter_layout_grid包建立一个具有内容大小项目的响应式网格小组件,该包是基于 CSS网格布局规范。

但让我们从GridView 开始,了解其局限性。

GridView有什么问题?

为了说明这个问题,让我们考虑这个例子。

如果上面的窗口有一个固定的宽度,我们可以调整所有需要的属性,让项目正确呈现。

这里有一个自定义的widget,使用GridView ,渲染上面的布局。

class ItemCardGridView extends StatelessWidget {
  const ItemCardGridView(
      {Key? key,
      required this.crossAxisCount,
      required this.padding,
      required this.items})
      // we plan to use this with 1 or 2 columns only
      : assert(crossAxisCount == 1 || crossAxisCount == 2),
        super(key: key);
  final int crossAxisCount;
  final EdgeInsets padding;
  // list representing the data for all items
  final List<ItemCardData> items;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: padding,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        mainAxisSpacing: 40,
        crossAxisSpacing: 24,
        // width / height: fixed for *all* items
        childAspectRatio: 0.75,
      ),
      // return a custom ItemCard
      itemBuilder: (context, i) => ItemCard(data: items[i]),
      itemCount: items.length,
    );
  }
}

到目前为止,没有什么令人惊讶的。我们使用一个GridView.builder 来创建ItemCard widgets,并使用SliverGridDelegateWithFixedCrossAxisCount 来定义所有的布局属性。

这里复杂的是ItemCard widget,它包含。

  • 一个横幅图片
  • 一个标题
  • 一些元数据
  • 一些标签
  • 一个描述

由于这些值是根据内容变化的,如果我们对所有的网格项目应用相同的 childAspectRatio ,我们会遇到一些问题。

作为证明,下面是我们调整窗口大小时的情况。

而如果我们试图使我们的应用程序具有响应性,并在窗口变小时切换到单栏布局,这就是我们得到的结果。

一句话:使用固定的长宽比是不可取的,因为。

  • 如果项目的宽度变得太小,我们会得到一个底部溢出。
  • 如果项目的宽度变得太大,我们就会在底部得到额外的空白空间

无论我们如何努力,我们都无法调整所有的GridView 属性,使其适用于所有的窗口尺寸。

我们需要一种不同的布局算法

我们真正需要的是一种布局算法,它可以随着宽度的变化计算出每个项目应该有多高

更具体地说,该算法应该计算出每一行中最高的项目的高度,并将其应用于该行的所有项目。这意味着网格中任何一个项目的大小都取决于网格中其他项目的大小。

这是不可能的,GridView 。而且用Stack 也不能做到。

为了在Flutter中实现这一点,我们需要使用一个相当深奥的widget,叫做 MultiChildRenderObjectWidget.

老实说:如果我们试图用MultiChildRenderObjectWidget 来实现我们自己的自定义布局,事情会变得相当复杂。如果你对所有的细节感到好奇,你可以阅读StackOverflow上的这个优秀页面。

但这不是我们在这里要做的。相反,我们将使用一个名为flutter_layout_grid的包,它支持开箱即用的内容大小项目。

使用flutter_layout_grid包的内容大小项目

该包的README将其描述为Flutter的一个强大的网格布局系统,为复杂的用户界面设计而优化

因此,让我们来使用它吧

首先,我们需要将其添加到我们的pubspec.yaml

dependencies:
  flutter_layout_grid: ^1.0.3

然后,我们可以定义一个自定义的ItemCardLayoutGrid widget,在一个LayoutGrid widget里面显示每个ItemCard

import 'package:flutter_layout_grid/flutter_layout_grid.dart';

class ItemCardLayoutGrid extends StatelessWidget {
  const ItemCardLayoutGrid({
    Key? key,
    required this.crossAxisCount,
    required this.items,
  })  
  // we only plan to use this with 1 or 2 columns
  : assert(crossAxisCount == 1 || crossAxisCount == 2),
    // assume we pass an list of 4 items for simplicity
    assert(items.length == 4),
    super(key: key);
  final int crossAxisCount;
  final List<ItemCardData> items;

  @override
  Widget build(BuildContext context) {
    return LayoutGrid(
      // set some flexible track sizes based on the crossAxisCount
      columnSizes: crossAxisCount == 2 ? [1.fr, 1.fr] : [1.fr],
      // set all the row sizes to auto (self-sizing height)
      rowSizes: crossAxisCount == 2
          ? const [auto, auto]
          : const [auto, auto, auto, auto],
      rowGap: 40, // equivalent to mainAxisSpacing
      columnGap: 24, // equivalent to crossAxisSpacing
      // note: there's no childAspectRatio
      children: [
        // render all the cards with *automatic child placement*
        for (var i = 0; i < items.length; i++)
          ItemCard(data: items[i]),
      ],
    );
  }
}

以下是事情的进展。

首先,我们定义columnSizes

// set some flexible track sizes based on the crossAxisCount
columnSizes: crossAxisCount == 2 ? [1.fr, 1.fr] : [1.fr]

这使用一个FlexibleTrackSize单元来定义列应该如何确定自己的大小。1.fr 与在一个Flexible widget内设置flex: 1 相同。


然后,我们定义rowSizes

// set all the row sizes to auto (self-sizing height)
// here we assume there will be 4 items in total
rowSizes: crossAxisCount == 2
    ? const [auto, auto]
    : const [auto, auto, auto, auto]

在这种情况下,我们使用auto ,因为我们想让行根据内容来决定自己的大小。

注意:上面的代码使用三元操作符,根据crossAxisCount ,呈现出2x2或1x4的网格,这样我们就向columnSizesrowSizes 参数传递一个具有正确数量的列表。


之后,我们设置rowGapcolumnGap 属性,这相当于mainAxisSpacingcrossAxisSpacing

rowGap: 40, // equivalent to mainAxisSpacing
columnGap: 24, // equivalent to crossAxisSpacing

最后,我们在一个循环中传递一个子列表(我们的ItemCard widgets)。

children: [
  // render all the cards with *automatic child placement*
  for (var i = 0; i < items.length; i++)
    ItemCard(data: items[i]),
]

这就是了!如果我们用我们新的ItemCardLayoutGrid widget交换我们以前的实现,我们就会得到内容大小的项目,而不管窗口大小。

使用LayoutGrid和Slivers

网格布局很少单独使用,你可能会在其他可滚动的内容旁边显示它们。

这就是分流器的作用,Flutter提供了一个SliverGrid widget,我们可以把它放在CustomScrollView 里面(作为GridView 的替代)。

然而,LayoutGrid 并没有一个 "兼容sliver的 "替代品。

因此,如果你打算在CustomScrollView 内使用它,请确保将其包裹在SliverToBoxAdapter 内。

child: CustomScrollView(
  slivers: [
    // some slivers
    SliverToBoxAdapter(child: LayoutGrid(...)),
    // other slivers
  ]
)

要了解更多关于分流器的信息,请看我关于SliverGrid、SliverToBoxAdapter和其他分流器小工具的视频。

结语

就这样吧!通过用LayoutGrid 替换GridView ,并将所有的rowSizes 设置为auto ,我们可以得到内容大小的项目,这些项目可以在完全响应的布局下工作,而没有固定的儿童长宽比的限制

关于展示我们所涉及的所有代码的完整演示,请看GitHub上的这个项目。

这显示了一个完整的主页,其中有多个部分,CustomScrollView

flutter_layout_grid的功能远不止这些,所以请务必查看文档以了解所有其他功能。

参考文献

在研究这篇文章的时候,我发现以下资源很有用。

编码愉快!