
                            
                            因收到Google相关通知，网站将会择期关闭。相关通知内容
                            
                            
                            16 用滑动列表展示记录
                            1. 功德记录的功能需求

现在想要记录每次点击时的功德数据，以便让用户了解功德增加的历史。对于一个需求而言，最重要的是分析其中需要的数据，以及如何维护这些数据。

效果如下，点击左上角按钮，跳转到功德记录的面板。功德记录界面是一个可以滑动的列表，每个条目展示当前功德的 木鱼图片、音效名称、时间、功德数 信息：




标题














根据界面中的数据，可以封装一个类来维护，如下 MeritRecord 类。其中 id 是一条记录的身份标识，就像人的身份证编号，就可以确保他在社会中的唯一性。

class MeritRecord {
  final String id; // 记录的唯一标识
  final int timestamp; // 记录的时间戳
  final int value; // 功德数
  final String image; // 图片资源
  final String audio; // 音效名称

  MeritRecord(this.id, this.timestamp, this.value, this.image, this.audio);
}


对于功德记录的需求而言，需要在 _MuyuPageState 中新加的数据也很明确: 对 MeritRecord 列表的维护。接下来的任务就是，在点击时，为 _records 列表添加 MeritRecord 类型的元素。

---->[_MuyuPageState]----
List<MeritRecord> _records = [];




2. 列表数据的维护

对于生成唯一 id ， 这里使用 uuid 库，目前最新版本 3.0.7。记得在 pubspec.yaml 的依赖节点配置:

dependencies:
  ...
  uuid: ^3.0.7


如下代码是 _onKnock 函数处理点击时的逻辑，其中创建 MeritRecord 对象，构造函数中的相关数据，在状态类中都可以轻松的到。

---->[_MuyuPageState]----
final Uuid uuid = Uuid();

void _onKnock() {
  pool?.start();
  setState(() {
    _cruValue = knockValue;
    _counter += _cruValue;
    // 添加功德记录
    String id = uuid.v4();
    _records.add(MeritRecord(
      id,
      DateTime.now().millisecondsSinceEpoch,
      _cruValue,
      activeImage,
      audioOptions[_activeAudioIndex].name,
    ));
  });
}




这里来分析一下上一篇中切换木鱼样式，会导致动画触发的问题。原因非常简单，因为动画控制器会在 didUpdateWidget 中启动。而切换木鱼样式时，通过触发了 setState 更新主界面的木鱼图片。于是会连带触发动画器的启动，解决方案有很多：


解决方案 1 : 将动画控制器交由上层维护，仅在点击时启动动画。
解决方案 2 ：为 didUpdateWidget 回调中动画启动添加限制条件。
解决方案 3 ：切换木鱼样式时，使用局部更新图片，不重新构建整个 _MuyuPageState 。




上面的三个方案中各有优劣，综合来看，方案 1 是最好的；方案 2 次之；方案 3 虽然可以解决当前问题，但治标不治本。就当前代码而言，使用 方案 2 处理最简单，那么动画启动的限制条件是什么呢?


当功德发生变化时，才需要启动动画。


而现在每个功德对应一个 MeritRecord 对象，且 id 可以作为功德的身份标识。功德发生变化 ,也就意味着新旧功德 id 的不同。那么具体的方案就呼之欲出了，如下所示：让 AnimateText 持有 MeritRecord 数据



在状态类 didUpdateWidget 回调中，可以访问到旧的组件，通过对比新旧 AnimateText 组件持有的 record 记录 id ，当不同时才启动动画。这样就避免了在功德未变化的情况下，启动动画的场景。通过个问题的解决，想必大家对状态类的 didUpdateWidget 方法，有更深一点的了解。

@override
void didUpdateWidget(covariant AnimateText oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.record.id != widget.record.id) {
    controller.forward(from: 0);
  }
}



小练习: 自己尝试使用解决方案 1 ，解决动画误启动问题。




3. ListView 的简单使用

我们已经知道，通过 Column 组件可以实现若干个组件的竖向排列。但当组件数量众多时，超越 Column 组件尺寸范围后，就无法显示；并且在 Debug 模式中会出现越界的异常(下左图)。很多场景中，需要支持视图的滑动，可以使用 ListView 组件来实现 (下右图)：




Column
ListView










这里历史记录界面通过 RecordHistory 组件构建，构造函数中传入 MeritRecord 列表。主体内容通过 ListView.builder 构建滑动列表，通过 _buildItem 方法，根据索引值返回条目组件。

class RecordHistory extends StatelessWidget {
  final List<MeritRecord> records;

  const RecordHistory({Key? key, required this.records}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar:_buildAppBar(),
      body: ListView.builder(
        itemBuilder: _buildItem, 
        itemCount: records.length,
      ),
    );
  }

  PreferredSizeWidget _buildAppBar() =>
      AppBar(
        iconTheme: const IconThemeData(color: Colors.black),
        centerTitle: true,
        title: const Text(
          '功德记录', style: TextStyle(color: Colors.black, fontSize: 16),),
        elevation: 0,
        backgroundColor: Colors.white,
      );


在 _buildItem 方法中，使用 ListTile 组件构建条目视图；其中：


leading 表示左侧组件，使用 CircleAvatar 展示圆形图形；
title 表示标题组件，展示功德数；
subtitle 表示副标题组件，展示音效名称；
trailing 表示尾部组件，展示日期。
注: 这里使用 DateFormat 来格式化时间，需要在依赖中添加 intl: ^0.18.1 包。


DateFormat format = DateFormat('yyyy年MM月dd日 HH:mm:ss');

  Widget? _buildItem(BuildContext context, int index) {
    MeritRecord merit = records[index];
    String date = format.format(DateTime.fromMillisecondsSinceEpoch(merit.timestamp));
    return ListTile(
      leading: CircleAvatar(
        backgroundColor: Colors.blue,
        backgroundImage: AssetImage(merit.image),
      ),
      title: Text('功德 +${merit.value}'),
      subtitle: Text(merit.audio),
      trailing: Text(
        date, style: const TextStyle(fontSize: 12, color: Colors.grey),),
    );
  }
}




最后在 _MuyuPageState 中处理 _toHistory 点击事件,通过 Navigator 跳转到 RecordHistory 。

---->[_MuyuPageState]----
void _toHistory() {
  Navigator.of(context).push(
    MaterialPageRoute(
      builder: (_) => RecordHistory( records: _records.reversed.toList()),
    ),
  );
}




4.本章小结

本章主要介绍了如何使用可滑动列表展示非常多的内容，ListView 组件可以很便捷地帮我们让记录列表支持滑动。滑动视图使用起来并不是很复杂，但真的能用好，理解其内部的原理，还有很长的路要走。对于新手而言，目前简单能用就行了，以后可以基于小册来深入研究。

到这里，电子木鱼的基本功能完成了，当前代码位置 muyu 。不过现在的数据都是存储在内存中的，应用退出之后无论是选项，还是功德记录都会重置。想要数据持久化存储，在后面的 数据的持久化存储 一章中再继续完善，木鱼项目先告一段落。

                        
                        
                            