Flutter ListView.builder - 如何通过编程方式跳转到特定的索引

11

我有一个使用MaterialAppDefaultTabControllerScaffoldTabBarView构建的屏幕。

在这个屏幕上,我有一个主体内容,它使用StreamBuilder从sqllite检索元素列表。我获得100个确切的元素(“有限列表”),以使用ListView显示。

我的问题是,在此屏幕打开时,如何使用ListView.builder跳转到特定索引?

我的主屏幕:

...
ScrollController controller = ScrollController();

 @override
  Widget build(BuildContext context) {

    return MaterialApp(
      debugShowCheckedModeBanner : false,
      home: DefaultTabController(
        length: 3,
        child: Scaffold(
            appBar: AppBar(
              backgroundColor: Pigment.fromString(UIData.primaryColor),
              elevation: 0,
              centerTitle: true,
              title: Text(translations.text("quran").toUpperCase()),
              bottom: TabBar(
                tabs: [
                    Text("Tab1"),
                    Text("Tab2"),
                    Text("Tab3")
                ],
              ),
              leading: Row(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  Expanded(
                      child: InkWell(
                        child: SizedBox(child: Image.asset("assets/images/home.png"), height: 10, width: 1,),
                        onTap: () => Navigator.of(context).pop(),
                      )
                  ),
                ],
              ),
            ),

            floatingActionButton: FloatingActionButton(
              onPressed: _scrollToIndex,
              tooltip: 'Testing Index Jump',
              child: Text("GO"),
            ),

            body:
            TabBarView(
              children: [
                Stack(
                  children: <Widget>[
                    MyDraggableScrollBar.create(
                        scrollController: controller,
                        context: context,
                        heightScrollThumb: 25,
                        child: ListView(
                          controller: controller,
                          children: <Widget>[
                            Padding(
                                padding: EdgeInsets.fromLTRB(30, 15, 30, 8),
                                child: Container(
                                    alignment: Alignment.center,
                                    height: 30,
                                    child: ClipRRect(
                                      borderRadius: BorderRadius.circular(8),
                                      child: TextField(
                                        style: TextStyle(color: Colors.green),
                                        decoration: new InputDecoration(
                                            contentPadding: EdgeInsets.all(5),
                                            border: InputBorder.none,
                                            filled: true,
                                            hintStyle: new TextStyle(color: Colors.green, fontSize: 14),
                                            prefixIcon: Icon(FontAwesomeIcons.search,color: Colors.green,size: 17,),
                                            hintText: translations.text("search-quran"),
                                            fillColor: Colors.grey[300],
                                            prefixStyle: TextStyle(color: Colors.green)
                                        ),
                                        onChanged: (val) => quranBloc.searchSurah(val),
                                      ),
                                    )
                                )
                            ),

                            //surah list
                            streamBuilderQuranSurah(context)

                          ],
                        )
                    ) // MyDraggableScrollBar

                  ],
                ),
                Icon(Icons.directions_transit),
                Icon(Icons.directions_bike),
              ],
            )
        )));
  }

  Widget streamBuilderQuranSurah(BuildContext ctx){
    return StreamBuilder(
      stream: quranBloc.chapterStream ,
      builder: (BuildContext context, AsyncSnapshot<ChaptersModel> snapshot){
        if(snapshot.hasData){

          return ListView.builder(
            controller: controller,
            shrinkWrap: true,
            physics: NeverScrollableScrollPhysics(),
            itemCount:(snapshot.data.chapters?.length ?? 0),
            itemBuilder: (BuildContext context, int index) {
              var chapter =
              snapshot.data.chapters?.elementAt(index);
              return chapterDataCell(chapter);
            },
          );
        }
        else{

          return SurahItemShimmer();
        }
      },
    );
  }
...

MyDraggableScrollBar.dart

import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';

class MyDraggableScrollBar {
  static Widget create({
    @required BuildContext context,
    @required ScrollController scrollController,
    @required double heightScrollThumb,
    @required Widget child,
  }) {
    return DraggableScrollbar(

      alwaysVisibleScrollThumb: true,
      scrollbarTimeToFade: Duration(seconds: 3),
      controller: scrollController,
      heightScrollThumb: heightScrollThumb,
      backgroundColor: Colors.green,
      scrollThumbBuilder: (
        Color backgroundColor,
        Animation<double> thumbAnimation,
        Animation<double> labelAnimation,
        double height, {
        Text labelText,
        BoxConstraints labelConstraints,
      }) {
        return InkWell(
          onTap: () {},
          child: Container(
            height: height,
            width: 7,
            color: backgroundColor,
          ),
        );
      },
      child: child,
    );
  }
}
我已尝试寻找其他解决方案,但似乎都不起作用,例如只支持无限列表的indexed_list_view
而且Flutter似乎仍然没有这个功能,可以看看这个问题
有什么想法吗?

1
你已经知道要跳过的索引号或项目吗? - Ajil O.
1
嗨@AjilO。是的,因为它是有限列表,并且它将是来自其他屏幕的参数。 - questionasker
1
你可能正在寻找这个 https://dev59.com/ObPma4cB1Zd3GeqPqGg0#58809961 ScrollController(initialScrollOffset: _) - TWL
7个回答

