Flutter ListView双向延迟加载(上,下) [英] Flutter ListView lazy loading in both directions (up, down)

查看:90
本文介绍了Flutter ListView双向延迟加载(上,下)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望在Flutter中有一个ListView,它提供两个方向(上,下)的延迟加载.

I would like to have a ListView in flutter which provides lazy loading in both directions (up, down).

示例:

  • 从理论上讲,后端数据库中有60000项可以显示.
  • 首先我要显示项目100..120
  • 从这些索引中,我希望能够在懒加载新项目的同时上下滚动

注意事项:

  • 如果到达滚动

我尝试过的事情:

  • Most of the approaches in Flutter ListView lazy loading. These solutions just worked for lazy loading in one direction (e.g. down or up if list is reversed). If scrolling into the other direction, the list view "jumped" because indices changed (the old index 1 isn't the new index 1 anymore).
  • ScrollablePositionedList (https://pub.dev/documentation/flutter_widgets/latest/flutter_widgets/ScrollablePositionedList-class.html): Here the issue was that the widget wants to load every item if e.g. an itemcount of 60000 is given. Anyhow, the itemcount is required in order that this solution works properly.
  • IndexedListView (https://pub.dev/packages/indexed_list_view): Same issue as in ScrollablePositionedList. Anyhow, here also the "bouncing" at the top and bottom of the list didn't work (because of missing scroll extents).
  • InfiniteListView (https://github.com/fluttercommunity/flutter_infinite_listview): Same issue as in IndexedListView

我希望这里有一些很聪明的人可以帮助我解决这个问题;).几天以来,我已经在搜索并尝试此问题.谢谢!

I hope that here are some pretty smart people who can help me solving this issue ;). I'm already searching and trying around since days on this issue. Thanks!

更新为了使事情更清楚:这是一个ListView的示例,它具有用于上下滚动的延迟加载(大多数代码是从 https:/RémiRousselet的/stackoverflow.com/a/49509349/10905712 ):

Update To make things clearer: Here is an example for a ListView with lazy loading for scrolling up and down (Most of the code copied from https://stackoverflow.com/a/49509349/10905712 by Rémi Rousselet):

import 'dart:math';

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

class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => new _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  ScrollController controller;
  List<String> items = new List.generate(100, (index) => 'Hello $index');

  @override
  void initState() {
    super.initState();
    controller = new ScrollController()..addListener(_scrollListener);
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Scrollbar(
        child: new ListView.builder(
          controller: controller,
          itemBuilder: (context, index) {
            return new Text(items[index]);
          },
          itemCount: items.length,
        ),
      ),
    );
  }

  double oldScrollPosition = 0.0;
  void _scrollListener() {
    bool scrollingDown = oldScrollPosition < controller.position.pixels;
    print(controller.position.extentAfter);

    if (controller.position.extentAfter < 500 && scrollingDown) {
      setState(() {
        items.addAll(new List.generate(
            42, (int index) => Random().nextInt(10000).toString()));
      });
    } else if (controller.position.extentBefore < 500 && !scrollingDown) {
      setState(() {
        items.insertAll(
            0,
            new List.generate(
                42, (index) => Random().nextInt(10000).toString()));
      });
    }

    oldScrollPosition = controller.position.pixels;
  }
}

如果执行此代码并尝试向上滚动,则列表中将显示跳转".向下滚动+延迟加载效果完美.如果将ListView反转,则向上滚动+延迟加载将起作用.无论如何,使用此解决方案,我们在此处向下滚动+延迟加载会遇到相同的问题.

If you execute this code and try scrolling up, you'll see a "jumping" in the list. Scrolling down + lazy load works perfectly. Scrolling up + lazy load would work if the ListView would be reversed. Anyhow, with this solution we would have the same issue with scrolling down + lazy load here.

推荐答案

更新

我刚刚创建了一个新库 bidirectional_listview ,该库可用于解决此问题.BidirectionalListView是 infinite_listview 的分支.

I just created a new library bidirectional_listview which can be used to solve this issue. BidirectionalListView is a fork from infinite_listview.

旧答案

我只是通过稍微修改库InfiniteListView来解决了它.我必须为minScrollExtent和maxScrollExtent扩展一个setter.另外,我为负索引添加了一个单独的计数:

I just solved it by adapting the library InfiniteListView a bit. I had to extend a setter for minScrollExtent and maxScrollExtent. In addition, I added a separate count for negative indices:

library infinite_listview;

import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

/// Infinite ListView
///
/// ListView that builds its children with to an infinite extent.
///
class BidirectionalListView extends StatelessWidget {
  /// See [ListView.builder]
  BidirectionalListView.builder({
    Key key,
    this.scrollDirection = Axis.vertical,
    BidirectionalScrollController controller,
    this.physics,
    this.padding,
    this.itemExtent,
    @required IndexedWidgetBuilder itemBuilder,
    int itemCount,
    int negativeItemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    this.anchor = 0.0,
    this.cacheExtent,
  })  : positiveChildrenDelegate = SliverChildBuilderDelegate(
          itemBuilder,
          childCount: itemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        negativeChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) => itemBuilder(context, -1 - index),
          childCount: negativeItemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        controller = controller ?? BidirectionalScrollController(),
        super(key: key);

  /// See [ListView.separated]
  BidirectionalListView.separated({
    Key key,
    this.scrollDirection = Axis.vertical,
    BidirectionalScrollController controller,
    this.physics,
    this.padding,
    @required IndexedWidgetBuilder itemBuilder,
    @required IndexedWidgetBuilder separatorBuilder,
    int itemCount,
    int negativeItemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    this.cacheExtent,
    this.anchor = 0.0,
  })  : assert(itemBuilder != null),
        assert(separatorBuilder != null),
        itemExtent = null,
        positiveChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            final itemIndex = index ~/ 2;
            return index.isEven
                ? itemBuilder(context, itemIndex)
                : separatorBuilder(context, itemIndex);
          },
          childCount: itemCount != null ? math.max(0, itemCount * 2 - 1) : null,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        negativeChildrenDelegate = SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            final itemIndex = (-1 - index) ~/ 2;
            return index.isOdd
                ? itemBuilder(context, itemIndex)
                : separatorBuilder(context, itemIndex);
          },
          childCount: negativeItemCount,
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
        ),
        controller = controller ?? BidirectionalScrollController(),
        super(key: key);

  /// See: [ScrollView.scrollDirection]
  final Axis scrollDirection;

  /// See: [ScrollView.controller]
  final BidirectionalScrollController controller;

  /// See: [ScrollView.physics]
  final ScrollPhysics physics;

  /// See: [BoxScrollView.padding]
  final EdgeInsets padding;

  /// See: [ListView.itemExtent]
  final double itemExtent;

  /// See: [ScrollView.cacheExtent]
  final double cacheExtent;

  /// See: [ScrollView.anchor]
  final double anchor;

  /// See: [ListView.childrenDelegate]
  final SliverChildDelegate negativeChildrenDelegate;

  /// See: [ListView.childrenDelegate]
  final SliverChildDelegate positiveChildrenDelegate;

  @override
  Widget build(BuildContext context) {
    final List<Widget> slivers = _buildSlivers(context, negative: false);
    final List<Widget> negativeSlivers = _buildSlivers(context, negative: true);
    final AxisDirection axisDirection = _getDirection(context);
    final scrollPhysics = AlwaysScrollableScrollPhysics(parent: physics);
    return Scrollable(
      axisDirection: axisDirection,
      controller: controller,
      physics: scrollPhysics,
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return Builder(builder: (BuildContext context) {
          /// Build negative [ScrollPosition] for the negative scrolling [Viewport].
          final state = Scrollable.of(context);
          final negativeOffset = BidirectionalScrollPosition(
            physics: scrollPhysics,
            context: state,
            initialPixels: -offset.pixels,
            keepScrollOffset: controller.keepScrollOffset,
            negativeScroll: true,
          );

          /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition].
          offset.addListener(() {
            negativeOffset._forceNegativePixels(offset.pixels);
          });

          /// Stack the two [Viewport]s on top of each other so they move in sync.
          return Stack(
            children: <Widget>[
              Viewport(
                axisDirection: flipAxisDirection(axisDirection),
                anchor: 1.0 - anchor,
                offset: negativeOffset,
                slivers: negativeSlivers,
                cacheExtent: cacheExtent,
              ),
              Viewport(
                axisDirection: axisDirection,
                anchor: anchor,
                offset: offset,
                slivers: slivers,
                cacheExtent: cacheExtent,
              ),
            ],
          );
        });
      },
    );
  }

  AxisDirection _getDirection(BuildContext context) {
    return getAxisDirectionFromAxisReverseAndDirectionality(
        context, scrollDirection, false);
  }

  List<Widget> _buildSlivers(BuildContext context, {bool negative = false}) {
    Widget sliver;
    if (itemExtent != null) {
      sliver = SliverFixedExtentList(
        delegate:
            negative ? negativeChildrenDelegate : positiveChildrenDelegate,
        itemExtent: itemExtent,
      );
    } else {
      sliver = SliverList(
          delegate:
              negative ? negativeChildrenDelegate : positiveChildrenDelegate);
    }
    if (padding != null) {
      sliver = new SliverPadding(
        padding: negative
            ? padding - EdgeInsets.only(bottom: padding.bottom)
            : padding - EdgeInsets.only(top: padding.top),
        sliver: sliver,
      );
    }
    return <Widget>[sliver];
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new EnumProperty<Axis>('scrollDirection', scrollDirection));
    properties.add(new DiagnosticsProperty<ScrollController>(
        'controller', controller,
        showName: false, defaultValue: null));
    properties.add(new DiagnosticsProperty<ScrollPhysics>('physics', physics,
        showName: false, defaultValue: null));
    properties.add(new DiagnosticsProperty<EdgeInsetsGeometry>(
        'padding', padding,
        defaultValue: null));
    properties
        .add(new DoubleProperty('itemExtent', itemExtent, defaultValue: null));
    properties.add(
        new DoubleProperty('cacheExtent', cacheExtent, defaultValue: null));
  }
}

