dart 如何在平铺展开时滚动ExpansionTile / Listview?

px9o7tmv  于 2023-10-13  发布在  其他
关注(0)|答案(3)|浏览(83)

我们在ListView中有一堆ExpansionTile(到目前为止还不算太疯狂,对吧?我们的用户希望我们在打开ExpansionTile时“自动滚动”列表,这样新扩展的磁贴就位于屏幕的顶部(或尽可能位于顶部),这样他们就不必手动滚动来查看我们刚刚打开的内容。
我认为我们需要在ListView上使用类似ScrollController的东西--并注册onExpansionChanged..做……做……某事。对我来说这是一件非常模糊的事情。:)
任何关于如何做到这一点的提示或帮助将不胜感激!
TIA!TIA!

sqxo8psd

sqxo8psd1#

如果我得到了正确的你的要求,基本上是一种自动滚动你的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,它将在它的生成器中使用这个_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),
      ),
    );
  }
}
wqnecbli

wqnecbli2#

如果你仍然面临这个问题,我发现了另一个解决方案,使用滚动部件。使用GlobalKey,我们必须将ExpansionTile的当前上下文传递给Scrollable类的ensureVisible方法,它将完成所有工作。
为了获得更高的可重用性,请在GlobalKey上创建扩展:

extension GlobalKeyEnsureVisible on GlobalKey {
  void ensureVisible() {
    final keyContext = currentContext;
    if (keyContext != null) {
      Future.delayed(const Duration(milliseconds: 200)).then((_) {
        Scrollable.ensureVisible(keyContext, duration: const Duration(milliseconds: 200));
      });
    }
  }
}

然后在Widget中声明一个全局键:

final expansionTileKey = GlobalKey();

并将此全局密钥分配给扩展块。另外,当平铺展开时,调用我们上面定义的ensureVisible()方法。

ExpansionTile(
  key: expansionTileKey,
  onExpansionChanged: (expanded) {
    if (expanded) expansionTileKey.ensureVisible();
  },

你可以查看我的文章How to scroll an ExpansionTile when it is expanded?了解更多信息。

kse8i1jr

kse8i1jr3#

根据这个答案,你可以通过从ListView的控制器中获取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;

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

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,并给予你想要的任何扩展时间。

相关问题