5
你可以使用 https://pub.dev/packages/scrollable_positioned_list。你可以将初始索引传递给小部件。
ScrollablePositionedList.builder(
 initialScrollIndex: 12, //you can pass the desired index here//
 itemCount: 500,
 itemBuilder: (context, index) => Text('Item $index'),
 itemScrollController: itemScrollController,
 itemPositionsListener: itemPositionsListener,
);

有没有办法在这个地方使用滚动条? - Matthew Trent
如何在这里使用滚动条? - RahulZx

3

通用解决方案:

为存储可以表示为数字/字符串/字符串列表等任何内容,Flutter 提供了一种功能强大且易于使用的插件,可将需要存储的值与一个键一起存储。因此,下次您需要检索或更新该值时,只需使用该键即可。

要开始使用,请将 shared_preferences 插件添加到 pubspec.yaml 文件中,

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: "<newest version>"

从终端运行flutter pub get或者如果您使用的是IntelliJ,只需点击Packages get(在查看pubspec.yaml文件时,您会在屏幕右上角某处找到它)

执行以上命令成功后,在main.dart或相关文件中导入下面的文件。

  import 'package:shared_preferences/shared_preferences.dart';

现在只需将 ScrollController 附加到你的 ListView.builder() 组件上,并确保最终/最后的偏移量与特定密钥一起使用 shared_preferences 存储,以便用户以任何方式离开应用程序时保存状态,并在调用所关注的小部件的 initState 时进行设置。
为了检测我们应用程序状态的变化并相应地采取行动,我们将从我们的类中继承 WidgetsBindingObserver
跟随以下步骤:
1. 让你的 StatefulWidget 的 State 类同时继承 WidgetsBindingObserver 类。 2. 在上述类的函数成员中定义异步函数 resumeController()。
  Future<void> resumeController() async{
    _sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
      if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
      else _sharedPreferences.setDouble("scroll-offset-0", 0);
      setState((){});
      return _sharedPreferences;
    });
  1. 声明两个变量,一个用于存储和传递scrollcontroller,另一个用于存储和使用SharedPreferences实例。
  ScrollController _scrollController;
  SharedPreferences _sharedPreferences;
  1. 调用resumeController()方法,并将你的类作为参数传递给WidgetsBinding类实例对象的addObserver方法。
  resumeController();
  WidgetsBinding.instance.addObserver(this);

只需将此代码粘贴到类定义中(其他成员函数之外)。
 @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
       _sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
    super.didChangeAppLifecycleState(state);
  }
  1. ScrollController() 传递给相应的可滚动对象。

工作示例:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver{

  //[...]
  ScrollController _scrollController;
  SharedPreferences _sharedPreferences;

  Future<void> resumeController() async{
    _sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
      if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
      else _sharedPreferences.setDouble("scroll-offset-0", 0);
      setState((){});
      return _sharedPreferences;
    });

  }

  @override
  void initState() {
    resumeController();
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
       _sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
    super.didChangeAppLifecycleState(state);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: Text("Smart Scroll View"),
        ),
        body: ListView.builder(
            itemCount: 50,
            controller: _scrollController,
            itemBuilder: (c,i)=>
                Padding(
                  padding: EdgeInsets.symmetric(horizontal: 24,vertical: 16),
                  child: Text((i+1).toString()),
                ),
        ),
      ),
    );
  }
}

你认为AppLifecycleState.pausedAppLifecycleState.inactiveAppLifecycleState.suspending覆盖了所有情况,并且无论应用程序如何“关闭”,滚动偏移量始终会被保存吗? - TechAurelian
此外,“暂停”不再是AppLifecycleState的一个值。 - TechAurelian

1

您可以使用 flutter_scrollview_observer 库实现所需的功能,而无需过多干扰。

正常创建和使用 ScrollController 实例即可。

ScrollController scrollController = ScrollController();

ListView _buildListView() {
  return ListView.builder(
    controller: scrollController,
    ...
  );
}

创建一个ListObserverController实例并将其传递给ListViewObserver

ListObserverController observerController = ListObserverController(controller: scrollController);

ListViewObserver(
  controller: observerController,
  child: _buildListView(),
  ...
)

现在您可以滚动到指定的索引位置

// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)

// Jump to the specified index position with animation.
observerController.animateTo(
  index: 1,
  duration: const Duration(milliseconds: 250),
  curve: Curves.ease,
);

