前言
如下所示,在上一篇中我们通过绘制,自定义了一个秒表盘的组件。本文将对该组件进行实际的应用,让其实现秒表运动的展示功能。
1. 等宽字体
在实现秒表运动之前,先来看个问题。下面通过点击
号,让当前的 Duration
对象增加 100 ms
,这里有一点小问题:由于目前字体不同数字的宽度存在差异,所以在变化过程中存在 “抖动”
的现象:
这是字体本身的问题,比如下面字体十个数字有 8
种不同的宽度。在像秒表这样有连续变化数字的场景,这种字体是不能用的。我们需要一种等宽字体 (Monospace
),在编程时,为了便于对齐,IDE
中的字体一般都是等宽字体。
可以在 https://fonts.google.com/
中搜索 Monospace
类型的字体:
如下是 IBMPlexMono
字体,由于每个字是等宽的,所以在变化时就不会出现抖动的问题。
https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1a8f7dbf4f347c29791f80c34194ba5~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?
2. 表盘更新的代码实现
上一篇说过 StopWatchWidget
需要展示什么由使用者决定,自身并不承担改变状态的责任。也就是说它是 不可变状态的
组件。我们如果想在点击时改变表盘显示的内容,就要由使用者来维护状态的变化,其实这本质上和 计数器
项目没有区别,只不过这里变化的是 Duration
对象而已。
如下,HomePage
为 StatefulWidget
,在其状态类 _HomePageState
中维护 Duration
对象的变化。当点击按钮时,触发 updateDuration
方法,在当前 Duration
对象的基础上 100 ms
。之后,通过 setState
触发重新构建。
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
Duration duration = Duration(minutes: 0, seconds: 24, milliseconds: 850);
void updateDuration(){
int minus = duration.inMinutes % 60;
int second = duration.inSeconds % 60;
int milliseconds = duration.inMilliseconds % 1000;
duration = Duration(minutes: minus,seconds: second,milliseconds: milliseconds 100);
setState(() {
});
}
Widget buildStopWatch(){
return StopWatchWidget(
duration: duration,
radius: 120,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed:updateDuration,
),
appBar: AppBar( title: Text('HomePage')),
body: Center(child: buildStopWatch),
);
}
}
3.使用 ValueListenableBuilder 组件局部构建
在频繁触发更新
的情况下,靠考虑尽可能减少构建的范围。比如这里 _HomePageState
在触发 setState
时,其 build
方法会被触发,导致构建的范围较大,整个界面都会 重新构建
。
秒表运行需要频繁的更新,而且像标题、按钮并不需要跟随 Duration
对象而更新,所以没必要被频繁重新构建。有没有一种方式,可以只让 StopWatchWidget
组件根据 Duration
对象而更新?
最简单的一种实现方式就是 ValueListenableBuilder
组件。它的实现原理非常简单,就是组件抽离 监听更新而已。在 《Flutter 组件 | ValueListenableBuilder 局部刷新小能手》一文中有原理的详细说明,感兴趣的可以研究一下。这里主要说一下它的使用方式。
如下所示,buildStopWatch
方法中,使用 ValueListenableBuilder
,构造时需要提供一个 ValueListenable
类型的可监听对象 valueListenable
。当该对象值发生变化,会触发 builder
回调方法,从而只更新 StopWatchWidget
组件,实现局部更新。
class _HomePageState extends State<HomePage> {
ValueNotifier<Duration> duration = ValueNotifier(Duration.zero);
void updateDuration(){
int minus = duration.value.inMinutes % 60;
int second = duration.value.inSeconds % 60;
int milliseconds = duration.value.inMilliseconds % 1000;
duration.value = Duration(minutes: minus,
seconds: second,milliseconds: milliseconds 100);
}
Widget buildStopWatch(){
return ValueListenableBuilder<Duration>(
valueListenable: duration,
builder:(_,value,__) => StopWatchWidget(
duration: value,
radius: 120,
),
);
}
@override
void dispose() {
duration.dispose();
super.dispose();
}
再强调一下,ValueListenableBuilder
组件的源码实现,内部也是通过 setState
触发更新的,不要对 setState
本身有任何偏见。工具没有好坏,只有场景的适不适合。
4.秒表的运动
之前有位朋友用 Flutter
做 节拍器
时抱怨,Flutter
通过 Timer
计时有很大的误差。其实 Timer.periodic
方法上有很明确的注释,该方法并不能保证每次回调间隔的正确性,还有一些误差。所以像节拍器、秒表这种需要精确时间间隔的场景,不能使用 Timer.periodic
来 "驱动"
。
当时我让这位朋友看一下 Ticker
,解决了他的问题。在 《Flutter 动画探索 - 流光幻影 · 十六章》中详细介绍了 Ticker
的源码,感兴趣的可以自己研究一下。这里主要介绍它的应用:可以通过构造方法直接构造 Ticker
对象,其中的入参是一个回调方法。当 Ticker
开启时,会不断触发回调,也就是下面的 _onTick
方法,回调的 Duration
对象就是 Ticker
运行的时间。这里说一下,动画的本质也是通过 Ticker
实现的。
late Ticker _ticker;
@override
void initState() {
super.initState();
_ticker = Ticker(_onTick);
}
void _onTick(Duration elapsed) {
// TODO
}
@override
void dispose() {
duration.dispose();
_ticker.dispose();
super.dispose();
}
所以只要开启 Ticker
,改变 StopWatchWidget
组件的 duration
值,就可以让秒表运动:
https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d855800ed6614d31a736de5ef2def2fc~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?
由于有暂停的需求,而 _ticker.stop
会让回调中的 Duration
对象重置。所以需要记录一下间隔时间 dt
,和最后记录时间 lastDuration
,来维护 duration
的值。
Duration dt = Duration.zero;
Duration lastDuration = Duration.zero;
void _onTick(Duration elapsed) {
dt = elapsed - lastDuration;
duration.value = dt;
lastDuration = elapsed;
}
void onTapIcon() {
if (_ticker.isTicking) {
_ticker.stop();
lastDuration = Duration.zero;
} else {
_ticker.start();
}
}
到这里,秒表的最核心功能就已经完成了。在 《Flutter 语法基础 - 梦始之地》 中,将对秒表基于此进行完善。那本文就到这里,谢谢观看 ~