【问题标题】:How to get Flutter ScrollController to save position of ListView.builder() when changing tabs?更改选项卡时如何让 Flutter ScrollController 保存 ListView.builder() 的位置?
【发布时间】:2020-06-03 04:24:14
【问题描述】:

我用 2 个选项卡制作了一个简单的示例,每个选项卡都包含一个 ListView 构建器。我的目标是能够在第一个列表视图中滚动,切换到第二个选项卡,然后切换回第一个选项卡并查看与之前相同的滚动位置。

我已经尝试将键添加到每个列表视图中,但这只是一个猜测,因为我并不完全理解键。这没有帮助。

为什么 ScrollControllers 不保存滚动位置?

这是 main.dart 的示例:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  ScrollController controllerA = ScrollController(keepScrollOffset: true);
  ScrollController controllerB = ScrollController(keepScrollOffset: true);
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: <Widget>[
              Text('controllerA'),
              Text('controllerB'),
            ],
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            ListView.builder(
                controller: controllerA,
                itemCount: 2000,
                itemBuilder: (context, i) {
                  return ListTile(
                      title: Text(
                    i.toString(),
                    textScaleFactor: 1.5,
                    style: TextStyle(color: Colors.blue),
                  ));
                }),
            ListView.builder(
                controller: controllerB,
                itemCount: 2000,
                itemBuilder: (context, i) {
                  return Card(
                    child: ListTile(
                      title: Text(i.toString()),
                    ),
                  );
                }),
          ],
        ),
      ),
    );
  }
}

这是我想要的一个 hacky 但有效的示例。但这感觉不是正确的方法,因为它每帧都重建两个控制器。

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  double offsetA = 0.0;
  double offsetB = 0.0;

  @override
  Widget build(BuildContext context) {
    ScrollController statelessControllerA =
        ScrollController(initialScrollOffset: offsetA);
    statelessControllerA.addListener(() {
      setState(() {
        offsetA = statelessControllerA.offset;
      });
    });

    ScrollController statelessControllerB =
        ScrollController(initialScrollOffset: offsetB);
    statelessControllerB.addListener(() {
      setState(() {
        offsetB = statelessControllerB.offset;
      });
    });

    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            tabs: <Widget>[
              Text('controllerA'),
              Text('controllerB'),
            ],
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            ListView.builder(
                controller: statelessControllerA,
                itemCount: 2000,
                itemBuilder: (context, i) {
                  return ListTile(
                      title: Text(
                    i.toString(),
                    textScaleFactor: 1.5,
                    style: TextStyle(color: Colors.blue),
                  ));
                }),
            ListView.builder(
                controller: statelessControllerB,
                itemCount: 2000,
                itemBuilder: (context, i) {
                  return Card(
                    child: ListTile(
                      title: Text(i.toString()),
                    ),
                  );
                }),
          ],
        ),
      ),
    );
  }
}

