展开图块时如何滚动 ExpansionTile / Listview?

How to scroll an ExpansionTile / Listview when a tile is expanded?

我们在 ListView 中有一堆 ExpansionTile(到目前为止还不算太疯狂,对吧?)。我们的用户希望我们在打开 ExpansionTile 时显示 "auto scroll" 列表,这样新的展开图块现在位于屏幕顶部(或尽可能位于顶部),这样他们就不会必须手动滚动才能看到我们刚刚打开的内容。

我认为我们需要 ListView 上的 ScrollController 之类的东西 -- 并注册一个 onExpansionChanged .. 来做.. 某事。这对我来说有点模糊。 :)

任何有关如何执行此操作的指示或帮助将不胜感激!

TIA!

如果我没看错你的要求,基本上是一种自动向上滚动 ExpansionTile 的方法,这样用户就不必手动查看内容了,对吧?

然后,您实际上可以在 ListView 中使用 ScrollController 并利用 ExpansionTileonExpansionChanged 回调来滚动它 up/down根据图块的大小与其当前位置。

但现在您想知道:我怎么知道要滚动多少?

为此,要么您之前通过给它一些约束(例如构建一个 Container(heigh: 100.0)) 或在这种特定情况下,您不知道确切的高度) ExpansionTile 高度需要多少像素。因此,我们可以使用 GlobalKey 然后通过在运行时检查它的 RenderBox 来检查它的渲染高度,以确切地知道 折叠时占用多少像素并将其乘以其索引。

  ExpansionTile _buildExpansionTile(int index) {
    final GlobalKey expansionTileKey = GlobalKey();
    double previousOffset;

    return ExpansionTile(
      key: expansionTileKey,
      onExpansionChanged: (isExpanded) {
        if (isExpanded) previousOffset = _scrollController.offset;
        _scrollToSelectedContent(isExpanded, previousOffset, index, expansionTileKey);
      },
      title: Text('My expansion tile $index'),
      children: _buildExpansionTileChildren(),
    );
  }

好的,现在我们有一个 ListView,它将在其生成器中使用这个 _buildExpansionTile 和一个 GlobalKey。当我们展开图块时,我们还会保存 ListView 的滚动偏移量。但是,我们实际上如何向上滚动以显示其内容?让我们看看 _scrollToSelectedContent 方法。

  void _scrollToSelectedContent(bool isExpanded, double previousOffset, int index, GlobalKey myKey) {
    final keyContext = myKey.currentContext;

    if (keyContext != null) {
      // make sure that your widget is visible
      final box = keyContext.findRenderObject() as RenderBox;
      _scrollController.animateTo(isExpanded ? (box.size.height * index) : previousOffset,
          duration: Duration(milliseconds: 500), curve: Curves.linear);
    }
  }

因此,我们通过其键访问渲染对象,然后使用 ListView 滚动控制器在展开时向上滚动,在折叠时通过将其高度乘以其索引向下滚动回到其初始位置。您不必向后滚动,这只是我个人对此示例的偏好。这将导致类似这样的结果:

这里有 StatefulWidget

