如何检查 ListView 的滚动位置是在顶部还是底部?

How to check if scroll position is at top or bottom in ListView?

我正在尝试实现无限滚动功能。

我尝试在 NotificationListener 上使用 ListView 来检测滚动事件,但我看不到指示滚动是否已到达视图底部的事件。

实现此目标的最佳方法是什么?

您可以使用 ListView.builder 创建包含无限项目的滚动列表。当显示新单元格时,您的 itemBuilder 将根据需要被调用。

如果您想收到有关滚动事件的通知,以便可以从网络加载更多数据,您可以传递 controller 参数并使用 addListener 将侦听器附加到 ScrollController. ScrollControllerposition可以用来判断滚动是否接近底部

_scrollController = new ScrollController();

    _scrollController.addListener(
        () {
            double maxScroll = _scrollController.position.maxScrollExtent;
            double currentScroll = _scrollController.position.pixels;
            double delta = 200.0; // or something else..
            if ( maxScroll - currentScroll <= delta) { // whatever you determine here
                //.. load more
            }
        }
    );

Collin 的答案应该被接受....

我使用了不同的无限滚动方法。我使用 ChangeNotifier class 作为变量变化监听器。 如果变量发生变化,它会触发事件并最终命中 API.

class DashboardAPINotifier extends ChangeNotifier {
   bool _isLoading = false;
    get getIsLoading => _isLoading;
    set setLoading(bool isLoading) => _isLoading = isLoading;
}

初始化仪表板API通知程序class。

@override
  void initState() {
    super.initState();
    _dashboardAPINotifier = DashboardAPINotifier();
    _hitDashboardAPI(); // init state

    _dashboardAPINotifier.addListener(() {
      if (_dashboardAPINotifier.getIsLoading) {
        print("loading is true");
        widget._page++; // For API page
        _hitDashboardAPI(); //Hit API
      } else {
        print("loading is false");
      }
    });

  }

现在最好的部分是你必须点击 API。 如果你用的是SliverList,那么你必须在什么时候打API。

SliverList(delegate: new SliverChildBuilderDelegate(
       (BuildContext context, int index) {
        Widget listTile = Container();
         if (index == widget._propertyList.length - 1 &&
             widget._propertyList.length <widget._totalItemCount) {
             listTile = _reachedEnd();
            } else {
                    listTile = getItem(widget._propertyList[index]);
                   }
            return listTile;
        },
          childCount: (widget._propertyList != null)? widget._propertyList.length: 0,
    addRepaintBoundaries: true,
    addAutomaticKeepAlives: true,
 ),
)


_reachEnd() method take care to hit the api. It trigger the `_dashboardAPINotifier._loading`

// Function that initiates a refresh and returns a CircularProgressIndicator - Call when list reaches its end
  Widget _reachedEnd() {
    if (widget._propertyList.length < widget._totalItemCount) {
      _dashboardAPINotifier.setLoading = true;
      _dashboardAPINotifier.notifyListeners();
      return const Padding(
        padding: const EdgeInsets.all(20.0),
        child: const Center(
          child: const CircularProgressIndicator(),
        ),
      );
    } else {
      _dashboardAPINotifier.setLoading = false;
      _dashboardAPINotifier.notifyListeners();
      print("No more data found");
      Utils.getInstance().showSnackBar(_globalKey, "No more data found");
    }
  }

注意:在你的API回应后你需要通知听众,

setState(() {
        _dashboardAPINotifier.setLoading = false;
        _dashboardAPINotifier.notifyListeners();
        }

我想为 添加示例。参考以下片段

    var _scrollController = ScrollController();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        // Perform your task
      }
    });

仅当最后一项在列表中可见时才会触发。

通常有两种方法。

1.使用 ScrollController

// Create a variable
final _controller = ScrollController();
  
@override
void initState() {
  super.initState();
  
  // Setup the listener.
  _controller.addListener(() {
    if (_controller.position.atEdge) {
      bool isTop = _controller.position.pixels == 0;
      if (isTop) {
        print('At the top');
      } else {
        print('At the bottom');
      }
    }
  });
}

