前言:
最近在做一个弹窗中点击发送短信的功能,要求是60秒倒计时,关闭弹窗或者退出当前页面后计时仍在进行。
准备阶段 - 思路:
1.先实现弹窗功能
2.绘制内容填充并完成ui展示
3.点击发送按钮触发倒计时功能,倒计时60秒,计时完成后可再触发点击
4.处理关闭弹窗后的计时生效问题,计时仍然有效(比如计时至56秒时关闭弹窗,等10秒再打开弹窗,此时倒计时时间更新为46秒)
UI效果展示:


编码阶段 - 实现过程:
新建class PersonQRCodeDialog
定义变量:
static const int sixtySeconds = 60; 倒计时60秒
static String _sendQRCode = S.current.pm_send_qrcode; 发送按钮文字
static Color _sendQRCodeColor = const Color(0xff3e71dd); 发送按钮文字颜色
static Timer? _timer; 计时器
static int _timeCount = sixtySeconds; 剩余倒计时时间
static int showDialogTime = 0; 弹窗被打开时的时间戳
static bool isTimerDialogClose = false; 判断弹窗是否被关闭
static Map<int?, int?> cacheTimeCountBindPerson = <int, int>{}; 根据不同用户id保存与之对应的倒计时时间
static Map<int?, int?> cacheMillisecondsTimeBindPerson = <int, int>{}; 根据不同用户id保存与之对应的关闭弹窗的时间(此map需要触发了倒计时且计时进行中且关闭了弹窗才会有作用)
定义入口函数:
showPersonQRCodeDialog(...) {
弹窗启动时间赋值
showDialogTime = (DateTime.now().millisecondsSinceEpoch ~/ 1000);
判断当前用户id在两个map中都包含有
if (cacheTimeCountBindPerson.containsKey(personId) &&
cacheMillisecondsTimeBindPerson.containsKey(personId)) {
获取到弹窗打开与关闭的时间间隔
var intervalTime =
showDialogTime - cacheMillisecondsTimeBindPerson[personId]!;
BILog.i(tag, "intervalTime: $intervalTime");
判断时间间隔是否大于等于当前用户id所保存的倒计时时间
if (intervalTime >= cacheTimeCountBindPerson[personId]!) {
BILog.i(tag, "countdown has expired. now you can send qrcode.");
大于说明倒计时时间已读完60秒,无需再处理计时,直接释放掉当前用户保存的时间
cacheTimeCountBindPerson.remove(personId);
} else {
BILog.i(tag, "still in range. you need wait for countdown.");
否则通过intervalTime更新掉cacheTimeCountBindPerson map中当前用户的倒计时时间,拿到map中存放的时间减去弹窗开关的时间间隔
cacheTimeCountBindPerson[personId] =
cacheTimeCountBindPerson[personId]! - intervalTime;
}
}
bool hasMobile = (qrcodeVM.mobile.isNotEmpty);
String bottomLeftText =
hasMobile ? S.current.pm_btn_cancel : S.current.pm_i_know;
Color bottomLeftTextColor =
hasMobile ? const Color(0xff000000) : const Color(0xff3e71dd);
String sendQrcodeTip = hasMobile
? S.current.pm_send_qrcode_to_visitor_tip
: S.current.pm_send_qrcode_need_complete_phone_info_tip;
Color sendQrcodeTipColor =
hasMobile ? const Color(0xff60a35c) : const Color(0xffddb83e);
var showModalBottomSheetFuture = showDialog(
context: context,
useSafeArea: false,
builder: (context) {
return StatefulBuilder(builder: (context, setStateBottomSheet) {
_initTimer(setStateBottomSheet, personId);
return SimpleDialog(
contentPadding: EdgeInsets.all(0.rpx),
children: [
SizedBox(
width: double.maxFinite,
child: Column(mainAxisSize: MainAxisSize.min, children: [
// 查看二维码标题
Container(
margin: EdgeInsets.only(top: 16.rpx),
alignment: Alignment.center,
child: Text(
S.current.pm_look_qrcode,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.rpx,
color: const Color(0xff000000),
fontWeight: FontWeight.w600),
),
),
SizedBox(height: 10.rpx),
// 提示词
Container(
alignment: Alignment.center,
child: Text(
sendQrcodeTip,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.rpx,
color: sendQrcodeTipColor,
fontWeight: FontWeight.w600),
),
),
SizedBox(height: 16.rpx),
// 二维码展示
Center(
child: QrImage(
data: qrinfo ?? "",
version: QrVersions.auto,
size: 135.rpx,
foregroundColor: Colors.black,
)),
SizedBox(height: 16.rpx),
Divider(height: 1.rpx, color: Colors.black12),
Container(
height: 40.rpx,
alignment: Alignment.center,
child: Row(children: [
// 关闭
Flexible(
flex: 1,
fit: FlexFit.loose,
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: Container(
alignment: Alignment.center,
child: Text(
bottomLeftText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.rpx,
color: bottomLeftTextColor,
fontWeight: FontWeight.w600),
),
),
),
),
Offstage(
offstage: hasMobile ? false : true,
child: VerticalDivider(
color: Colors.black12, width: 1.rpx),
),
// 发送二维码
Flexible(
flex: hasMobile ? 1 : 0,
fit: FlexFit.loose,
child: Offstage(
offstage: hasMobile ? false : true,
child: InkWell(
onTap: () async {
if (_timer != null &&
(_timer?.isActive ?? false)) {
return;
}
var code = await qrcodeVM.sendQRCodeToSms();
if (code == 200) {
_startTimer(setStateBottomSheet, personId);
showCustomToast(
S.current.pm_send_qrcode_success_tip,
_toastContent());
}
BIEvent.onCountEvent(
CustomAnalysisEvent.eventClickSendQRCode);
},
child: Container(
alignment: Alignment.center,
child: Text(
_sendQRCode,
style: TextStyle(
fontSize: 14.rpx,
color: _sendQRCodeColor,
fontWeight: FontWeight.w600),
),
),
),
),
),
]),
),
]),
),
]);
});
});
showModalBottomSheetFuture.then((value) {
BILog.i(tag, "showModalBottomSheet dismiss");
// 弹窗消失时关闭计时器,更新personId对应的计时时间
if (_timer != null) {
_timer?.cancel();
_timer = null;
cacheTimeCountBindPerson[personId] = _timeCount;
cacheMillisecondsTimeBindPerson[personId] =
DateTime.now().millisecondsSinceEpoch ~/ 1000;
isTimerDialogClose = true;
}
});
}
static void _initTimer(StateSetter setStateBottomSheet, int? personId) {
if (_timer != null && (_timer?.isActive ?? false)) {
_startTimer(setStateBottomSheet, personId);
} else {
if (cacheTimeCountBindPerson.containsKey(personId)) {
var timeCount = cacheTimeCountBindPerson[personId];
_timeCount = timeCount ?? 0;
if (_timeCount > 0) {
// 当前person之前触发了倒计时,应继续执行倒计时操作
_startTimer(setStateBottomSheet, personId);
// 立即刷新发送按钮的倒计时时间
setStateBottomSheet(() {
_sendQRCodeColor = C.font99;
_sendQRCode = "${S.current.pm_send_qrcode}(${_timeCount}s)";
});
return;
}
}
_sendQRCode = S.current.pm_send_qrcode;
_sendQRCodeColor = const Color(0xff3e71dd);
_timer = null;
_timeCount = sixtySeconds;
}
}
static void _startTimer(StateSetter state, int? personId) {
_timer ??= Timer.periodic(const Duration(seconds: 1), (timer) {
state(() {
if (_timeCount <= 0) {
_timer?.cancel();
_timer = null;
_timeCount = sixtySeconds;
if (cacheTimeCountBindPerson.containsKey(personId)) {
cacheTimeCountBindPerson.remove(personId);
}
if (cacheMillisecondsTimeBindPerson.containsKey(personId)) {
cacheMillisecondsTimeBindPerson.remove(personId);
}
_sendQRCode = S.current.pm_send_qrcode;
_sendQRCodeColor = const Color(0xff3e71dd);
} else {
_timeCount = (!isTimerDialogClose && Platform.isIOS)
? (sixtySeconds - timer.tick)
: (_timeCount - 1);
_sendQRCode = "${S.current.pm_send_qrcode}(${_timeCount}s)";
_sendQRCodeColor = C.font99;
}
});
});
}}
触发弹窗直接调用:
PersonQRCodeDialog.showPersonQRCodeDialog(...);