中的完整示例
import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final ScrollController _scrollController = ScrollController();

  void _scrollToSelectedContent(bool isExpanded, double previousOffset, int index, GlobalKey myKey) {
    final keyContext = myKey.currentContext;

    if (keyContext != null) {
      // make sure that your widget is visible
      final box = keyContext.findRenderObject() as RenderBox;
      _scrollController.animateTo(isExpanded ? (box.size.height * index) : previousOffset,
          duration: Duration(milliseconds: 500), curve: Curves.linear);
    }
  }

  List<Widget> _buildExpansionTileChildren() => [
        FlutterLogo(
          size: 50.0,
        ),
        Text(
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse vulputate arcu interdum lacus pulvinar aliquam. Donec ut nunc eleifend, volutpat tellus vel, volutpat libero. Vestibulum et eros lorem. Nam ut lacus sagittis, varius risus faucibus, lobortis arcu. Nullam tempor vehicula nibh et ornare. Etiam interdum tellus ut metus faucibus semper. Aliquam quis ullamcorper urna, non semper purus. Mauris luctus quam enim, ut ornare magna vestibulum vel. Donec consectetur, quam a mattis tincidunt, augue nisi bibendum est, quis viverra risus odio ac ligula. Nullam vitae urna malesuada magna imperdiet faucibus non et nunc. Integer magna nisi, dictum a tempus in, bibendum quis nisi. Aliquam imperdiet metus id metus rutrum scelerisque. Morbi at nisi nec risus accumsan tempus. Curabitur non sem sit amet tellus eleifend tincidunt. Pellentesque sed lacus orci.',
          textAlign: TextAlign.justify,
        ),
      ];

  ExpansionTile _buildExpansionTile(int index) {
    final GlobalKey expansionTileKey = GlobalKey();
    double previousOffset;

    return ExpansionTile(
      key: expansionTileKey,
      onExpansionChanged: (isExpanded) {
        if (isExpanded) previousOffset = _scrollController.offset;
        _scrollToSelectedContent(isExpanded, previousOffset, index, expansionTileKey);
      },
      title: Text('My expansion tile $index'),
      children: _buildExpansionTileChildren(),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('MyScrollView'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: 100,
        itemBuilder: (BuildContext context, int index) => _buildExpansionTile(index),
      ),
    );
  }
}

根据 this 的回答,您可以通过从其控制器获取 ListView 的当前滚动偏移来计算需要滚动多少:

_scrollController.offset

并将其添加到其键给定的当前 ExpansionTile Y 位置:

RenderBox _box = _ExpansiontileKey.currentContext.findRenderObject();
yPosition = _box.localToGlobal(Offset.zero).dy;

这个位置考虑了状态栏和AppBar的高度,所以滚动点可以这样计算:

scrollPoint = _scrollController.offset + yPosition - MediaQueryData.fromWindow(window).padding.top - 56;

MediaQueryData.fromWindow(window).padding.top 会给你状态栏高度,从 constats.dart 你可以得到 AppBar 高度:

/// The height of the toolbar component of the [AppBar].
const double kToolbarHeight = 56.0;

您可以通过检查滚动点是否低于滚动的最大范围来避免最后一项的计算错误:

if(_scrollPoint <= _scrollController.position.maxScrollExtent)
  _scrollController.animateTo(_scrollPoint, duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
else
  _scrollController.animateTo(_scrollController.position.maxScrollExtent,
    duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn
  );

注意 ExpansionTiles 自己的展开动画需要 200 毫秒,如其定义所示: const Duration _kExpand = Duration(milliseconds: 200); 因此,最好在开始滚动之前等待展开动画完成,以便您可以获得更新的值。

一种方法是使用 Future.delayed(duration):

延迟调用
Future.delayed(Duration(milliseconds: 250)).then((value){
  RenderBox _box = _ExpansiontileKey.currentContext.findRenderObject();
  yPosition = _box.localToGlobal(Offset.zero).dy;
  scrollPoint = _scrollController.offset + yPosition - 
    MediaQueryData.fromWindow(window).padding.top - 56;
    if(_scrollPoint <= _scrollController.position.maxScrollExtent)
      _scrollController.animateTo(_scrollPoint, duration: Duration(milliseconds: 
        500), curve: Curves.fastOutSlowIn);
    else
      _scrollController.animateTo(_scrollController.position.maxScrollExtent, 
        duration: Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
  });

除此之外,您还可以制作自己的 ExpansionTile 并为其指定任何您想要的扩展持续时间。

如果您仍然面临这个问题,我找到了另一个使用可滚动小部件的解决方案。我们必须将 ExpansionTile 的当前上下文传递给 Scrollable class 的 ensureVisible 方法,它会完成工作。您可以查看我的文章 How to scroll an ExpansionTile when it is expanded? 了解更多信息。