1
我将提供另一种方法,该方法支持列表的懒加载,不像@Shinbly的方法,还支持在列表中调整瓷砖大小而无需重新计算ListView的正确偏移量或保存任何持久信息,就像“@Nephew of Stackoverflow”所做的那样。
这种方法的关键是利用CustomScrollViewCustomScrollView.center属性。
以下是基于Flutter文档示例代码(widgets.CustomScrollView.2)的示例:
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  List<int> top = [];
  List<int> bottom = [0];
  List<int> test = List.generate(10, (i) => -5 + i);
  bool positionSwitcher = true;

  @override
  Widget build(BuildContext context) {
    positionSwitcher = !positionSwitcher;
    final jumpIndex = positionSwitcher ? 1 : 9;
    Key centerKey = ValueKey('bottom-sliver-list');
    return Scaffold(
      appBar: AppBar(
        title: const Text('Press Jump!! to jump between'),
        leading: IconButton(
          icon: const Icon(Icons.add),
          onPressed: () {
            setState(() {
              top.add(-top.length - 1);
              bottom.add(bottom.length);
            });
          },
        ),
      ),
      body: Column(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              RaisedButton(
                child: Text('Jump!!'),
                onPressed: () => setState(() {}),
              ),
              Text(positionSwitcher ? 'At top' : 'At bottom'),
            ],
          ),
          Expanded(
            child: CustomScrollView(
              center: centerKey,
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int i) {
                      final index = jumpIndex - 1 - i;
                      return Container(
                        alignment: Alignment.center,
                        color: Colors.blue[200 + test[index] % 4 * 100],
                        height: 100 + test[index] % 4 * 20.0,
                        child: Text('Item: ${test[index]}'),
                      );
                    },
                    childCount: jumpIndex,
                  ),
                ),
                SliverList(
                  key: centerKey,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int i) {
                      final index = i + jumpIndex;
                      return Container(
                        alignment: Alignment.center,
                        color: i == 0
                            ? Colors.red
                            : Colors.blue[200 + test[index] % 4 * 100],
                        height: 100 + test[index] % 4 * 20.0,
                        child: Text('Item: ${test[index]}'),
                      );
                    },
                    childCount: test.length - jumpIndex,
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }
}

解释:

  1. 我们使用单个列表作为两个 SliverList 的数据源
  2. 在每次重建时,我们使用 center 键将第二个 SliverList 重新定位到 ViewPort
  3. 仔细管理从 SliverList 索引到数据源列表索引的转换
  4. 注意滚动视图如何通过传递从此 SliverList 的底部开始的索引来构建第一个 SliverList(即索引 0 表示第一个列表片段中的最后一项)
  5. CustomeScrollView 一个适当的 key 来决定是否“重新定位”

1

不知道小部件大小的解决方案

我找到的解决方案是,在不知道小部件大小的情况下显示从索引到结尾的反向“子列表”,然后滚动到您的“子列表”的顶部并重置整个列表。由于它是一个反向列表,因此项目将添加到列表的顶部,并且您将保持在您的位置(即索引)。

问题是您无法使用listView.builder,因为您需要更改列表的大小。

例子

class _ListViewIndexState extends State<ListViewIndex> {
  ScrollController _scrollController;
  List<Widget> _displayedList;
  @override
  void initState() {
    super.initState();

    _scrollController = ScrollController();

    _displayedList = widget.items.sublist(0, widget.items.length - widget.index);

    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback((_) {
//here the sublist is already build
        completeList();
      });
    }
  }

  completeList() {
//to go to the last item(in first position) 
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
//reset the list to the full list
    setState(() {
      _displayedList = widget.items;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ListView(
          controller: _scrollController,
          reverse: true,
          children: _displayedList,
        ),
      ]
    );
  }
}

1

这个https://pub.dev/packages/indexed_list_view包可能会对你有所帮助。使用类似于以下的东西:

IndexedListView.builder(
    controller: indexScrollController, 
    itemBuilder: itemBuilder
);


indexScrollController.jumpToIndex(10000);

0

工作示例:

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';

class ScrollToIndexDemo extends StatefulWidget {
  const ScrollToIndexDemo({Key? key}) : super(key: key);

  @override
  _ScrollToIndexDemoState createState() => _ScrollToIndexDemoState();
}

class _ScrollToIndexDemoState extends State<ScrollToIndexDemo> {
  late AutoScrollController controller = AutoScrollController();
  var rng = Random();
  ValueNotifier<int> scrollIndex = ValueNotifier(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: ValueListenableBuilder(
          valueListenable: scrollIndex,
          builder: (context, index, child) {
            return Text('Scroll Demo - $index');
          },
        ),
      ),
      body: ListView.builder(
        itemCount: 100,
        controller: controller,
        itemBuilder: (context, index) {
          return Padding(
            padding: EdgeInsets.all(8),
            child: AutoScrollTag(
              key: ValueKey(index),
              controller: controller,
              index: index,
              highlightColor: Colors.black.withOpacity(0.1),
              child: Container(
                padding: EdgeInsets.all(10),
                alignment: Alignment.center,
                color: Colors.grey[300],
                height: 100,
                child: Text(
                  'index: $index',
                  style: TextStyle(color: Colors.black),
                ),
              ),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          scrollIndex.value = rng.nextInt(100);
          await controller.scrollToIndex(scrollIndex.value, preferPosition: AutoScrollPosition.begin);
        },
        tooltip: 'Increment',
        child: Center(
          child: Text(
            'Next',
            textAlign: TextAlign.center,
          ),
        ),
      ),
    );
  }
}

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

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