如何检查ListView滚动位置是否在顶部或底部?

79

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

我试过在NotificationListener中使用ListView来检测滚动事件,但是我找不到一个事件来判断滚动是否已经到达视图的底部。

有什么最好的方法可以实现这个功能呢?

11个回答

165

通常有两种方法来完成它。

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,
  ),
)

1
谢谢,你节省了我的时间。 - DolDurma
3
这个答案应该排名更靠前。尝试了上面的回答,但这个答案更简洁、更易实现。 - Maki
3
它可以检测到列表的顶部,但无法检测到底部。 - Syed Ali Raza Bokhari
@CopsOnRoad,当我想使用另一个listview.builder并在第一个list view Item Builder内跟踪滚动时,如何解决scroll-controller错误?我的意思是:ListTile(title: Text('Item $i'))我的目标是使用嵌套/更内部的列表,并在内部跟踪滚动的顶部/底部,以更改物理属性并滚动到直接父列表视图。 - Jamshed Alam
像这个改进:https://github.com/nirav4273/flutter_nested_listview/issues,https://stackoverflow.com/questions/69422101/flutter-nested-list-scroll-parent-child-scroll-control-is-not-working,https://dev59.com/oWsMtIcB2Jgan1znsjvi。 - Jamshed Alam
显示剩余2条评论

45

使用ListView.builder可以创建一个无限滚动的列表,itemBuilder会在新的单元格被显示时按需调用。

如果你想要在滚动事件发生时得到通知,以便从网络加载更多数据,可以传递一个controller参数并使用addListener将一个监听器附加到ScrollController上。可以利用ScrollControllerposition来确定滚动是否接近底部。


1
请问您能否解释一下如何通过ScrollController确定ListView的最大滚动偏移量? - toregua
17
我已找到解决方案:scrollController.position.maxScrollExtent。 - toregua

39
_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的回答应该被接受....


1
@Pierre.Vriens 是 noam-aghai 问题的答案... 这段代码创建了一个 scrollController 用于控制列表,并附加了一个监听器,让您知道何时到达其末尾并继续加载更多内容或执行其他操作。 - Esteban Díaz
1
我们根据什么来定义“delta”的值? - Anuj Sharma
@AnujSharma 没有具体的标准...这只是为了举例而随机生成的数字...你可以完全忽略它并实现一些不同的东西(比如在列表末尾显示一个加载器)... - Esteban Díaz
1
请确保在此函数中限制您的 API 调用次数,因为如果您在滚动结束时且第一个 API 调用需要时间返回数据,我将尽可能多地调用它。 - Natwar Singh
@NatwarSingh 我们可以设置一个变量标志来知道是否有任何 API 调用正在运行。如果没有 API 正在运行,则可以向服务器发送 API 请求,或者在相反的情况下系统将不会向服务器发送任何 API 请求。 - Kamlesh

19

我想为Collin Jackson提供的答案添加一个示例。请参考以下代码段

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

仅当列表中最后一项可见时,才会触发此操作。


6
更简单的方法可以像这样:

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.atEdge 在用户位于顶部或底部滚动位置时均为true
log 方法来自于包import 'dart:developer';


3
我认为这个回答是Esteban的回答的补充(包括扩展方法和节流器),但它也是一个有效的答案,所以在这里它是:

Dart 最近(不确定)推出了一个很好的功能--方法扩展,使我们可以像写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"。链接地址为:https://quickbirdstudios.com/blog/dart-extension-methods/

2
您可以使用以下任何一种条件:
 NotificationListener<ScrollNotification>(
    onNotification: (notification) {
      final metrices = notification.metrics;

      if (metrices.atEdge && metrices.pixels == 0) {
        //you are at top of  list

      }
      
      if (metrices.pixels == metrices.minScrollExtent) {
         //you are at top of list
      }

      if (metrices.atEdge && metrices.pixels > 0) {
        //you are at end of  list

      }

      if (metrices.pixels >= metrices.maxScrollExtent) {
        //you are at end of list
      }

      return false;
    },
     child: ListView.builder());

2
  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);
 }

请提供一些解释,如果你希望你的代码有用。 - Milvintsiss

1
我使用了不同的方法来实现无限滚动。我使用了ChangeNotifier类作为变量更改监听器。如果变量发生更改,它会触发事件并最终调用API。
class DashboardAPINotifier extends ChangeNotifier {
   bool _isLoading = false;
    get getIsLoading => _isLoading;
    set setLoading(bool isLoading) => _isLoading = isLoading;
}

初始化 DashboardAPINotifier 类。

@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();
        }

1
使用ScrollController方法会更容易,而且这样做可以避免很多混乱的代码。 - SacWebDeveloper

1
你可以使用包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(),
    ],
  ),
),

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接