【问题讨论】:

    标签: flutter dart flutter-layout flutter-web


    【解决方案1】:

    您可以使用AutomaticKeepAliveClientMixin 将状态保留在选项卡视图中。

    举例

    class GetListView extends StatefulWidget{
      @override
      State<StatefulWidget> createState() =>_GetListViewState();
    
    }
    
    class _GetListViewState extends State<GetListView> with AutomaticKeepAliveClientMixin<GetListView>{
    
      @override
      Widget build(BuildContext context){
        return ListView.builder(
    
                    itemCount: 2000,
                    itemBuilder: (context, i) {
                      return ListTile(
                          title: Text(
                        i.toString(),
                        textScaleFactor: 1.5,
                        style: TextStyle(color: Colors.blue),
                      ));
                    });
      }
    
      @override
      bool get wantKeepAlive => true;
    
    } 
    

    不要在TabBarView 的子代中使用ListView.builder,而是使用GetListView

    举例

    TabBarView(
              children: <Widget>[
                GetListView(),
                ListView.builder(
                    controller: controllerB,
                    itemCount: 2000,
                    itemBuilder: (context, i) {
                      return Card(
                        child: ListTile(
                          title: Text(i.toString()),
                        ),
                      );
                    }),
              ],
            ),
          )
    

    实现此目的的第二种方法是使用PageStorageKeyPageStorageKey 被 Scrollables 用来保存滚动偏移量。每次滚动完成时,可滚动的页面存储都会更新。

    举例

     ListView.builder(
                    key: PageStorageKey<String>('controllerA'),
                    controller: statelessControllerA,
                    itemCount: 2000,
                    itemBuilder: (context, i) {
                      print("Rebuilded 1");
                      return ListTile(
                          title: Text(
                        i.toString(),
                        textScaleFactor: 1.5,
                        style: TextStyle(color: Colors.blue),
                      ));
                    }),
    

    注意:在第二个示例中,每次都会使用特定的滚动偏移量重建小部件。建议使用第一种方案。

    【讨论】:

    • 如果控制器已经保存在有状态小部件中,为什么我需要尝试让它保持活动状态?我认为控制器的目的是避免让视图保持活力?如果不保持滚动偏移,控制器的 keepScrollOffset 属性有什么意义?
    • 如果您正在使用控制器并保存滚动偏移量以供以后使用,它会在每次切换选项卡时使用您提供的偏移量重建小部件,这是不推荐的持久化方式州。
    • 对于滚动偏移,我已经使用PageStorageKey更新了我的答案
    • 谢谢,第二种方法是我想找的,但我会尝试第一种方法,不要每次都重建。
    • 我很惊讶AutomaticKeepAliveClientMixin 导致dispose 方法永远不会被调用,因此我的bloc.close 永远不会被调用并且一切都保持不变。
    【解决方案2】:

    您可以使用PageStorageKey,它还保留滚动位置。

    一个键可用于在销毁后将小部件状态保存在存储中,并在重新创建时恢复。

    ListView.builder(
          key: PageStorageKey(0), //0 is Store index you should use a new one for each page you can also use string
    )
    

    【讨论】:

      【解决方案3】:

      你可以像这样使用pageStorage

      final PageStorageBucket appBucket = PageStorageBucket();
      saveScrollOffset(BuildContext context, double offset, String key) =>
          appBucket.writeState(context, offset, identifier: ValueKey(key));
      double currentPageScrollOffset(BuildContext context, String key) =>
          appBucket.readState(context, identifier: ValueKey(key)) ?? 0.0;
      main() {
        runApp(MaterialApp(
          home: HomeScreen(),
        ));
      }
      
      class HomeScreen extends StatelessWidget {
        HomeScreen();
      
        @override
        Widget build(BuildContext context) {
          return PageStorage(
            bucket: appBucket,
            child: Scaffold(
              body: Container(
                child: Center(
                  child: TextButton(
                      onPressed: () {
                        Navigator.of(context).push(MaterialPageRoute(
                            builder: (_) => NeedToSaveScrollPosition()));
                      },
                      child: Text('push')),
                ),
              ),
            ),
          );
        }
      }
      
      class NeedToSaveScrollPosition extends StatelessWidget {
        final String bucketOffsetKey = 'thisPageOffsetKey';
        @override
        Widget build(BuildContext context) {
          return Material(
            child: NotificationListener<ScrollNotification>(
                onNotification: (ScrollNotification pos) {
                  if (pos is ScrollEndNotification) {
                    saveScrollOffset(context, pos.metrics.pixels, bucketOffsetKey);
                    print(currentPageScrollOffset(context, bucketOffsetKey));
                  }
                  return true;
                },
                child: CustomScrollView(
                  controller: ScrollController(
                      initialScrollOffset:
                          currentPageScrollOffset(context, bucketOffsetKey)),
                  slivers: [
                    SliverList(
                        delegate: SliverChildBuilderDelegate(
                            (context, index) => Text("$index"),
                            childCount: 500))
                  ],
                )),
          );
        }
      }
      
      

      使用 NotificationListener 或 scrollController 监听器获取 listView 位置

      【讨论】:

        【解决方案4】:

        我还必须构建一个类似的功能,ListView.builder 应该保存当前滚动位置,并在第二天用户每次打开应用时从该位置开始。

        我使用scrollable_positioned_list包来实现它。

        Step- 1. 首先安装并导入包。

        第 2 步。使用 ScrollablePositionedList.builder 代替 ListView.builder

                           ScrollablePositionedList.builder(
                              itemCount: 100,
                              itemBuilder: (context, index) {
                                return Text('item number $index');
                              });
        

        第 3 步:添加 ItemPositionsListener 以获取当前滚动位置。 ItemScrollController 下次滚动到该位置。

          final ItemScrollController itemScrollController = ItemScrollController();
          final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
        
                           ScrollablePositionedList.builder(
                              itemCount: 100,
                              itemScrollController: itemScrollController,
                              itemPositionsListener: itemPositionsListener,
                              itemBuilder: (context, index) {
                                return Text('item number $index');
                              });
        

        第 4 步:像这样获取屏幕上可见的第一个和最后一个项目。

        第 5 步:并将第一项保存在 sharedPreferences 中。

        //step-4...
        Widget get positionsView => ValueListenableBuilder<Iterable<ItemPosition>>(
            valueListenable: itemPositionsListener.itemPositions,
            builder: (context, positions, child) {
              int? firstItem;
              int? lastItem;
              if (positions.isNotEmpty) {
                // Determine the first visible item by finding the item with the
                // smallest trailing edge that is greater than 0.  i.e. the first
                // item whose trailing edge in visible in the viewport.
                firstItem = positions
                    .where((ItemPosition position) => position.itemTrailingEdge > 0)
                    .reduce((ItemPosition first, ItemPosition position) =>
                        position.itemTrailingEdge < first.itemTrailingEdge ? position : first)
                    .index;
                // Determine the last visible item by finding the item with the
                // greatest leading edge that is less than 1.  i.e. the last
                // item whose leading edge in visible in the viewport.
                lastItem = positions
                    .where((ItemPosition position) => position.itemLeadingEdge < 1)
                    .reduce((ItemPosition last, ItemPosition position) =>
                        position.itemLeadingEdge > last.itemLeadingEdge ? position : last)
                    .index;
              }
        
        //Step-5....
              sharedPreferences?.setInt('scrollPosition', firstItem ?? 0);
              return SizedBox.shrink();
            },
          );
        

        Step-6:将这个positionsView getter添加到构建方法中,在ScrollablePositionedList.builder的上方或下方

                     Column(
                     children: [
                           ScrollablePositionedList.builder(
                              itemCount: 100,
                              itemScrollController: itemScrollController,
                              itemPositionsListener: itemPositionsListener,
                              itemBuilder: (context, index) {
                                return Text('item number $index');
                              }),
                            positionsView,
                        ]
                     );
        

        步骤 7:将 sharedPreferences 中保存的滚动位置添加到 ScrollablePositionedList.builderinitialScrollIndex:

        完成了。

                     Column(
                     children: [
                           ScrollablePositionedList.builder(
                              itemCount: 100,
                              itemScrollController: itemScrollController,
                              itemPositionsListener: itemPositionsListener,
                              initialScrollIndex: sharedPreferences?.getInt('scrollPosition'),
                              itemBuilder: (context, index) {
                                return Text('item number $index');
                              }),
                            positionsView,
                        ]
                     );
        

        下次建立可滚动定位列表时,将从上次保存的滚动位置开始。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-04-21
          • 2014-11-01
          • 2019-05-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-06-10
          • 1970-01-01
          相关资源
          最近更新 更多