Flutter开发之ListView下拉刷新&上拉加载更多(35)

在Flutter开发之ListView组件(21) 文章中,我们了解了ListView组件的基本使用。但是数据比较少,没有涉及分页加载。而实际开发中,下拉刷新和分页加载几乎是所有APP的标配。在iOS 开发中我们通过MJRefresh 给UITableView添加mj_header和mj_footer刷新事件来触发下拉刷新和分页加载实现的。那么我们看一下Flutter中的下拉刷新跟上拉加载更多是如何实现的尼?

1. ListView组件

首先创建一个不带RefreshIndicator ListView组件。就像之前的一样。
Flutter开发之ListView下拉刷新&上拉加载更多(35)_第1张图片
基础代码

import 'package:flutter/material.dart';
import 'package:hello/Models/Todo.dart';
import 'package:hello/Moudles/DetailScreen.dart';

class ListViewRefreshTest extends StatefulWidget {
  @override
  createState()=> _ListViewRefreshState();
}


Widget _listViewWidget (BuildContext context, List sourceList) {
  if(sourceList.length==0) {
    return null;
  } else {
    return ListView.builder(
        itemCount: sourceList.length,
        //设置physics属性总是可滚动
      	physics: AlwaysScrollableScrollPhysics(),
        itemBuilder: (context,index) {

          return ListTile(
            title: Text(sourceList[index].title,style: TextStyle(fontSize: 22),),
            subtitle: Text(sourceList[index].description,style: TextStyle(fontSize: 18)),
            onTap: () {
              Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder:(context)=> DetailScreen(todo: sourceList[index]),
                  )
              );
            },
          );
        }
    );
  }
}

List _myDataList(){
  return List.generate(20,
          (i) => new Todo(
            '我是表第 $i 项',
            '我是内容 $i',
      )
  );
}

class _ListViewRefreshState extends State {
  List entityList = _myDataList();

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(

      appBar: AppBar(
        title: Text('刷新和分页加载'),
      ),

      body: _listViewWidget(
          context,
          entityList,
      ),
    );
  }
}

下拉刷新

在Flutter中是通过RefreshIndicator组件来完成下拉刷新的监听动作的。它跟原生Android中的SwipeRefreshLayout设计思路一样,而且t在外观上几乎也一样,都是为了简化完成下拉刷新的监听动作。Google 发布的Material Design语言更像是一套界面设计标准。

class RefreshIndicator extends StatefulWidget {
  /// Creates a refresh indicator.
  ///
  /// The [onRefresh], [child], and [notificationPredicate] arguments must be
  /// non-null. The default
  /// [displacement] is 40.0 logical pixels.
  ///
  /// The [semanticsLabel] is used to specify an accessibility label for this widget.
  /// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel].
  /// An empty string may be passed to avoid having anything read by screen reading software.
  /// The [semanticsValue] may be used to specify progress on the widget.
  const RefreshIndicator({
    Key key,
    @required this.child,
    this.displacement = 40.0,
    @required this.onRefresh,
    this.color,
    this.backgroundColor,
    this.notificationPredicate = defaultScrollNotificationPredicate,
    this.semanticsLabel,
    this.semanticsValue,
  }) : assert(child != null),
       assert(onRefresh != null),
       assert(notificationPredicate != null),
       super(key: key);

  /// The widget below this widget in the tree.
  ///
  /// The refresh indicator will be stacked on top of this child. The indicator
  /// will appear when child's Scrollable descendant is over-scrolled.
  ///
  /// Typically a [ListView] or [CustomScrollView].

上面的构造方法中必要参数的注释说明,光说不如直接实战来的直接。我们在 1.中的代码基础上添加 RefreshIndicator组件来完成下拉刷新的功能。我们只需要3步:

  • RefreshIndicator 来warpper ListView
  • 实现onRefresh并在方法中更新数据源

增加代码片段

// 下拉刷新
  Future _handleRefresh() async {
    print('-------开始刷新------------');
    await Future.delayed(Duration(seconds: 2), () { //模拟延时
      setState(() {
        entityList.clear();
        entityList = List.generate(10,
                (i) => new Todo(
                "刷新我是表第 $i 项",
                '刷新我是内容 $i'
            )
        );
        return null;
      });
    });
  }

修改代码片段

body: RefreshIndicator(
          child: _listViewWidget(
              context,
              entityList
          ),
          onRefresh: _handleRefresh
      ),

效果如图:

上拉加载更多

刷新完成了,接下来我们实现分页加载。我们借助ScrollController给ListView添加滑动监听事件。也需要两步:

  • 增加两个变量:ScrollControllerisLoadData
  • 实现_scrollController.addListener并在方法中更新数据源

新增代码片段

ScrollController _scrollController = new ScrollController();
bool isLoadData = false;

