前言:
最近在做一个弹窗中点击发送短信的功能,要求是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(...);