/// Same as a [ScrollController] except it provides [ScrollPosition] objects with infinite bounds.
class BidirectionalScrollController extends ScrollController {
  /// Creates a new [BidirectionalScrollController]
  BidirectionalScrollController({
    double initialScrollOffset = 0.0,
    bool keepScrollOffset = true,
    String debugLabel,
  }) : super(
          initialScrollOffset: initialScrollOffset,
          keepScrollOffset: keepScrollOffset,
          debugLabel: debugLabel,
        );

  @override
  ScrollPosition createScrollPosition(ScrollPhysics physics,
      ScrollContext context, ScrollPosition oldPosition) {
    return new BidirectionalScrollPosition(
      physics: physics,
      context: context,
      initialPixels: initialScrollOffset,
      keepScrollOffset: keepScrollOffset,
      oldPosition: oldPosition,
      debugLabel: debugLabel,
    );
  }
}

class BidirectionalScrollPosition extends ScrollPositionWithSingleContext {
  BidirectionalScrollPosition({
    @required ScrollPhysics physics,
    @required ScrollContext context,
    double initialPixels = 0.0,
    bool keepScrollOffset = true,
    ScrollPosition oldPosition,
    String debugLabel,
    this.negativeScroll = false,
  })  : assert(negativeScroll != null),
        super(
          physics: physics,
          context: context,
          initialPixels: initialPixels,
          keepScrollOffset: keepScrollOffset,
          oldPosition: oldPosition,
          debugLabel: debugLabel,
        ) {
    if (oldPosition != null &&
        oldPosition.minScrollExtent != null &&
        oldPosition.maxScrollExtent != null) {
      _minScrollExtent = oldPosition.minScrollExtent;
      _maxScrollExtent = oldPosition.maxScrollExtent;
    }
  }