  @override
  void initState() {
    super.initState();
    print('你好!');

    _scrollController.addListener((){
      if (_scrollController.position.pixels ==
          _scrollController.position.maxScrollExtent) {
        print("------------加载更多-------------");
        _getMoreData();
        callTimes = callTimes+1
      }
    });
  }
Future _getMoreData() async {
    await Future.delayed(Duration(seconds: 2), () { //模拟延时操作
      if (!isLoadData) {
        isLoadData = true;
        setState(() {
          isLoadData = false;
          List newList = List.generate(5,
                  (i) => Todo(
                      '分页--我是表第 ${i + entityList.length} 项',
                      '刷新我是内容 ${i + entityList.length} '
                  )
          );
          entityList.addAll(newList);
        });
      }
    });
  }

完整代码如下:

import 'package:flutter/material.dart';
import 'package:hello/Models/Todo.dart';
import 'package:hello/Moudles/DetailScreen.dart';

class ListViewRefreshTest extends StatefulWidget {
  @override
  createState()=> _ListViewRefreshState();
}


class LoadMoreView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(child: Padding(
      padding: const EdgeInsets.all(18.0),
      child: Center(
        child: Row(children: [
          CircularProgressIndicator(),
          Padding(padding: EdgeInsets.all(10)),
          Text('加载中...')
        ],
          mainAxisAlignment: MainAxisAlignment.center,
        ),
      ),
    ), color: Colors.white70,);
  }
}

Widget _listViewWidget (BuildContext context, List sourceList,ScrollController scrollController,bool isNeedLoad) {
  if(sourceList.length==0) {
    return null;
  } else {

    return ListView.builder(
      itemCount: isNeedLoad? sourceList.length+1 : sourceList.length,
      //设置physics属性总是可滚动
      physics: AlwaysScrollableScrollPhysics(),
      itemBuilder: (context,index) {

          if (index == sourceList.length) {
            return LoadMoreView();
          } else {
            return ListTile(
              title: Text(sourceList[index].title,style: TextStyle(fontSize: 22),),
              subtitle: Text(sourceList[index].description,style: TextStyle(fontSize: 18)),
              onTap: () {
                Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder:(context)=> DetailScreen(todo: sourceList[index]),
                    )
                );
              },
            );
          }
        },
        controller: scrollController,
    );
  }
}

List _myDataList(){
  return List.generate(10,
          (i) => new Todo(
            '我是表第 $i 项',
            '我是内容 $i',
      )
  );
}


 int callTimes = 0;
class _ListViewRefreshState extends State {
  List entityList = _myDataList();
  ScrollController _scrollController = new ScrollController();
  bool isLoadData = false;

  @override
  void initState() {
    super.initState();
    print('你好!');
    _scrollController.addListener((){
      if (_scrollController.position.maxScrollExtent == _scrollController.position.pixels) {
        print("------------加载更多-------------");
        _getMoreData();
      }
    });
  }

  // 下拉刷新
  Future _handleRefresh() async {
    print('-------开始刷新------------');
    await Future.delayed(Duration(seconds: 2), () { //模拟延时
      setState(() {
        entityList.clear();
        entityList = List.generate(10,
                (i) => new Todo(
                "刷新我是表第 $i 项",
                '刷新我是内容 $i'
            )
        );
        return null;
      });
    });
  }


  Future _getMoreData() async {
    await Future.delayed(Duration(seconds: 2), () { //模拟延时操作
      if (!isLoadData) {
        isLoadData = true;
        setState(() {
          isLoadData = false;
          List newList = List.generate(5,
                  (i) => Todo(
                      '分页--我是表第 ${i + entityList.length} 项',
                      '刷新我是内容 ${i + entityList.length} '
                  )
          );
          entityList.addAll(newList);
        });
      }
    });
  }


  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(

      appBar: AppBar(
        title: Text('刷新和分页加载'),
      ),

      body: RefreshIndicator(
          onRefresh: _handleRefresh,
          child: _listViewWidget(
              context,
              entityList,
              _scrollController,
              isLoadData
          ),
        ),
    );
  }
}

整体效果图如下:

遇到问题

  • 问题:使用ListView.builder做个简单的下拉刷新,发现数据不满一屏,无法滑动。

解决方法:增加physics: AlwaysScrollableScrollPhysics(),即可
ListView.builder(
//设置physics属性总是可滚动
physics: AlwaysScrollableScrollPhysics(),

  • 问题:数据不满一屏,上拉分页加载,无法触发_scrollController.addListener();
    暂时解决:
    body修改
body:new NotificationListener(
    onNotification: _onNotification,
    child: RefreshIndicator(
      onRefresh: _handleRefresh,
      child: _listViewWidget(
          context,
          entityList,
          _scrollController,
          isLoadData
      ),
    ),
  ),
);

添加监听通知的方法

 int callTimes = 0;
class _ListViewRefreshState extends State {
  List entityList = _myDataList();
  ScrollController _scrollController = new ScrollController();
  bool isLoadData = false;
  double offset = 0;


// 为了解决不满一屏无法上拉加载分页的问题。
  bool _onNotification(ScrollNotification notification) {
    if (notification is! ScrollNotification) {
      // 如果不是滚动事件,直接返回
      return false;
    }

    if (notification is OverscrollNotification) {
      offset = notification.overscroll>offset ? notification.overscroll:offset;
    }

    print('hdhhd---$offset');
    if (notification is ScrollEndNotification) {
      // 要加上不满一屏幕的条件判断,直执行一次,待到列表数据内容超过屏幕高度,不再这里指执行了。走_scrollController.addListener来分页
      if(offset>15&&callTimes==0) {
        callTimes = callTimes + 1;
        _getMoreData();
        offset = 0;
      }
    }
    return true;
  }

你可能感兴趣的:(Flutter开发教程)