Flutter学习 滚动组件(2):ListView进阶使用

目录

前言:

上一篇文章介绍了,Flutter学习 滚动组件(1):ListView基本使用介绍了ListView基本使用,这篇文章介绍一下进阶使用的方法。

一、实现复杂的ListView列表:

先看效果图:

1.1 Item布局封装

dart 复制代码
// list item
class ListItem {
  ImageProvider image; // 图片
  var title; // 标题
  var author; // 作者
  var summary; // 摘要
  ListItem({required this.image, this.title, this.author, this.summary});
}

// list item界面实现
typedef OnItemClickListener = void Function();

class ListItemView extends StatelessWidget {
  final ListItem data;
  final OnItemClickListener onItemClickListener;

  const ListItemView(
      {required Key key, required this.data, required this.onItemClickListener})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    var headIcon = Container(
        // 左边头部
        decoration: BoxDecoration(
          color: Colors.white,
          shape: BoxShape.circle,
          boxShadow: [
            BoxShadow(
              color: Colors.grey.withOpacity(0.3),
              offset: const Offset(0.0, 0.0),
              blurRadius: 3.0,
              spreadRadius: 0.0,
            ),
          ],
        ),
        width: 70,
        height: 70,
        child: Padding(
          padding: const EdgeInsets.all(3),
          child: CircleAvatar(
            backgroundImage: data.image,
          ),
        ));
    var center = Column(
      // 中间介绍
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(data.title,
            style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            "作者:${data.author}",
            style: const TextStyle(color: Colors.grey, fontSize: 12),
          ),
        ),
      ],
    );
    var summary = Text(
      // 尾部摘要
      data.summary,
      maxLines: 3,
      overflow: TextOverflow.ellipsis,
      style: const TextStyle(color: Colors.grey, fontSize: 12),
    );
    var item = Row(
      // 条目拼合
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        const SizedBox(width: 10),
        headIcon,
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child: center,
        ),
        Expanded(
          child: summary,
        ),
        const SizedBox(width: 10),
      ],
    );
    var result = Card(
        // 卡片化+事件监听
        elevation: 5,
        child: InkWell(
            onTap: onItemClickListener,
            child: Padding(
              padding: const EdgeInsets.all(10),
              child: item,
            )));
    return result;
  }
}

1.2 ListView的使用

dart 复制代码
Widget showListView() {
  var data = [];
  for (var i = 0; i < 20; i++) {
    data.add(ListItem(
        image: const AssetImage("assets/images/android_fly.webp"),
        title: "$i:指鹿为马",
        author: "ddup",
        summary: "公元前210年,秦始皇病死,担任中车府令(掌管皇帝车马)的宦官赵高,不愿让秦始皇的大儿子扶苏继承皇位,而想让秦始皇的小儿子胡亥当皇帝。"));
  }
    return ListView.builder(
      padding: const EdgeInsets.all(8.0),
      itemCount: data.length, //条目的个数
      itemBuilder: (BuildContext context, int index) {
        return ListItemView(
          //数据填充条目
            data: data[index],
            onItemClickListener: () {
              //事件响应
              print(index);
            },
            key: UniqueKey()
        );
      });
}

class MyApp extends StatelessWidget {
  final List<String> items;

  const MyApp({super.key, required this.items});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    const title = 'ListView的使用';

    return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: title,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: const Text(title),
          ),
          body:
              MyHomeBody(),
        )
        );
  }
}

class MyHomeBody extends StatelessWidget {
  List<String> list = [];

  MyHomeBody({super.key}) {}


  @override
  Widget build(BuildContext context) {
    return showListView();
  }
}

1.3 增加分割线

dart 复制代码
// 显示自定义ListView
Widget showListView() {
  var data = [];
  for (var i = 0; i < 20; i++) {
    data.add(ListItem(
        image: const AssetImage("assets/images/android_fly.webp"),
        title: "$i:指鹿为马",
        author: "ddup",
        summary: "公元前210年,秦始皇病死,担任中车府令(掌管皇帝车马)的宦官赵高,不愿让秦始皇的大儿子扶苏继承皇位,而想让秦始皇的小儿子胡亥当皇帝。"));
  }

  return ListView.separated(
    padding: const EdgeInsets.all(8.0),
    itemCount: data.length, // 条目的个数
    itemBuilder: (BuildContext context, int index) {
      return ListItemView(
        // 数据填充条目
        data: data[index],
        onItemClickListener: () {
          // 事件响应
          print(index);
        },
        key: UniqueKey(),
      );
    },
    separatorBuilder: (BuildContext context, int index) {
      return const Padding(
        padding: EdgeInsets.only(left: 90),
        child: Divider(
          height: 1,
          color: Colors.blue,
        ),
      );
    },
  );
}

效果如下:

二、实现ListView下拉刷新:

