133 lines
3.3 KiB
Dart
133 lines
3.3 KiB
Dart
/// ============================================================
|
||
/// 【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,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|