更新
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
/// ============================================================
|
||||
/// 【QuestionPlanCountDown】单题建议作答时长倒计时
|
||||
/// (1:1 迁移 Vue classExercises/QuestionPlanCountDown.vue,
|
||||
/// 底层由 uview up-count-down 换为 Flutter Timer.periodic)
|
||||
///
|
||||
/// 对外方法(`GlobalKey<QuestionPlanCountDownState>` 调用):
|
||||
/// start / pause / reset / getRemainTime(毫秒)
|
||||
/// autoStart:initState 后自动开始;到点只触发一次 onFinish(key)。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class QuestionPlanCountDown extends StatefulWidget {
|
||||
const QuestionPlanCountDown({
|
||||
super.key,
|
||||
this.questionKey,
|
||||
required this.timeMs,
|
||||
this.format = 'mm:ss',
|
||||
this.onFinish,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
final Object? questionKey;
|
||||
|
||||
/// 剩余总时长(毫秒)
|
||||
final int timeMs;
|
||||
|
||||
/// 'mm:ss'(本页另有 HH:mm:ss 的动态格式,按是否含 HH 切换)
|
||||
final String format;
|
||||
final ValueChanged<Object?>? onFinish;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
State<QuestionPlanCountDown> createState() => QuestionPlanCountDownState();
|
||||
}
|
||||
|
||||
class QuestionPlanCountDownState extends State<QuestionPlanCountDown> {
|
||||
Timer? _timer;
|
||||
late int _remainMs;
|
||||
bool _finished = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_remainMs = widget.timeMs;
|
||||
// autoStart
|
||||
start();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant QuestionPlanCountDown oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.timeMs != widget.timeMs) {
|
||||
reset();
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void start() {
|
||||
if (_timer != null) return;
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||||
if (!mounted) return;
|
||||
_remainMs -= 200;
|
||||
if (_remainMs <= 0) {
|
||||
_remainMs = 0;
|
||||
pause();
|
||||
if (!_finished) {
|
||||
_finished = true;
|
||||
widget.onFinish?.call(widget.questionKey);
|
||||
}
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void pause() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
/// 重置回初始时长并暂停(与 uview reset 行为一致,调用方需再 start)
|
||||
void reset() {
|
||||
pause();
|
||||
_remainMs = widget.timeMs;
|
||||
_finished = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// 剩余毫秒
|
||||
int getRemainTime() => _remainMs < 0 ? 0 : _remainMs;
|
||||
|
||||
String _two(int v) => v < 10 ? '0$v' : '$v';
|
||||
|
||||
String get _display {
|
||||
// 展示向上取整:刚开始显示完整秒
|
||||
final seconds = (_remainMs / 1000).ceil().clamp(0, 1 << 31);
|
||||
final h = seconds ~/ 3600;
|
||||
final m = (seconds % 3600) ~/ 60;
|
||||
final s = seconds % 60;
|
||||
if (widget.format.contains('HH')) {
|
||||
return '${_two(h)}:${_two(m)}:${_two(s)}';
|
||||
}
|
||||
return '${_two(m)}:${_two(s)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 字体的 ascent/descent 度量让文字视觉偏下,整体上移 2px 校正
|
||||
return Transform.translate(
|
||||
offset: const Offset(0, -2),
|
||||
child: Text(
|
||||
_display,
|
||||
style: widget.textStyle ??
|
||||
const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user