【问题标题】:Flutter ListView.builder - How to Jump to Certain Index ProgrammaticallyFlutter ListView.builder - 如何以编程方式跳转到某个索引
【发布时间】:2019-10-29 17:36:49
【问题描述】:

我有一个使用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还没有这个功能,见this issue

有什么想法吗?

【问题讨论】:

  • 您是否已经知道要跳过的索引号或项目?
  • 嗨@AjilO。是的,因为它是有限列表,它将是其他屏幕的参数。
  • 你可能正在寻找这个stackoverflow.com/a/58809961/6668797ScrollController(initialScrollOffset: _)

标签: user-interface scroll flutter dart


【解决方案1】:

您可以使用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,
);

【讨论】:

    【解决方案2】:

    一般解决方案:

    为了存储可以表示为数字/字符串/字符串列表的任何内容,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 的 initState 时设置调用了您关注的小部件。

    为了知道检测应用状态的变化并据此采取行动,我们将继承 WidgetsBindingObserver 到我们的类。

    要遵循的步骤:

    1. 将 WidgetsBindingObserver 类与 一起扩展您的 StatefulWidget 的 State 类。

    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. 声明两个变量,一个用于存储和传递滚动控制器,另一个用于存储和使用 SharedPreferences 的实例。
      ScrollController _scrollController;
      SharedPreferences _sharedPreferences;
    
    1. 调用resumeController() 并将您的类传递给WidgetsBinding 类中实例对象的addObserver 方法。
      resumeController();
      WidgetsBinding.instance.addObserver(this);
    
    1. 只需将此代码粘贴到类定义中(在其他成员函数之外)
     @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() 传递给相关的 Scrollable。

    工作示例:

    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 覆盖所有情况并且无论应用如何“关闭”,滚动偏移量都会一直保存?
    • 另外,suspending 不是AppLifecycleState 的(不再是a)值
    【解决方案3】:

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

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

    问题是你不能使用 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,
            ),
          ]
        );
      }
    }
    

    【讨论】:

      【解决方案4】:

      https://pub.dev/packages/indexed_list_view 包可能会帮助您解决这个问题。使用这样的东西:

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

      【讨论】:

        【解决方案5】:

        我将介绍另一种方法,它支持与 @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 来决定是否“重新定位”

        【讨论】:

          【解决方案6】:

          工作示例:

          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,
                    ),
                  ),
                ),
              );
            }
          }
          

          【讨论】:

          • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
          猜你喜欢
          • 2011-03-28
          • 2022-07-20
          • 2019-03-14
          • 2011-06-08
          • 2012-10-11
          • 2017-09-26
          • 1970-01-01
          • 1970-01-01
          • 2021-07-22
          相关资源
          最近更新 更多