代码之家  ›  专栏  ›  技术社区  ›  Marcelo Glasberg

在Flutter中,我怎样才能创建一个列表视图,它的顶部和底部都是无限的?

  •  2
  • Marcelo Glasberg  · 技术社区  · 6 年前

    换言之,无限滚动到负方向和正方向。

    2 回复  |  直到 6 年前
        1
  •  4
  •   Marcelo Glasberg    6 年前

    更新:

    截至2018年12月,请使用: https://pub.dartlang.org/packages/infinite_listview

    旧答案:

    InfiniteListView.builder(itemBuilder: itemBuilder);
    

    代码如下:

    import 'dart:math';
    import 'package:flutter/material.dart';
    import 'package:flutter/rendering.dart';
    
    
    void main() {
      runApp(MaterialApp(home: HomePage()));
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        //
        List<double> heights =
            new List<double>.generate(100, (i) => Random().nextInt(90).toDouble() + 45.0);
    
        var itemBuilder = (BuildContext context, int index) {
          return Card(
            child: Container(
              height: heights[index % 100],
              color: Colors.green,
              child: Center(
                  child: MaterialButton(
                color: Colors.grey,
                child: Text('ITEM $index'),
                onPressed: () => print("PRESSED $index"),
              )),
            ),
          );
        };
    
        return Scaffold(
          appBar: AppBar(title: const Text('Infinite ListView')),
          body: InfiniteListView.builder(itemBuilder: itemBuilder),
        );
      }
    }
    
    //////////////////////////////////////////////////////////////////
    
    class InfiniteListView extends StatefulWidget {
      //
      const InfiniteListView.builder({Key key, this.itemBuilder}) : super(key: key);
    
      final IndexedWidgetBuilder itemBuilder;
    
      @override
      _InfinitListViewState createState() => _InfinitListViewState();
    }    
    
    class _UnboundedScrollPosition extends ScrollPositionWithSingleContext {
      _UnboundedScrollPosition({
        ScrollPhysics physics,
        ScrollContext context,
        ScrollPosition oldPosition,
        double initialPixels,
      }) : super(
              physics: physics,
              context: context,
              oldPosition: oldPosition,
              initialPixels: initialPixels,
            );
    
      @override
      double get minScrollExtent => double.negativeInfinity;
    
      /// There is a feedback-loop between aboveController and belowController. When one of them is
      /// being used, it controlls the other. However if they get out of sync, for timing reasons,
      /// the controlled one with try to controll the other, and the jump will stop the real controller.
      /// For this reason, we can't let one stop the other (idle and ballistics) in this situattion.
      void jumpToWithoutGoingIdleAndKeepingBallistic(double value) {
        if (pixels != value) {
          forcePixels(value);
        }
      }
    }
    
    class _UnboundedScrollController extends ScrollController {
      //
      _UnboundedScrollController({
        double initialScrollOffset = 0.0,
        keepScrollOffset = true,
        debugLabel,
      }) : super(
                initialScrollOffset: initialScrollOffset,
                keepScrollOffset: keepScrollOffset,
                debugLabel: debugLabel);
    
      @override
      _UnboundedScrollPosition createScrollPosition(
        ScrollPhysics physics,
        ScrollContext context,
        ScrollPosition oldPosition,
      ) {
        return _UnboundedScrollPosition(
          physics: physics,
          context: context,
          oldPosition: oldPosition,
          initialPixels: initialScrollOffset,
        );
      }
    
      void jumpToWithoutGoingIdleAndKeepingBallistic(double value) {
        assert(positions.isNotEmpty, 'ScrollController not attached.');
        for (_UnboundedScrollPosition position in new List<ScrollPosition>.from(positions))
          position.jumpToWithoutGoingIdleAndKeepingBallistic(value);
      }
    }
    
    class _InfinitListViewState extends State<InfiniteListView> {
      //
      _UnboundedScrollController _positiveController;
      _UnboundedScrollController _negativeController;
    
      @override
      void initState() {
        super.initState();
    
        // Instantiate the negative and positive list positions, relative to one another.
        WidgetsBinding.instance.addPostFrameCallback((_) => _negativeController
            .jumpTo(-_negativeController.position.extentInside - _positiveController.position.pixels));
    
        _positiveController?.dispose();
        _negativeController?.dispose();
        _positiveController = _UnboundedScrollController(keepScrollOffset: false);
        _negativeController = _UnboundedScrollController();
    
        // ---
    
        // The POSITIVE list moves the NEGATIVE list, but only if the NEGATIVE list position would change.
        _positiveController.addListener(() {
          var newNegativePosition =
              -_negativeController.position.extentInside - _positiveController.position.pixels;
          var oldNegativePosition = _negativeController.position.pixels;
    
          if (newNegativePosition != oldNegativePosition) {
            _negativeController.jumpToWithoutGoingIdleAndKeepingBallistic(newNegativePosition);
          }
        });
    
        // ---
    
        // The NEGATIVE list moves the POSITIVE list, but only if the POSITIVE list position would change.
        _negativeController.addListener(() {
          var newBelowPosition =
              -_positiveController.position.extentInside - _negativeController.position.pixels;
          var oldBelowPosition = _positiveController.position.pixels;
    
          if (newBelowPosition != oldBelowPosition) {
            _positiveController.jumpToWithoutGoingIdleAndKeepingBallistic(newBelowPosition);
          }
        });
      }
    
      @override
      void dispose() {
        _positiveController.dispose();
        _negativeController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        //
        var sliverList = SliverList(
          delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
            return widget.itemBuilder(context, -index - 1);
          }),
        );
    
        var negativeList = CustomScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          controller: _negativeController,
          reverse: true,
          slivers: [sliverList],
        );
    
        var positiveList = ListView.builder(
          controller: _positiveController,
          itemBuilder: (BuildContext context, int index) {
            return widget.itemBuilder(context, index);
          },
        );
    
        return Stack(
          children: <Widget>[
            negativeList,
            _ControlledIgnorePointer(
              child: positiveList,
              controller: _positiveController,
            ),
          ],
        );
      }
    }
    
    class _ControlledIgnorePointer extends SingleChildRenderObjectWidget {
      //
      final ScrollController controller;
    
      const _ControlledIgnorePointer({Key key, @required this.controller, Widget child})
          : assert(controller != null),
            super(key: key, child: child);
    
      @override
      _ControlledRenderIgnorePointer createRenderObject(BuildContext context) {
        return new _ControlledRenderIgnorePointer(controller: controller);
      }
    
      @override
      void updateRenderObject(BuildContext context, _ControlledRenderIgnorePointer renderObject) {
        renderObject..controller = controller;
      }
    }
    
    /// Render object that is invisible to hit testing in offsets that depend on the controller.
    class _ControlledRenderIgnorePointer extends RenderProxyBox {
      _ControlledRenderIgnorePointer({RenderBox child, ScrollController controller})
          : _controller = controller,
            super(child) {
        assert(_controller != null);
      }
    
      ScrollController get controller => _controller;
      ScrollController _controller;
      set controller(ScrollController value) {
        assert(value != null);
        if (value == _controller) return;
        _controller = value;
      }
    
      @override
      bool hitTest(HitTestResult hitTestResult, {Offset position}) {
        bool ignore = -controller.position.pixels > position.dy;
        var boolResult = ignore ? false : super.hitTest(hitTestResult, position: position);
        return boolResult;
      }
    }
    
        2
  •  0
  •   tatsuDn    6 年前

    https://github.com/flutter/flutter/issues/20608

    这背后的想法是,不使用CustomScrollView小部件,它实际上把您的切片放在viewPort中,您可以创建自己的ScrollView,它将定义“中心”属性( https://docs.flutter.io/flutter/widgets/Viewport/center.html https://docs.flutter.io/flutter/widgets/SliverList-class.html )