RefreshIndicator是Flutter用于实现下拉刷新的功能组件,RefreshIndicator可以包裹一个可以滚动的组件,如ListView、GridView,下拉到顶部时会触发刷新操作,调用onRefresh方法,这方法返回一个Future 的异步函数,用于执行刷新操作。RefreshIndicator常见属性如下:

  • onRefresh: 必须实现的回调函数,执行刷新时的操作。
  • child: 需要包裹的可滚动子组件。
  • color:刷新指示器的进度条颜色。
  • backgroundColor: 刷新指示器的背景色。
  • displacement:指示器开始显示时与顶部的距离。

示例如下:

dart 复制代码
class MyRefreshableList extends StatefulWidget {
  const MyRefreshableList({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _MyRefreshableListState createState() => _MyRefreshableListState();
}

class _MyRefreshableListState extends State<MyRefreshableList> {
  final List<String> items = List.generate(5, (i) => 'Item ${i + 1}');

  Future<void> _onRefresh() async {
    await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求
    // 更新数据
    setState(() {
      items
          .addAll(List.generate(10, (i) => 'New item ${i + items.length + 1}'));
    });
  }

  @override
  Widget build(BuildContext context) {
    return RefreshIndicator(
      onRefresh: _onRefresh,
      child: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(items[index]));
        },
      ),
    );
  }
}

class MyHomeBody extends StatelessWidget {

  MyHomeBody({super.key}) {}


  @override
  Widget build(BuildContext context) {
    return MyRefreshableList();
  }
}
class MyApp extends StatelessWidget {
  final List<String> items;

  const MyApp({super.key, required this.items});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    const title = 'ListView的使用';

    return MaterialApp(
        debugShowCheckedModeBanner: false,
        title: title,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
          useMaterial3: true,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: const Text(title),
          ),
          body:
              MyHomeBody(),
        )
        );
  }
}

这个例子是在下拉刷新onRefresh 回调中,模拟了一个2s延时的网络请求 ,增加了10个新的item条目。

效果如下:


三、实现上拉加载更多:

上拉加载更多功能可以利用ScrollController判断是否滚动到底部,执行loadmore实现上拉加载更多功能:

dart 复制代码
class _MyLoadMoreListState extends State<MyLoadMoreList> {
  final List<String> items = List.generate(20, (i) => 'Item ${i + 1}');
  final ScrollController _scrollController = ScrollController();
  bool isLoadingMore = false;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      // 滑动到底部时触发加载更多(gif加载动画)
      if (_scrollController.position.pixels == // scrollController.position.pixels:表示当前滚动的位置
          _scrollController.position.maxScrollExtent) { // scrollController.position.maxScrollExtent:表示可滚动区域的最大值
        _loadMore();
      }
    });
  }

  // 回调函数,执行刷新时的操作
  Future<void> _loadMore() async {
    if (!isLoadingMore) {
      setState(() => isLoadingMore = true);
      // 模拟网络请求结束后加载更多数据
      await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求延迟
      setState(() {
        // 刷新操作:在底部增加10个item
        items.addAll(
            List.generate(10, (i) => 'New item ${items.length + i + 1}'));
        isLoadingMore = false;
      });
    }
  }

  // dispose字段主要用于在异步操作完成后,确保不会调用已经被销毁的State对象的setState方法
  @override
  void dispose() {
    _scrollController.dispose(); // 不要忘记在dispose方法中清理控制器
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      controller: _scrollController,
      itemCount: items.length + 1, // 添加一个进度指示器作为最后一项
      itemBuilder: (context, index) {
        if (index == items.length) { // 最后一项作为进度指示器
          return Visibility(
            visible: isLoadingMore,
            child: const Center(
              child: CircularProgressIndicator(),
            ),
          );
        }
        return ListTile(title: Text(items[index]));
      },
    );
  }
}

上述代码可以看到,我们在initState方法,增加ListView滚动监听,position.pixels == position.maxScrollExtent 执行onLoadMore 方法,增加一个isLoadingMore 变量来控制重复刷新state ,另外我们把loading条放在ListView最后一个条目加1 ,这样不会遮挡ListView条目,最后通过Visibility 来控制loading条的显隐。最后需要注意的一点是,我们在dispose方法时,调用_scrollController.dispose()。

效果图如下:

四、实现下拉刷新、上拉加载更多:

我们把下拉刷新和上拉加载结合一起实现:

