比 Flutter ListView 更灵活的布局方式

比 Flutter ListView 更灵活的布局方式在 Flutter 中,涉及到滚动布局的时候,很多同学会大量使用 ListView。没错,在实现效果的方面 ListView 确实能做到大多数,但是有些情况下会很别扭。

大家好,我是 17。

在 Flutter 中,涉及到滚动布局的时候,很多同学会大量使用 ListView

ListView 的局限

没错,在实现效果方面 ListView 确实能实现大多数,但是有些情况下会很别扭,性能也不好。你可能遇到过下面的设计:

比 Flutter ListView 更灵活的布局方式

banner 和下面的列表是一起滚动的。如果用 ListView ,你一定可以马上写出代码:

ListView.builder(
    itemBuilder: (context, index) {
      if (index == 0) {
        return Container(
          height: 100,
          color: Colors.blue,
          child: Text('banner'),
        );
      } else {
       return ListTile(title: Text('${index - 1}'));
      }
    },
    itemCount: 100
  )

上面的代码会有一个问题,banner 的高度和下面列表的高度不一样,导致无法使用定高列表,造成性能下降。需要 if else,如果有多个 banner,if else 也要多个,那就相当复杂了。

还有一个问题,在你没有设置任何边距的情况下,ListView 和上面的 Widget 可能会一段空白。

比 Flutter ListView 更灵活的布局方式

你需要这样去除空白。

ListView.builder(
        padding: EdgeInsets.zero,

为什么会有空白呢?这是因为 ListView 继承自 BoxScrollView,它的主要贡献就是加了这个空白!

这个空白的值是多少呢?就是取的 mediaQuery 的 padding。因为浏海屏的出现,ios 中,上面和下面会有一部分不适合显示主要内容,所以就有了这个安全 padding。BoxScrollView 在设计的时候也考虑到了这一点,于是就默认加了这个 padding。但实际上,如果 listView 不是在最顶部,反而是帮了倒忙。

ListView 最理想的使用场景是展示的 item 都一样高,但多数情况下,item 是不一样高的。ListView 出现的目的是为了方便使用,但却是牺牲了灵活性。它只能有一个 SliverChild,这会导致 itemBuilder 函数逻辑的复杂和性能的下降。

更灵活的布局方式

其实我们可以直接从 ScrollView 继承,根据实际情况定制需要的组件。说到定制你可能会觉得一定很复杂,实际上是非常简单的,而且因为我们是根据业务量身定做的组件,所以用起来会特别顺手。

要用 ScrollView 实现上面的设计,只需要下面的代码:

class MyListView extends ScrollView {
  const MyListView(
      {Key? key,
      this.banner,
      required this.itemBuilder,
      required this.itemExtent,
      required this.itemCount})
      : super(key: key);
  final Widget? banner;
  final IndexedWidgetBuilder itemBuilder;
  final double itemExtent;
  final int itemCount;

  @override
  List<Widget> buildSlivers(BuildContext context) {
    List<Widget> list = [];
    if (banner != null) {
      list.add(SliverToBoxAdapter(child: banner!));
    }
    list.add(SliverFixedExtentList(
      delegate: SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
      itemExtent: itemExtent,
    ));
    return list;
  }
}

很简单吧。实际上,我们只是 override buildSlivers 方法,生成一个 list。SliverToBoxAdapter 可以看作是一个转换器,把普通的 Widget 转换为 Sliver Widget。虽然 buildSlivers 的返回值是 List<Widget> ,但实际上,Widget 应该是 Sliver Widget,否则无法滚动。

MyListView 使用起来也很方便,代码更简洁,没有了讨厌的 if else 了。

MyListView(
     banner: Container(color: Colors.green, height: 100),
     itemExtent: 20,
     itemCount: 100,
     itemBuilder: (context, index) => Text('$index'),
)

现在 banner 和 item 的逻辑是分开的,代码更加清晰,也更好维护。把 banner 这个高度不一样的 widget 分开后,剩下的 item 高度都是一样的,本例中,我们设置固定高度 itemExtent: 20,每个 item 的高度都是 20,在 buildSlivers 中用 itemExtent 做为参数,用 SliverFixedExtentList 生成定高列表,性能得到大大提高。

老板说,把第 10 条数据显示在第一的位置

这个需求还是很常见的,在某个时刻,需要把某条数据显示在第一的位置。如果用 ListView 实现起来不容易,你可能想要调整数据的位置,但需求是数据的位置不变,只是想让 ViewPort 滚动到 第 10 条数据的位置。你可能还想到了用 ListView 的 controller 来控制滚动位置,尝试一下可以知道并不方便实现,或者实现了也不方便维护。

直接用 ScrollView 就很简单了。 ScrollView 有一个参数可以直接实现在这样的功能,这个参数就是 center。你可能很奇怪,ListView 是从 BoxScrollView 继承,BoxScrollView 是从 ScrollView 继承,但是在 ListView 中没有发现这个参数啊?为了方便使用,BoxScrollView 只有一个 Sliver Child,center 参数没有了用武之地,在 ListView 中找不到这个参数也就不奇怪了。

实现功能

先看下效果,不使用 center 参数,banner 在第一个位置显示。

比 Flutter ListView 更灵活的布局方式

使用 center 参数后,第 10 条数据,自动显示在第一个位置。

比 Flutter ListView 更灵活的布局方式

下面是完整代码,贴到 main.dart 就能运行

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(body: MyWidget()),
    );
  }
}

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: MyListView(
            banner: Container(
              color: Colors.blue[100],
              alignment: Alignment.center,
              height: 100,
              child: const Text(
                'IAM17 Flutter 天天更新',
              ),
            ),
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('$index'),
              );
            },
            center: const ValueKey(9),
            itemExtent: 20,
            itemCount: 100));
  }
}