用法:

ListView(controller: _controller) // Assign the controller.

2。使用 NotificationListener

NotificationListener<ScrollEndNotification>(
  onNotification: (scrollEnd) {
    final metrics = scrollEnd.metrics;
    if (metrics.atEdge) {
      bool isTop = metrics.pixels == 0;
      if (isTop) {
        print('At the top');
      } else {
        print('At the bottom');
      }
    }
    return true;
  },
  child: ListView.builder(
    physics: ClampingScrollPhysics(),
    itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
    itemCount: 20,
  ),
)

我觉得这个答案是对 Esteban 的答案的补充(带有扩展方法和限制),但它也是一个有效的答案,所以这里是:

Dart 最近(不确定)有一个很好的特性,method extensions,它允许我们像 ScrollController:

的一部分一样编写 onBottomReach 方法
import 'dart:async';

import 'package:flutter/material.dart';

extension BottomReachExtension on ScrollController {
  void onBottomReach(VoidCallback callback,
      {double sensitivity = 200.0, Duration throttleDuration}) {
    final duration = throttleDuration ?? Duration(milliseconds: 200);
    Timer timer;

    addListener(() {
      if (timer != null) {
        return;
      }

      // I used the timer to destroy the timer
      timer = Timer(duration, () => timer = null);

      // see Esteban Díaz answer
      final maxScroll = position.maxScrollExtent;
      final currentScroll = position.pixels;
      if (maxScroll - currentScroll <= sensitivity) {
        callback();
      }
    });
  }
}

这是一个用法示例:

// if you're declaring the extension in another file, don't forget to import it here.

class Screen extends StatefulWidget {
  Screen({Key key}) : super(key: key);

  @override
  _ScreenState createState() => _ScreenState();
}

class _ScreenState extends State<Screen> {
  ScrollController_scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController()
      ..onBottomReach(() {
        // your code goes here
      }, sensitivity: 200.0, throttleDuration: Duration(milliseconds: 500));
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}

注意:如果您使用方法扩展,则需要配置一些内容,请参阅 "How to enable Dart Extension Methods"

更简单的方法是这样的:

NotificationListener<ScrollEndNotification>(
    onNotification: onNotification,
    child: <a ListView or Wrap or whatever widget you need>
)

并创建一个检测位置的方法:

 bool onNotification(ScrollEndNotification t) {
   if (t.metrics.pixels >0 && t.metrics.atEdge) {
     log('I am at the end');
   } else {
     log('I am at the start')
   }
   return true;
}

t.metrics.pixel 当用户将滚动条放在顶部时为 0,当确定滚动条时大于 0。
t.metrics.atEdgetrue,当用户在滚动条 的顶部或 在滚动条
的末尾时 log 方法来自包 import 'dart:developer';

  final ScrollController controller = ScrollController();


  void _listener() {

  double maxPosition = controller.position.maxScrollExtent;
  double currentPosition = controller.position.pixels;


  /// You can change this value . It's a default value for the 
  /// test if the difference between the great value and the current value is smaller 
  /// or equal
  double difference = 10.0;

  /// bottom position
  if ( maxPosition - currentPosition <= difference )
   
 
  /// top position
  else
   




if(mounted)
  setState(() {}); 
 }


@override
void initState() {
  super.initState();
  controller.addListener(_listener);
 }

您可以使用包 scroll_edge_listener

它带有一个非常有用的偏移和去抖动时间配置。用 ScrollEdgeListener 包裹你的滚动视图并附加一个监听器。就是这样。

ScrollEdgeListener(
  edge: ScrollEdge.end,
  edgeOffset: 400,
  continuous: false,
  debounce: const Duration(milliseconds: 500),
  dispatch: true,
  listener: () {
    debugPrint('listener called');
  },
  child: ListView(
    children: const [
      Placeholder(),
      Placeholder(),
      Placeholder(),
      Placeholder(),
    ],
  ),
),