当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?

Posted

技术标签:

【中文标题】当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?【英文标题】:When heights of items near top of the ListView.builder are cached but have changed, how to get normal behavior from animateTo? 【发布时间】:2021-08-16 23:45:02 【问题描述】:

小例子

我创建了一个最小的示例,每次单击一个项目时,列表都会滚动到该项目的顶部。同时,列表可以从有标题变为没有标题。

怎么了?

如果您在向下滚动列表时保持标题打开或关闭标题,则没有任何问题。当您切换标题时会发生不良行为,而列表中的好方法。 cacheExtent 的 firstIndex 之上的某些项目将不会被重建,因此滚动偏移量将偏离那么多标题高度。

下面是示例代码:

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

void main() 
  runApp(MyApp(
    items: List<String>.generate(10000, (i) => 'Item $i'),
  ));


class MyApp extends StatefulWidget 
  final List<String> items;
  MyApp(Key? key, required this.items) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();


class _MyAppState extends State<MyApp> 
  final _scrollController = ScrollController();
  List<bool> hasHeaders = [true, false];
  int selectedIndex = 0;

  Widget _headerR = Container(
    height: 50,
    color: Colors.amber,
    child: Text('header'),
  );

  Widget _item(required int index) 
    bool isSelected = index == selectedIndex;
    void _handleItemTap() 
      setState(() 
        print('setting index to $index');
        selectedIndex = index;
      );
      double futureOffset = 100.0 * index;
      if (hasHeaders[0]) 
        futureOffset += (index + 1) * 50;
      
      Future.delayed(Duration(milliseconds: 500), () 
        _scrollController.animateTo(futureOffset,
            duration: Duration(milliseconds: 500), curve: Curves.easeOut);
        print('scrolling to $futureOffset');
      );
    

    return Container(
      height: 100.0,
      color: isSelected ? Colors.green : Colors.white,
      child: Material(
        color: isSelected ? Colors.green : Colors.white,
        child: InkWell(
          onTap: _handleItemTap,
          child: ListTile(
            title: Text('$widget.items[index]'),
          ),
        ),
      ),
    );
  

  Widget _itemBuilder(BuildContext context, int index) 
    if (hasHeaders[0]) 
      return Column(
        children: [_headerR, _item(index: index)],
      );
     else 
      return _item(index: index);
    
  

  @override
  Widget build(BuildContext context) 
    final title = 'Long List';

    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Column(
          children: [
            Flexible(
              flex: 1,
              child: ListView.builder(
                  controller: _scrollController,
                  itemCount: widget.items.length,
                  itemBuilder: _itemBuilder),
            ),
            ToggleButtons(
                onPressed: (int index) 
                  setState(() 
                    for (int buttonIndex = 0;
                        buttonIndex < hasHeaders.length;
                        buttonIndex++) 
                      if (buttonIndex == index) 
                        hasHeaders[buttonIndex] = true;
                       else 
                        hasHeaders[buttonIndex] = false;
                      
                    
                  );
                ,
                children: [Text('headers on'), Text('headers off')],
                isSelected: hasHeaders)
          ],
        ),
      ),
    );
  


【问题讨论】:

【参考方案1】:

一种解决方案是在每次切换标题时重新初始化一个新的 ScrollController(具有指定的初始偏移量),并为 ListView 提供不同的 ValueKey。这将重置缓存大小计算。虽然可能不如理想的解决方案表现出色(像 React-Virtualized 的 rowHeight 回调可能会很好),但在这个简单的示例中,当它位于列表顶部时,它的性能似乎并不算太​​差。

当列表很长时,它的性能确实很差,尤其是当项目高度不同时。更改项目的高度可能需要几秒钟。与此问题相关:https://github.com/flutter/flutter/issues/52207

这是代码


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

void main() 
  runApp(MyApp(
    items: List<String>.generate(10000, (i) => 'Item $i'),
  ));


class MyApp extends StatefulWidget 
  final List<String> items;
  MyApp(Key? key, required this.items) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();


class _MyAppState extends State<MyApp> 
  late ScrollController _scrollController = ScrollController();
  List<bool> hasHeaders = [true, false];
  int selectedIndex = 0;

  Widget _heading = Container(
    height: 50,
    color: Colors.amber,
    child: Text('heading'),
  );

  Widget _item(required int index) 
    bool isSelected = index == selectedIndex;
    void _handleItemTap() 
      setState(() 
        print('setting index to $index');
        selectedIndex = index;
      );
      double futureOffset = 100.0 * index;
      if (hasHeaders[0]) 
        futureOffset += (index + 1) * 50;
      
      _scrollController.animateTo(futureOffset,
          duration: Duration(milliseconds: 800), curve: Curves.ease);
      print('scrolling to $futureOffset');
    

    return Container(
      height: 100.0,
      color: isSelected ? Colors.green : Colors.white,
      child: Material(
        color: isSelected ? Colors.green : Colors.white,
        child: InkWell(
          onTap: _handleItemTap,
          child: ListTile(
            title: Text('$widget.items[index]'),
          ),
        ),
      ),
    );
  

  Widget _itemBuilderWithHeaders(BuildContext context, int index) 
    return _itemBuilder(context, index, true);
  

  Widget _itemBuilderWithoutHeaders(BuildContext context, int index) 
    return _itemBuilder(context, index, false);
  

  Widget _itemBuilder(BuildContext context, int index, bool hasHeader) 
    if (hasHeader) 
      return Column(
        children: [_heading, _item(index: index)],
      );
     else 
      return _item(index: index);
    
  

  ListView _list(bool withHeaders) 
    return ListView.builder(
        key: ValueKey(withHeaders ? 'listWithHeaders' : 'compactList'),
        controller: _scrollController,
        itemCount: widget.items.length,
        itemBuilder:
            withHeaders ? _itemBuilderWithHeaders : _itemBuilderWithoutHeaders);
  

  @override
  Widget build(BuildContext context) 
    final title = 'Long List';
    return MaterialApp(
      title: title,
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Column(
          children: [
            Flexible(
              flex: 1,
              child: hasHeaders[0] ? _list(true) : _list(false),
            ),
            ToggleButtons(
                onPressed: (int index) 
                  double initialScrollOffset = _scrollController.offset;
                  if (hasHeaders[0]) 
                    int numItemsAbove = _scrollController.offset ~/ 150;
                    initialScrollOffset =
                        initialScrollOffset - (numItemsAbove + 1) * 50;
                   else 
                    int numItemsAbove = _scrollController.offset ~/ 100;
                    initialScrollOffset =
                        initialScrollOffset + numItemsAbove * 50;
                  
                  setState(() 
                    for (int buttonIndex = 0;
                        buttonIndex < hasHeaders.length;
                        buttonIndex++) 
                      if (buttonIndex == index) 
                        hasHeaders[buttonIndex] = true;
                       else 
                        hasHeaders[buttonIndex] = false;
                      
                    

                    _scrollController = ScrollController(
                        initialScrollOffset: initialScrollOffset);
                  );
                ,
                children: [Text('headers on'), Text('headers off')],
                isSelected: hasHeaders)
          ],
        ),
      ),
    );
  

【讨论】:

以上是关于当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?的主要内容,如果未能解决你的问题,请参考以下文章

当您一直滚动到 ListView.builder 的顶部时,如何禁用动画

如何在颤动中使我的文本与 listview.builder 一起滚动

Stream 内的 Flutter ListView.Builder 不更新

在顶部添加新项目时在 ListView 中保持滚动位置

Flutter ListView.builder 创建一个无限滚动。如何将其设置为在内容结束时停止?

Flutter - 另一个Listview内的Listview.builder