【Flutter】 如何快速高效地实现一个计时进度条动画+跳转
【Flutter】如何快速高效地实现一个计时进度条动画+跳转
没什么用的【背景】
今天群里一个意向移动组的小朋友突然提了个问题,需求是实现一个计时器动画+跳转
然后上午的时候我就初步地在群里回答了下,就没再管...
不过总之也是不太妥当,刚好下午也是水课,于是就简单做一个面向移动组新生的Flutter动画教程吧!
当然,如果你想省流,可以直接到最后看代码!还有如下的流程图:
下面是实现的效果:
本文使用环境:Flutter 3.10
工具:Android Studio
正式启动的小教程
由于在课上没带数据线,所以用edge来运行了,希望不会拉跨吧
- 新建一个默认项目
- 观察到MyHomePage这个部分就是Stateful的,所以我们就直接利用MyHomePage来实现计时器的功能,先把没用的模板数据清理干净。
-
我们分析动画的需求:
- 首先需要有一个计时器
- 计时器到一定时间之后要触发跳转方法
- 要有一个动画来表示计时器的状态
-
接下来声明一个
Timer
,我们先看看能否正常实现计时功能。
Timer? _timer; // 声明一个计时器,需要引入import 'dart:async';
int _countValue = 5; // 用于记录计时的变量
@override
void initState() { // 在StatefulWidget的生命周期中,initState只会在最开始被执行一次
super.initState();
startTimer();
}
void startTimer() {
// Timer.periodic方法可以反复执行,这里设置执行周期为1s
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() { // 使用setState来刷新整个StatefulWidget的状态
if (_countValue > 0) {
_countValue--;
print(_countValue);
} else {
print('jump'); // 这里使用print来代替跳转的行为,后面再进行不断完善
_timer?.cancel(); // 记得取消掉计时器,防止出现内存泄漏等问题
}
});
});
}
@override
void dispose() { // dispose方法也只会在StatefulWidget的生命周期中被执行一次,销毁内存
_timer?.cancel(); // 这里防止计时器没有计时到0时,此页面就被销毁而跳转了,保证严谨
super.dispose();
}
看一下实际效果:
看起来还是比较合理的,在第6秒,数字被减为-1,触发了<0的判断,于是print了'jump'
- 接下来,我们调整一下代码结构,这样方便后续的维护和管理。
注意这里的逻辑,我们最终想要5秒跳转,而不是6秒跳转,而我们的代码逻辑中,只有_countValue到了-1的时候才会触发跳转功能,所以这里有一个小细节
- 加入路由跳转的功能
-
首先随便写一个新页面
-
使用Navigator的路径跳转,这里就不多讲解了,不是重点
- 加入动画,放到下一章吧,原谅我混乱的排版
动画的小部分
如今我们已经完成了一个计时器+页面跳转的功能,那么接下来就要加入进度条动画了。
实现动画有很多简单的方式,这里选择最简单易写的方式!!!
使用AnimatedController来实现进度条
-
首先是简单地实现一个小小的进度条框,注意这里把框的宽度设置为屏幕宽度的0.8倍,高度设置为定值。
-
创建一个宽度随着时间增加的进度条框,注意Stack的渲染覆盖顺序由于AnimatedController的特性,会自动形成补间动画
-
这里有很多细节,先把截图放在这里,后面慢慢把这里的细节讲清楚。
小细节大改变!
- 关于duration的处理:可以看到我将AnimatedController的duration设置为了一个常量。
这是为什么呢?因为只有当AnimatedController的补间动画的周期,和计时器更新的周期同步时,才能实现秒和秒之间的动画可以稳定衔接!
(当然这里也需要另一个条件,就是动画的曲线默认是线性动画,才能实现完美衔接)
- 关于很多处-1的问题:(当然这是因为苯人是个OIER,所以很喜欢搞这些细节上的计算)
- 1:因为_countValue的值是从 jumpTime-1(即4)一直数到-1就跳转,而我们的进度条需要相应的,能够正确从占比0%一直到占比100%。这里需要有一个相减的计算
- 2:分母也需要减一,因为我们的AnimatedController的width修改了之后,需要用duration的时间进行补间动画,而我们数到最后一秒的时候就直接跳转走了,所以我们最后需要留出一秒的时间来进行跳转,可以参观图例:
- 关于decoration的问题:为什么这里还需要写一个边框呢?因为我们本质上是下面的紫色条覆盖了上面的框,如果紫色条本身不带框的话,就会出现如下的情况:
加了黑色的边框后:
总结与代码
至此!我们已经将大多数细节全部讲解完啦!
当然!动画还有很多种实现方式,这里还有一些没有涉及的细节没讲,比如关于Stack的元素对齐方式的问题,比如生命周期的问题。
后面再说吧~
再次放一下具体实现的录屏:
最后放一下代码:
main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:test_1/second_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const jumpTime = 5;
static const duration = Duration(seconds: 1);
Timer? _timer;
int _countValue = jumpTime - 1;
@override
void initState() {
super.initState();
startTimer();
}
void startTimer() {
_timer = Timer.periodic(duration, (timer) {
setState(() {
if (_countValue > 0) {
_countValue--;
print(_countValue);
} else {
//print('jump');
route2SecondPage();
_timer?.cancel();
}
});
});
}
void route2SecondPage() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SecondPage(),
),
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final width = size.width;
final boxWidth = width * 0.8;
const boxHeight = 30.0;
return Scaffold(
appBar: AppBar(
title: const Text('ZzTzZ'),
),
body: Center(
child: Stack(
children: [
Container(
width: boxWidth,
height: boxHeight,
decoration: BoxDecoration(
border: Border.all(
color: Colors.black87,
width: 3,
),
),
),
AnimatedContainer(
duration: duration,
width: boxWidth * (((jumpTime - 1) - _countValue) / (jumpTime-1)),
height: boxHeight,
decoration: BoxDecoration(
color: Colors.deepPurple,
border: Border.all(
color: Colors.black87,
width: 3,
),
),
),
],
),
),
);
}
}
second_page.dart
import 'package:flutter/material.dart';
class SecondPage extends StatefulWidget {
const SecondPage({super.key});
@override
State<SecondPage> createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('zZtZz')),
body: const Center(
child: Text('second page'),
)
);
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库