在Flutter中保持滚动视图偏移的同时,在列表视图中添加前置项

25
我正在寻找一种方法将新项插入到列表视图中,同时保持用户的滚动偏移量不变。基本上就像Twitter feed在下拉刷新后:新项添加到顶部,滚动位置保持不变。然后用户可以向上滚动以查看新添加的项目。
如果我只是在开头用几个新项重建列表/滚动部件,它会跳跃,因为滚动视图内容的高度增加了。仅估计这些新项的高度以纠正跳跃不是一个选项,因为新项的内容是可变的。 即使是提供了在任意位置动态插入项的方法的AnimatedList小部件,在插入索引0时也会跳跃。
有什么想法可以解决这个问题吗?也许使用Offstage小部件预先计算新项的高度?

你能看一下这些链接是否有帮助吗:stackoverflowmedium - Thanthu
@Thanthu:谢谢,但那些链接是关于在重建时记住滚动偏移量(例如切换选项卡时)的,而在我的情况下,它是关于在现有列表中添加新项目时维护用户感知的滚动偏移量。 - florisvdg
请检查我刚刚发布的答案。对我来说它是有效的。如果您需要,我可以添加更多细节。 - Thanthu
你能找到解决方法吗?我似乎遇到了同样的问题。 - Nick Mowen
1
@NickMowen:很遗憾,没有。我已经为此创建了一个问题:https://github.com/flutter/flutter/issues/21541 - florisvdg
@florisvdg 你找到解决方案了吗? - tudorprodan
3个回答

7
最近遇到了这个问题:我有一个聊天滚动条,根据滚动的方向异步加载前面或后面的消息。这个解决方案适合我的情况。
解决方案的思路是创建两个SliverLists,并将它们放在CustomScrollView中。
CustomScrollView(
  center: centerKey,
  slivers: <Widget>[
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
            return Container(
              // Here we render elements from the upper group
              child: top[index]
            )
        }
    ),
    SliverList(
      // Key parameter makes this list grow bottom
      key: centerKey,
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
            return Container(
              // Here we render elements from the bottom group
              child: bottom[index]
            )
        }
    ),
)

第一个列表向上滚动,而第二个列表向下滚动。它们的偏移零点固定在同一点且永远不会移动。如果您需要添加项目,则将其推送到顶部列表,否则将其推送到底部列表。这样它们的偏移不会改变,您的滚动视图也不会跳动。
您可以在以下DartPad示例中找到解决方案原型。
更新:在此DartPad示例中修复了空安全问题。

1
谢谢,这很棒。Dart Pad 报错了,因为 Dart 新的空安全特性,你能把第 19 行改成 MyStatefulWidget({Key? key}) : super(key: key); 吗? - mister_cool_beans
1
@MikeMoy 谢谢您的关注。在新的dartpad文件中修复了该问题。 - Arsenii Burov

2
我不知道你是否已经解决了这个问题... Marcin Szalek在他的博客上发布了一个非常好的解决方案,关于实现一个无限动态列表。我尝试了一下,并且在ListView中像魅力一样工作。然后,我尝试使用AnimatedList来做同样的事情,但遇到了你报告的同样的问题(每次刷新后跳转到顶部...)。无论如何,ListView相当强大,应该能为你解决问题! 代码如下:
import 'dart:async';

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: new ThemeData(primarySwatch: Colors.blue),
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<int> items = List.generate(10, (i) => i);
  ScrollController _scrollController = new ScrollController();
  bool isPerformingRequest = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        _getMoreData();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  _getMoreData() async {
    if (!isPerformingRequest) {
      setState(() => isPerformingRequest = true);
      List<int> newEntries = await fakeRequest(
          items.length, items.length + 10); //returns empty list
      if (newEntries.isEmpty) {
        double edge = 50.0;
        double offsetFromBottom = _scrollController.position.maxScrollExtent -
            _scrollController.position.pixels;
        if (offsetFromBottom < edge) {
          _scrollController.animateTo(
              _scrollController.offset - (edge - offsetFromBottom),
              duration: new Duration(milliseconds: 500),
              curve: Curves.easeOut);
        }
      }
      setState(() {
        items.addAll(newEntries);
        isPerformingRequest = false;
      });
    }
  }

  Widget _buildProgressIndicator() {
    return new Padding(
      padding: const EdgeInsets.all(8.0),
      child: new Center(
        child: new Opacity(
          opacity: isPerformingRequest ? 1.0 : 0.0,
          child: new CircularProgressIndicator(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text("Infinite ListView"),
      ),
      body: ListView.builder(
        itemCount: items.length + 1,
        itemBuilder: (context, index) {
          if (index == items.length) {
            return _buildProgressIndicator();
          } else {
            return ListTile(title: new Text("Number $index"));
          }
        },
        controller: _scrollController,
      ),
    );
  }
}

/// from - inclusive, to - exclusive
Future<List<int>> fakeRequest(int from, int to) async {
  return Future.delayed(Duration(seconds: 2), () {
    return List.generate(to - from, (i) => i + from);
  });
}

可以在这里找到包含整个类的要点。


2
你应该在回答中提供链接中的相关部分,这样如果博客文章在未来被删除,读者仍然能够利用你的回答。 - chevybow
6
这个解决方案将项目添加到列表的末尾。OP想要的是一种在列表开头添加项目的方法(向上滚动,而不是向下)。 - Sonius
2
这个答案完全相反于@florisvdg在问题中所问的。 - Ravinder Kumar

1
我认为反转 + 懒加载会对你有帮助。 反转列表:
ListView.builder(reverse: true, ...);

对于懒加载,请参考这里


1
这可能是某些用例的解决方案,但在我的情况下,我的列表已经支持在底部进行新项目的懒加载。因此,我需要 Twitter Feed 行为:在滚动到底部时进行懒加载(这相当简单)_并且_在顶部进行下拉刷新,但不会将用户的滚动偏移量向上推(这似乎更难)。 - florisvdg
抱歉,我误解了问题。 - Dinesh Balasubramanian
@florisvdg 你找到任何解决方案了吗? - Jaya Prakash

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