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

 


前一篇博客:Flutter可滚动组件(2):ListView基本使用 介绍了 ListView 的基本使用,下面通过一个示例介绍一下 ListView 的各种进阶用法。

一、实现复杂自定义ListView

先看下效果图:

Flutter_listView_F.png


1.1 完成条目的封装

// ignore_for_file: prefer_typing_uninitialized_variables, avoid_print
import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: MyHomeBody(),
      ),
    );
  }
}

// 诗歌item
class PoemItem {
  ImageProvider image; // 图片
  var title; // 标题
  var author; // 作者
  var summary; // 摘要
  PoemItem({required this.image, this.title, this.author, this.summary});
}

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

class PoemItemView extends StatelessWidget {
  final PoemItem data;
  final OnItemClickListener onItemClickListener;
  const PoemItemView(
      {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;
  }
}

// 显示自定义ListView
Widget showListView() {
  var data = [];
  for (var i = 0; i < 20; i++) {
    data.add(PoemItem(
        image: const AssetImage("assets/head.jpeg"),
        title: "$i:以梦为马",
        author: "海子",
        summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
  }

  return ListView.separated(
    padding: const EdgeInsets.all(8.0),
    itemCount: data.length, // 条目的个数
    itemBuilder: (BuildContext context, int index) {
      return PoemItemView(
        // 数据填充条目
        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.orangeAccent,
        ),
      );
    },
  );
}

1.2 ListView的使用

在构造器构造条目时,使用数据对条目进行数据填充,以达到数据展示效果:

// 显示自定义ListView
Widget showListView() {
  var data = [];
  for (var i = 0; i < 20; i++) {
    data.add(PoemItem(
        image: const AssetImage("assets/head.jpeg"),
        title: "$i:以梦为马",
        author: "海子",
        summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
  }

  return ListView.builder(
      padding: const EdgeInsets.all(8.0),
      itemCount: data.length, //条目的个数
      itemBuilder: (BuildContext context, int index) {
        return PoemItemView(
          //数据填充条目
          data: data[index],
          onItemClickListener: () {
            //事件响应
            print(index);
          },
          key: UniqueKey()          
        );
      });
}

class MyHomeBody extends StatelessWidget {
  const MyHomeBody({super.key});

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

至此这个代码初步完成。

二、分隔线的添加

这里现将 Card 组件去掉。separated 可以很轻松的实现下划线分隔,并且容易改变长短,颜色 如果你愿意,也可以去定义分隔的组件,专门作为分隔线。而且还能使用索引进行个性化设计:

// 显示自定义ListView,添加分割线
Widget showListViewDivider() {
  var data = [];
  for (var i = 0; i < 20; i++) {
    data.add(PoemItem(
        image: const AssetImage("assets/head.jpeg"),
        title: "$i:以梦为马",
        author: "海子",
        summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
  }

  return ListView.separated(
    padding: const EdgeInsets.all(8.0),
    itemCount: data.length, // 条目的个数
    itemBuilder: (BuildContext context, int index) {
      return PoemItemView(
        // 数据填充条目
        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.orangeAccent,
        ),
      );
    },
  );
}

class MyHomeBody extends StatelessWidget {
  const MyHomeBody({super.key});

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

效果图如下所示:

Flutter_listView_E.png


三、ListView下拉刷新

RefreshIndicator是 Flutter 中用于实现下拉刷新功能的一个组件。RefreshIndicator包裹一个可滚动的子组件,如 ListView 或 GridView,当用户下拉到列表顶部时,会触发刷新操作。onRefresh 是一个返回 Future 的异步函数,用于执行刷新操作。

RefreshIndicator关键属性:

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

示例如下:

import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: MyRefreshableList(),
      ),
    );
  }
}

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(20, (i) => 'Item ${i + 1}');

  Future<void> _onRefresh() async {
    await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求
    // 更新数据
    setState(() {
      items
          .addAll(List.generate(20, (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]));
        },
      ),
    );
  }
}

在这个例子中,RefreshIndicator配合ListView.builder实现下拉刷新。_onRefresh函数模拟了一个网络请求,并在完成后更新数据。效果图如下所示:

Flutter_listView_G.png


四、ListView上拉加载更多

RefreshIndicator结合ScrollController和底部加载指示器可以进一步实现上拉加载更多功能。

示例如下:

import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: MyLoadMoreList(),
      ),
    );
  }
}

class MyLoadMoreList extends StatefulWidget {
  const MyLoadMoreList({super.key});

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

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]));
      },
    );
  }
}

在这个例子中,我们使用一个ScrollController来检测列表是否滚动到底部。一旦到达底部,_loadMore函数会被触发,模拟加载更多内容的过程。同时,为了防止多次触发加载更多操作,我们添加了一个isLoadingMore变量作为标记。

ListView.builderitemCount设置为items.length + 1,这样列表的最后一项就是一个CircularProgressIndicator,显示加载中的动画。用Visibility控制它的显示,仅在加载更多数据时可见。

建议

  • 始终要记得在实现上拉加载时,需要在加载过程中防止重复触发加载方法。
  • 在生产环境中,用实际的网络请求替代示例中的延迟函数(即Future.delayed()),实现真正的分页加载数据。
  • 为了提升用户体验,可以在数据加载完成后,稍微滚动列表(通过ScrollController),以避免CircularProgressIndicator直接覆盖在最后一个列表项上。

效果图如下所示:

Flutter_listView_H.png


五、结合下拉刷新和上拉加载更多功能

下面是一个完整的实现示例,结合下拉刷新和上拉加载更多功能:

import 'package:flutter/material.dart';

main(List<String> args) {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: PullToRefreshAndLoadMore(),
      ),
    );
  }
}

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

通过结合RefreshIndicatorListView.builder,你可以创建一个用户友好的列表,支持下拉刷新和上拉加载更多数据,从而模拟典型的移动应用中常见的列表行为。请根据实际的业务逻辑和数据来源,适当调整示例中的模拟延迟和加载逻辑。

六、ListView的滚动方向和控制

ListView有两种滚动方向可供选择:垂直(默认)和水平。你可以使用scrollDirection属性来控制滚动方向。下面是一个例子:

ListView.builder(
  scrollDirection: Axis.horizontal,
  // ...
)

在这个例子中,我们将scrollDirection属性设置为Axis.horizontal,以创建一个水平滚动的ListView。


如果你想控制ListView的滚动行为,你可以使用controller属性并配合ScrollController来实现。

下面是一个示例,滚动 ListView 到指定的位置:

import 'package:flutter/material.dart';
 
void main() => runApp(const MyApp());
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: ScrollToPositionPage(),
    );
  }
}
 
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(25), // 假设我们想要滚动到第26个元素的位置
        child: const Icon(Icons.arrow_downward),
      ),
    );
  }
}

在这个例子中,我们创建了一个ScrollController对象,并将其分配给 ListView。我们还实现了一个_scrollToIndex方法,该方法接受一个索引参数,并使用animateTo方法滚动到该索引对应的位置。当点击浮动按钮时,我们调用_scrollToIndex(25),假设我们想要滚动到第 26 个元素的位置。

效果图如下所示:

Flutter_listView_I.png


参考:

- Flutter 基础篇 -] ListView的使用-腾讯云开发者社区-腾讯云

Flutter下拉刷新和上拉加载在Flutter中,可以通过多种方式实现下拉刷新和上拉加载的功能。其中一个非常流行的做法 - 掘金


posted @   fengMisaka  阅读(236)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示