dart 复制代码
class PullToRefreshAndLoadMore extends StatefulWidget {
  const PullToRefreshAndLoadMore({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _PullToRefreshAndLoadMoreState createState() =>
      _PullToRefreshAndLoadMoreState();
}

class _PullToRefreshAndLoadMoreState extends State<PullToRefreshAndLoadMore> {
  final List<String> _items = List.generate(20, (i) => 'Item ${i + 1}');
  final ScrollController _scrollController = ScrollController();
  bool _isLoadingMore = false;
  bool _hasMore = true; // 表示是否还有更多数据可加载

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
  }

  Future<void> _onRefresh() async {
    await Future.delayed(const Duration(seconds: 2));
    setState(() {
      _items.clear();
      _items.addAll(List.generate(20, (i) => 'Refreshed item ${i + 1}'));
    });
  }

  void _onScroll() {
    // 检测是否滚动到底部
    if (_scrollController.position.pixels >= // scrollController.position.maxScrollExtent:表示可滚动区域的最大值
        _scrollController.position.maxScrollExtent && // scrollController.position.maxScrollExtent:表示可滚动区域的最大值
        !_isLoadingMore &&
        _hasMore) {
      _loadMore();
    }
  }

  Future<void> _loadMore() async {
    if (_isLoadingMore) return; // 如果已经在加载,则不执行后续操作
    setState(() {
      _isLoadingMore = true;
    });

    await Future.delayed(const Duration(seconds: 2));

    if (mounted) {
      setState(() {
        _items.addAll(
            List.generate(10, (i) => 'New item ${_items.length + i + 1}'));

        // 假设每次增加了10个数据,加载了5次后认为没有更多数据
        if (_items.length >= 70) {
          _hasMore = false;
        }

        _isLoadingMore = false;
      });
    }
  }

  // dispose字段主要用于在异步操作完成后,确保不会调用已经被销毁的State对象的setState方法
  @override
  void dispose() {
    _scrollController.removeListener(_onScroll); // 移除滚动监听
    _scrollController.dispose(); // 清理控制器资源
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pull to Refresh & Load More'),
      ),
      body: RefreshIndicator(
        onRefresh: _onRefresh,
        child: ListView.builder(
          controller: _scrollController,
          itemCount: _hasMore
              ? _items.length + 1
              : _items.length, // 如果还有更多数据,添加额外一项来显示加载指示器
          itemBuilder: (context, index) {
            if (index == _items.length && _hasMore) { // 最后一项为加载进度指示器
              return const Center(
                child: Padding(
                  padding: EdgeInsets.all(8.0),
                  child: CircularProgressIndicator(),
                ),
              );
            }
            return ListTile(title: Text(_items[index]));
          },
        ),
      ),
    );
  }
}

上述示例,_loadMore会在滚动到底部时才会触发,并用setState来管理状态变化,并且通过_isLoadingMore来防止重复加载操作,以及用_hasMore判断是否还有更多数据需要加载,需要注意的是mounted检查以确保不会在Widget树移除后调用setState方法。

五、ListView滚动方向和控制:

ListView有两种滚动方向,垂直(默认)和水平,我们可以通过修改scrollDirection属性来控制滚动方向:

dart 复制代码
ListView.builder(
  scrollDirection: Axis.horizontal,
  // ...
)

我们将scrollDirection属性设置为Axis.horizontal,创建一个水平滚动的ListView。
如何滚动指定position?看下面例子:

dart 复制代码
class ScrollToPositionPage extends StatefulWidget {
  const ScrollToPositionPage({super.key});

  @override
  // ignore: library_private_types_in_public_api
  _ScrollToPositionPageState createState() => _ScrollToPositionPageState();
}

class _ScrollToPositionPageState extends State<ScrollToPositionPage> {
  final ScrollController _scrollController = ScrollController();
  final List<String> items = List.generate(100, (i) => 'Item $i');

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

  void _scrollToIndex(int index) {
    // 滚动到指定索引的位置
    _scrollController.animateTo(
      _scrollController.positions.first.maxScrollExtent * (index / items.length),
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _scrollToIndex(15), // 假设我们想要滚动到第16个元素的位置
        child: const Icon(Icons.arrow_downward),
      ),
    );
  }
}

我们可以看到,上述例子利用ScrollController 执行animateTo动画,根据_scrollController.positions.first.maxScrollExtent * (index / items.length)计算,滚动指定position位置。

效果图如下:

六、总结:

我们通过定义一个复杂的ListView布局和增加分割线,以及增加下拉刷新、上拉加载、修改ListView滚动方向,滚动到指定位置来介绍了一下ListView进阶使用,希望大家可以通过这些例子,更好的掌握ListView.

Thanks:
Flutter可滚动组件(3):ListView进阶使用

相关推荐
面包圈蘸可乐15 分钟前
论文学习:《创新编码策略的多类LncRNA亚细胞定位的集成深度学习框架》
人工智能·深度学习·学习
虾球xz37 分钟前
游戏引擎学习第234天:实现基数排序
c++·学习·算法·游戏引擎
良许Linux39 分钟前
如何系统地入门学习stm32?
stm32·单片机·学习
开开心心就好2 小时前
免费多平台运行器,手机畅玩经典主机大作
服务器·python·学习·安全·微信·智能手机·ocr
欢喜躲在眉梢里2 小时前
容器docker入门学习
运维·学习·nginx·docker·容器·虚拟化
爱吃生蚝的于勒3 小时前
数据结构0基础学习堆
android·c语言·数据结构·c++·学习·算法·链表
ll7788113 小时前
C++学习之路,从0到精通的征途:vector类的模拟实现
开发语言·c++·学习·算法·职场和发展
秋刀鱼不做梦3 小时前
五分钟学会如何基本使用JJWT!!!
java·开发语言·学习·intellij idea
reasonsummer3 小时前
【人工智能学习-01-01】20250419《数字图像处理》复习材料的word合并PDF,添加页码
学习·pdf