计数器
一个简单的计数器应用。
开发笔记
为了永久保留
滚动数字
使用 ListWheelScrollView widget 将数字一个接一个地显示出来。该 widget 需要两个字段:itemExtent 和 children。在后者属性中创建一个 widget 列表来显示这十个数字。
children: List<Widget>.generate(
digits,
(index) => FittedBox(
child: Text(
('$index'),
),
),
),
FittedBox 有助于扩展文本以填充可用空间。
控制
最终目标是通过两个按钮而不是滚轮来处理滚动。
使用 physics 禁用滚轮的滚动。
ListWheelScrollView(
physics: const NeverScrollableScrollPhysics(),
)
将 widget 变成一个有状态的 widget 来管理状态。
class Ticker extends StatefulWidget {
}
class _TickerState extends State<Ticker> {
}
在状态的子类中初始化一个控制器变量。
late FixedExtentScrollController _controller;
在 initState 生命周期方法中设置控制器。
@override
void initState() {
super.initState();
_controller = FixedExtentScrollController();
}
通过生命周期方法处置分配给控制器的资源。
@override
void dispose() {
_controller.dispose();
super.dispose();
}
将控制器包含在滚轮中。
ListWheelScrollView(
controller: _controller,
)
修改 widget 树,使其返回列表上方是一个带有两个列的行,用于计数和减计数。
Column
Expanded
Center
ListWheelScrollView
Row
IconButton
IconButton
向 onPressed 字段添加一个函数来向上和向下滚动。
onPressed: () => _scroll(-1),
onPressed: () => _scroll(1),
定义 _scroll 通过控制器更新滚轮。
void _scroll(int direction) {
}
通过 _controller.selectedItem 获取当前值,并根据输入方向修改它。
_controller.jumpToItem(
_controller.selectedItem + 1 * direction
);
使用 jumpToItem 即时跳转到相邻项。使用 animateToItem 则带有动画效果。
添加所需的持续时间和曲线。
_controller.animateToItem(
_controller.selectedItem + 1 * direction,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOutSine
);
无限滚动
与列表滚轮 widget 连接的是 ListWheelScrollView.useDelegate,用于以编程方式生成 children。在必需的 childDelegate 字段中,可以使用 ListWheelChildLoopingListDelegate widget 创建一个闭合的滚轮,或者说一个带有输入数字的重复滚轮。
ListWheelScrollView.useDelegate(
childDelegate: ListWheelChildLoopingListDelegate(
children: [] // List<Widget>...
)
)
然而,最终我选择不走循环路线。这更多地是出于对 _controller.selectedItem 中控制器值的偏好,我宁愿将其保持在一个给定的范围内,而不是扩展到大的正数或负数。
保留现有的 ListWheelScrollView widget,创建一个比必要数字多一个的列表。
List<Widget>.generate(digits, )
+List<Widget>.generate(digits + 1,)
移除文本 widget 中的多余部分。
Text(
(index % digits).toString()
)
在滚动函数中,当选中的项超出列表范围时,直接更新两个实例中的当前项。
if(direction == -1 && _controller.selectedItem == 0) {
_controller.jumpToItem(digits);
} else if(direction == 1 && _controller.selectedItem == digits) {
_controller.jumpToItem(0);
}
这种错觉之所以有效,是因为你在动画滚轮之前就跳转到了该项,并且最终只显示中心的一项——通过 overAndUnderCenterOpacity 属性。
多个滚轮
想法是最终实现一个计数功能,可以超出个位数。
让 Ticker widget 接收一个表示列数的命名属性,默认值为 3。
final int columns;
const Ticker({
Key? key,
this.columns = 3,
}) : super(key: key);
在有状态的 widget 中,创建一个控制器列表,而不是单个 FixedExtentScrollController 实例。
late List<FixedExtentScrollController> _controllers;
在 init 函数中初始化多个控制器。
_controllers = List<FixedExtentScrollController>.generate(
widget.columns,
(_) => FixedExtentScrollController()
);
在匹配的生命周期方法中处置所有控制器。
for(FixedExtentScrollController controller in _controllers) {
controller.dispose();
}
在 widget 树中,想法是循环遍历控制器,为每列添加一个滚轮。
在 widget 树方面,将 ListWheelScrollView 包装在 Expanded widget 中。将多个 Expanded widget 添加到行中,使滚轮并排显示。
Row
Expanded
Center
ListWheelScrollView
Expanded
Center
ListWheelScrollView
在 controller 字段中,从循环函数中添加相应的控制器。
_controllers.map(
(controller) => Expanded(
child: ListWheelScrollView(
controller: controller,
)
)
).toList()
在 children 字段中,为每个滚轮生成一个列表,而不是依赖于事先创建的列表。
children: List<Widget>.generate(
digits + 1,
// ...
)
使用单个列表似乎没有引起问题,至少从表面上看是这样,但为每个单独的滚轮创建一套是合理的。
更新计数器
对于多列,你需要考虑数字超出任一方向范围的情况。
想法是立即修改最后一个列来更新 scroll 函数,然后根据需要向后移动。
初始化一个计数器变量,从控制器的列表末尾开始。
int index = _controllers.length;
在 do..while 循环中递减计数器变量——这就是为什么初始值实际上会偏离一。
do {
index -= 1;
} while(index > 0);
如果你要修改所有列,你将为每个控制器执行之前的逻辑。
do {
index -= 1;
// _controllers[index]
} while(index > 0)
要只考虑 [0-9] 的极端值,在 while 条件中更新,同时考虑选中的项和方向。
while (
index > 0 &&
(
(direction == 1 && _controllers[index].selectedItem == digits - 1)
||
(direction == -1 && _controllers[index].selectedItem == digits)
)
);
在重复块的主体中,记录选中的项以双重检查值。
滚动顺序
出于偏好,应用程序通过使较大的值在上,较小的值在下来计数。实现这一目标的一种方法是
-
反转描述数字的 widget 列表
List<Widget>.generate( // digits + 1, ... ).reversed.toList()
-
在
_scroll函数中翻转输入方向void _scroll(int direction) { direction *= -1; }
-
重新考虑
while语句中的条件,因为数字的顺序是颠倒的
初始计数
当应用程序将计数的值本地存储时(例如通过 shared-preferences),让有状态的 widget 接收一个初始计数值很有用。
Ticker(count: 109)
初始化变量,使命名属性成为可选的。
class Ticker extends StatefulWidget {
final int count;
const Ticker({
Key? key,
this.count = 0
// ...
}) : super(key: key);
}
在状态的子类中,通过控制器更新列。由于逻辑依赖于 ListWheelScrollView widgets 的实际存在,请将说明包含在 initState 生命周期*以及* widget 构建时运行的函数中。
// initialize controllers
WidgetsBinding.instance?.addPostFrameCallback((_) {
// update controllers
});
请注意,列表中的数字顺序是颠倒的,因此你需要将各个数字映射到相应的索引。
一旦你在变量 digit 中提取了每列的数字
-
更新控制器以跳转到滚轮的底部
_controllers[index].jumpToItem(digits); -
将控制器动画回正确的值
_controllers[index].animateToItem( digits - digit, // ... )
要计算数字,请考虑输入的计数,并从最后一列开始。
int count = widget.count;
int index = _controllers.length - 1;
在一个 while 循环中,只要 1. 计数是正数并且 2. 还有剩余的列,就继续提取数字。
while(count > 0 && index >= 0) {
}
使用模运算符提取数字。
int digit = count % digits;
一旦你更新了控制器,就更新计数和索引以最终退出循环。
count = count ~/ digits;
index --;
~/ 用作整数除法的简写,(count / digits).toInt()。
交错动画
目标是交错滚动动画,包括连续列和初始计数。
对于任何一个,都从一个描述总持续时间的变量开始,并计算每位数字分配的毫秒数。
对于连续列,分配最多 600 毫秒,以防每个数字都被翻转。
int duration = 600 ~/ _controllers.length;
对于初始计数,分配最多 2 秒,因为最终可能滚动到更高的数字,并且动画会引入应用程序。
int duration = 2000 ~/ _controllers.length;
初始化一个变量来跟踪延迟,并随着每一列、每一个数字递增这个数字。
// successive column
delay += duration ~/ 3;
// initial count
delay += duration - (duration ~/ digits);
考虑一个比总持续时间小的值,以便连续滚动在之前的实例完成之前发生。
使用 Future.delayed 在规定的延迟后为控制器设置动画。
Future.delayed(
Duration(milliseconds: delay),,
() => // animateToItem
)
最重要的是,确保延迟动画实际上更新了当前控制器。这意味着要么在单独的变量中提取索引,要么提取控制器本身。
FixedExtentScrollController controller = _controllers[index];
// later
controller.animateToItem()
index 在 while 循环中更新,因此使用该变量意味着该方法将应用于最后一个可用实例。
-_controllers[index].animateToItem()