class MyListView extends ScrollView {
  const MyListView(
      {Key? key,
      this.banner,
      required this.itemBuilder,
      required this.itemExtent,
      required this.itemCount,
      Key? center})
      : super(key: key, center: center);
  final Widget? banner;
  final IndexedWidgetBuilder itemBuilder;
  final double itemExtent;
  final int itemCount;

  @override
  List<Widget> buildSlivers(BuildContext context) {
    List<Widget> list = [];
    if (banner != null) {
      list.add(SliverToBoxAdapter(child: banner!));
    }
    if (center == null) {
      list.add(SliverFixedExtentList(
        delegate:
            SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
        itemExtent: itemExtent,
      ));
    } else {
      for (var i = 0; i < itemCount; i++) {
        list.add(SliverToBoxAdapter(
          key: ValueKey(i),
          child: itemBuilder(context, i),
        ));
      }
    }
    return list;
  }
}

当 center 不为 null 的时候,放弃使用 SliverFixedExtentList,只能把 child 一个一个加到 list 中。这样会损失一些性能,但能快速实现需求,还是值得的。

center 参数是如何影响位置的?

在 ViewPort 的构造函数中有一个 assert,如果 center 不为空,那么在 slivers 中必须要找到 key 为 center 的 child。

Viewport({
    ...
    this.center,
    ...
  }) : 
       assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),

最终是给 ViewPort 对应的 renderObject 的 center 赋值。

代码位置 : flutter/lib/src/widgets/viewport.dart

void _updateCenter() {
    // TODO(ianh): cache the keys to make this faster
    final Viewport viewport = widget as Viewport;
    if (viewport.center != null) {
      int elementIndex = 0;
      for (final Element e in children) {
        if (e.widget.key == viewport.center) {
          renderObject.center = e.renderObject as RenderSliver?;
          break;
        }
        elementIndex++;
      }
      assert(elementIndex < children.length);
      _centerSlotIndex = elementIndex;
    } else if (children.isNotEmpty) {
      renderObject.center = children.first.renderObject as RenderSliver?;
      _centerSlotIndex = 0;
    } else {
      renderObject.center = null;
      _centerSlotIndex = null;
    }
  }

总之,就是通过 key 找到对应的 Sliver Widget,对应到 renderObject,实现 center 的功能。

关于 Flutter Key 的详细说明可以看看这篇 flutter key 详解

通过这个简单的案例说明,我们应该自己动手定制适合自己项目的 ”ListView“!通过简单的封装,就能让我们的代码更简洁,更容易维护,性能也会更好。

更多关于滚动的参数介绍可以看这篇 flutter 滚动的基石 Scrollable

回答下 @法的空间 提的问题:CustomScrollView 的意义何在?

BoxScrollView 和 CustomScrollView 都是 ScrollView 的 子类。BoxScrollView 只能创建一块滑动内容,CustomScrollView 可以支持滑动列表,这就是 CustomScrollView 的意义。

之所以没有直接用 CustomScrollView ,而是直接从 ScrollView 继承是为了可以把一些属性和滑动列表一起封装起来,方便使用。

如果代码不需要复用,直接用 CustomScrollView 也是可以的,而且也是最简单的方式。

CustomScrollView 的代码就一句:

 @override
 List<Widget> buildSlivers(BuildContext context) => slivers;

ScrollView 是抽象类,不能直接用,CustomScrollView 的意义在于:我们不需要每次都要 extends 一个类出来,用 CustomScrollView 就可以支持滑动列表。

希望已经解答了你的问题,谢谢提问!

今天的文章比 Flutter ListView 更灵活的布局方式分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18964.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注