如何在ExpansionTile / ListView展开时滚动?

12
我们在一个ListView内有许多ExpansionTile(目前还算不错,对吧?)我们的用户希望我们在打开ExpansionTile时“自动滚动”列表,使新展开的ExpansionTile现在位于屏幕顶部(或尽可能靠近顶部),以便他们无需手动滚动即可查看我们刚刚打开的内容。
我认为我们需要在ListView上使用类似于ScrollController的东西,并注册一个onExpansionChanged事件...做一些事情。是这个“事情”对我来说比较模糊 :)
任何关于如何实现这一点的指针或帮助将不胜感激!
提前致谢!
3个回答

25

如果我理解得正确的话,您想要的基本上是一种自动滚动ExpansionTile的方式,这样用户就不必手动滚动来查看其内容,对吗?

那么,您实际上可以在ListView中使用ScrollController,并利用ExpansionTileonExpansionChanged回调根据瓷砖与当前位置的大小相应地向上/向下滚动它。

但现在您正在想:我如何知道要滚动多少?

为此,您可以事先了解构建小部件时给它一些约束条件(例如构建一个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,它将在其生成器中使用具有GlobalKey_buildExpansionTile。 我们还将在扩展瓷砖时保存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 滚动控制器向上滚动以展开,通过将其高度乘以其索引向下滚动回其初始位置时折叠。你不必滚动回来,这只是我对此示例的个人偏好。这将导致类似以下的结果:

example

您可以在 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),
      ),
    );
  }
}

哇哦——天才啊,说实话,我绝对想不到那个!谢谢你!! - sjmcdowall
1
很高兴能帮忙。只是想提醒一下,如果您也想要相同的行为,可能需要为ListView上的最后几个项进行微调,因为在展开最后一个ExpansionTile之前,没有更多内容可供滚动。对此,您需要确定何时完成展开,然后滚动其子项高度的数量。 - Miguel Ruivo
答案提供了一个很好的起点,但是有几个要考虑的问题:1)如果您扩展一个瓷砖,然后再扩展一个瓷砖,滚动位置就不准确了;2)如何在最后一个项目扩展时得到通知,并获取其扩展高度? - Angel Todorov
你是什么意思? - Miguel Ruivo
@MiguelRuivo,我们能用这个来扩展一个项目吗? - Nurbek Batyrzhan uulu

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

这个非常简单,没有其他答案中的冗长计算。 - undefined
哇,简洁而优雅的解决方案! - undefined

5

根据这篇答案,您可以通过从其控制器获取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可以获得状态栏的高度,而从constants.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自己的展开动画需要200ms,就像定义中所示: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,并为其展开设置任何持续时间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接