  final bool negativeScroll;

  void _forceNegativePixels(double value) {
    super.forcePixels(-value);
  }

  @override
  double get minScrollExtent => _minScrollExtent;
  double _minScrollExtent = 0.0;

  @override
  double get maxScrollExtent => _maxScrollExtent;
  double _maxScrollExtent = 0.0;

  void setMinMaxExtent(double minExtent, double maxExtent) {
    _minScrollExtent = minExtent;
    _maxScrollExtent = maxExtent;
  }

  @override
  void saveScrollOffset() {
    if (!negativeScroll) {
      super.saveScrollOffset();
    }
  }

  @override
  void restoreScrollOffset() {
    if (!negativeScroll) {
      super.restoreScrollOffset();
    }
  }
}

以下示例演示了在上下两个方向上具有滚动边界的延迟加载:

The following example demonstrates lazy loading with scroll boundaries in both directions, up and down:

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tiverme/ui/helpwidgets/BidirectionalListView.dart';

class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => new _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  BidirectionalScrollController controller;
  Map<int, String> items = new Map();
  static const double ITEM_HEIGHT = 30;

  @override
  void initState() {
    super.initState();

    for (int i = -10; i <= 10; i++) {
      items[i] = "Item " + i.toString();
    }
    controller = new BidirectionalScrollController()
      ..addListener(_scrollListener);
  }

  @override
  void dispose() {
    controller.removeListener(_scrollListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    List<int> keys = items.keys.toList();
    keys.sort();

    int negativeItemCount = keys.first;
    int itemCount = keys.last;
    print("itemCount = " + itemCount.toString());
    print("negativeItemCount = " + negativeItemCount.abs().toString());
    return new Scaffold(
      body: new Scrollbar(
        child: new BidirectionalListView.builder(
          controller: controller,
          physics: AlwaysScrollableScrollPhysics(),
          itemBuilder: (context, index) {
            return Container(
                child: Text(items[index]),
                height: ITEM_HEIGHT,
                padding: EdgeInsets.all(0),
                margin: EdgeInsets.all(0));
          },
          itemCount: itemCount,
          negativeItemCount: negativeItemCount.abs(),
        ),
      ),
    );
  }

  void _rebuild() => setState(() {});

  double oldScrollPosition = 0.0;
  void _scrollListener() {
    bool scrollingDown = oldScrollPosition < controller.position.pixels;
    List<int> keys = items.keys.toList();
    keys.sort();
    int negativeItemCount = keys.first.abs();
    int itemCount = keys.last;

    double positiveReloadBorder = (itemCount * ITEM_HEIGHT - 3 * ITEM_HEIGHT);
    double negativeReloadBorder =
        (-(negativeItemCount * ITEM_HEIGHT - 3 * ITEM_HEIGHT));

    print("pixels = " + controller.position.pixels.toString());
    print("itemCount = " + itemCount.toString());
    print("negativeItemCount = " + negativeItemCount.toString());
    print("minExtent = " + controller.position.minScrollExtent.toString());
    print("maxExtent = " + controller.position.maxScrollExtent.toString());
    print("positiveReloadBorder = " + positiveReloadBorder.toString());
    print("negativeReloadBorder = " + negativeReloadBorder.toString());

    bool rebuildNecessary = false;
    if (scrollingDown && controller.position.pixels > positiveReloadBorder) {
      for (int i = itemCount + 1; i <= itemCount + 20; i++) {
        items[i] = "Item " + i.toString();
      }
      rebuildNecessary = true;
    } else if (!scrollingDown &&
        controller.position.pixels < negativeReloadBorder) {
      for (int i = -negativeItemCount - 20; i < -negativeItemCount; i++) {
        items[i] = "Item " + i.toString();
      }
      rebuildNecessary = true;
    }

    try {
      BidirectionalScrollPosition pos = controller.position;
      pos.setMinMaxExtent(
          -negativeItemCount * ITEM_HEIGHT, itemCount * ITEM_HEIGHT);
    } catch (error) {
      print(error.toString());
    }
    if (rebuildNecessary) {
      _rebuild();
    }

    oldScrollPosition = controller.position.pixels;
  }
}

这篇关于Flutter ListView双向延迟加载(上,下)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