import 'dart:async'; import 'package:fast/extension/button.dart'; import 'package:fast/extension/relation.dart'; import 'package:fast/model/model.dart'; import 'package:fast/utils/api.dart'; import 'package:fast/utils/global.dart'; import 'package:fast/utils/http_utils.dart'; import 'package:fast/utils/size_fit.dart'; import 'package:fast/utils/storage.dart'; import 'package:fast/utils/util.dart'; import 'package:fast/view/component/alert_widget.dart'; import 'package:fast/view/component/challenge_checkout.dart'; import 'package:fast/view/component/choose_mode.dart'; import 'package:fast/view/component/frame_image.dart'; import 'package:fast/view/component/guide.dart'; import 'package:fast/view/component/record.dart'; import 'package:fast/view/component/toast.dart'; import 'package:flutter/material.dart'; import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; import '../../constants.dart'; import 'bubble_widget.dart'; import 'progress_painter.dart'; import 'target_painter.dart'; class Fast extends StatefulWidget { const Fast({Key? key}) : super(key: key); @override FastState createState() => FastState(); } class FastState extends State with SingleTickerProviderStateMixin { double arcAngle = 0; //圆弧终点 double beginAngle = 0.0; //圆弧起点 double dy = 0; //起点y坐标 double dx = 0; //起点x坐标 late ui.Image startImage; //起点图标 late ui.Image progressImage; //起点图标 bool isLoad = false; //起点图片是否加载完毕,加载完毕后再用canvas画出 double endLeft = 0; //终点x坐标 double endTop = 0; //终点y坐标 int status = 0; //0准备中 1单次模式 2挑战模式 3进食模式 int targetHour = Global().fastHour; //目标小时 int targetMinute = Global().fastMinute; //目标分钟 int leftHour = 0; //进食小时 int leftMinute = 0; //进食分钟 String beginHour = ''; //开始时 点击开始按钮前,每分钟刷新 String beginMinutes = ''; //开始分 String endHour = ''; //结束时 开始时+目标时 String endMinutes = ''; //结束分 String currentHour = ''; //点击开始后,断食时长 String currentMinute = ''; DateTime beginTime = DateTime.now(); String beginCount = '00:00'; String beginSecond = '00'; String beginFormateTime = "00:00:00"; double rotate = 0; //当前位置箭头旋转的角度 double currentAngel = 0; //当前位置的弧度 bool showBeginCount = true; //是否显示正计时 bool showMinuteCountdown = false; //1分钟倒计时 bool showSecondCountdown = false; //10秒倒计时 String strMinuteCountdown = '00:59'; //倒计时60秒 String strSecondCountdown = '9'; //倒计时10秒 bool hiddenTime = false; //开始断食后4秒钟,再隐藏时间 bool showTips = false; //显示断食中的气泡 int recordBegin = 0; int recordEnd = 0; int recordTarget = 0; int enterTimes = 0; //进入页面倒计时,如果进食中,显示tips 这个时间判断是否显示进行中气泡以及是否显示结束任务 FastBean? currentFast; double circleWidth = Global().circleWidth; double paintWidth = Global().paintWidth; double progressWidth = 0.0; bool hasShowSingleConfirm = false; late AnimationController controller; late Animation animation0; late Animation animation1; late Animation animation2; late Animation animation3; late Animation animation4; Timer? _timer; @override void dispose() { controller.dispose(); _timer!.cancel(); super.dispose(); } @override void initState() { super.initState(); if (targetMinute > 0) { leftMinute = 60 - targetMinute; leftHour = 24 - targetHour - 1; } else { leftHour = 24 - targetHour; } setBeginTime(serverTime()); _getLocalImage(); const timeout = Duration(seconds: 1); if (_timer != null) { _timer!.cancel(); } _timer = Timer.periodic(timeout, (timer) { if (status == 0) { setBeginTime(serverTime()); } else { setBeginTime(beginTime); calculate(); } }); controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 2200)); controller.addListener(() { setState(() {}); }); animation0 = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: const Interval(0.3, 0.45))); animation1 = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: const Interval(0.45, 0.6))); animation2 = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: const Interval(0.6, 0.8))); animation3 = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: const Interval(0.8, 0.9))); animation4 = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: controller, curve: const Interval(0.9, 1.0))); Timer(const Duration(milliseconds: 200), () { controller.forward(); setState(() {}); }); Timer(const Duration(milliseconds: 3000), () { showGuide(); }); } void updateProgress() { FastBean? fastBean = Global().currentFast; if (fastBean == null) { setState(() { status = 0; }); return; } int status1 = 0; int targetDuration = (fastBean.endTime! - fastBean.startTime!) ~/ 60; int tH = targetDuration ~/ 60; int tM = (targetDuration % 60).toInt(); List result = leftHourAndMinute(tH, tM); int leftHour1 = result[0]; int leftMinute1 = result[1]; if (fastBean.ongoing == true) { if (fastBean.mode == 'SINGLE') { status1 = 1; } else if (fastBean.mode == 'CHALLENGER') { status1 = 2; getCheckinInfo(); } } DateTime time = fastBean.ongoing == true ? DateTime.fromMillisecondsSinceEpoch(fastBean.startTime! * 1000) : serverTime(); setState(() { currentFast = fastBean; targetHour = tH; targetMinute = tM; leftHour = leftHour1; leftMinute = leftMinute1; status = status1; beginTime = time; recordBegin = fastBean.startTime! * 1000; recordTarget = fastBean.endTime! * 1000; recordEnd = serverTime().millisecondsSinceEpoch; enterTimes = 6; }); if (fastBean.ongoing == false || fastBean.endTime! > serverTime().millisecondsSinceEpoch ~/ 1000) { return; } Timer(const Duration(seconds: 1), () { if (status == 2) { bool supertimeout = false; if (serverTime().millisecondsSinceEpoch ~/ 1000 - fastBean.startTime! > 24 * 3600) { supertimeout = true; } Global().homePage.showCheckout(false); return; } //show record page Timer(const Duration(seconds: 2), () { showRecordConfirm(currentFast!.startTime!, serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!); }); }); } List leftHourAndMinute(int h, int m) { int leftHour1 = 24; int leftMinute1 = 0; if (m > 0) { leftHour1--; leftMinute1 = 60 - m; } leftHour1 -= h; return [leftHour1, leftMinute1]; } void getCheckinInfo() {} DateTime serverTime() { int milliseconds = DateTime.now().millisecondsSinceEpoch; milliseconds = milliseconds + Global().timeSeconds * 1000; return DateTime.fromMillisecondsSinceEpoch(milliseconds); } void demo() { showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, useSafeArea: false, builder: (BuildContext context) { return Record( fast: this, begin: 0, end: 0, target: 0, ); }); } Future showGuide() async { bool? hasShowGuide = StorageUtil().prefs!.getBool('hasShowGuide'); if (hasShowGuide != null && hasShowGuide) { return; } StorageUtil().prefs!.setBool('hasShowGuide', true); showDialog( context: context, barrierDismissible: false, barrierColor: const Color(0x99000D1F), useSafeArea: false, builder: (BuildContext context) { return Guide( endLeft: endLeft, endTop: endTop, left: circleWidth / 2.0 + Global().progressWidth * cos(beginAngle) - paintWidth / 2.0, top: circleWidth / 2.0 + Global().progressWidth * sin(beginAngle) - paintWidth / 2.0, ); }); } calculate() { enterTimes--; var count = (serverTime().millisecondsSinceEpoch - beginTime.millisecondsSinceEpoch) / 60000; var hour = count ~/ 60; var minute = (count % 60).toInt(); var count1 = (serverTime().millisecondsSinceEpoch - beginTime.millisecondsSinceEpoch) / 1000; var second = (count1 % 3600 % 60).toInt(); var secondCount = (serverTime().millisecondsSinceEpoch - beginTime.millisecondsSinceEpoch) / 1000; var millis = beginTime.millisecondsSinceEpoch; millis = millis + (targetHour * 60 + targetMinute) * 60 * 1000; var targetDate = DateTime.fromMillisecondsSinceEpoch(millis); var leftSeconds = ((targetDate.millisecondsSinceEpoch - serverTime().millisecondsSinceEpoch) / 1000) .floor(); if (leftSeconds < 60) { leftSeconds++; } var show1 = false; var show2 = false; var show3 = false; if (leftSeconds < 0 || leftSeconds >= 60) { show1 = true; } else if (leftSeconds >= 10) { show2 = true; } else { show3 = true; } setState(() { beginCount = (hour.toString()).padLeft(2, '0') + ':' + (minute.toString()).padLeft(2, '0'); beginSecond = second.toString().padLeft(2, '0'); beginFormateTime = (hour.toString()).padLeft(2, '0') + ':' + (minute.toString()).padLeft(2, '0') + ':' + second.toString().padLeft(2, '0'); currentAngel = count / (24 * 60) * 2 * pi; // rotate = 180 showBeginCount = show1; showMinuteCountdown = show2; showSecondCountdown = show3; strSecondCountdown = leftSeconds.toString(); strMinuteCountdown = '00:' + (leftSeconds.toString()).padLeft(2, '0'); hiddenTime = secondCount >= 6 && (enterTimes < 0 || enterTimes > 6); showTips = (secondCount > 0 && secondCount < 4) || (enterTimes > 1 && enterTimes < 4); }); if (leftSeconds == 0) { achieve(); } } void start() { showDialog( context: context, barrierDismissible: false, useSafeArea: false, barrierColor: const Color(0xFF000D1F), builder: (BuildContext context) { return ChooseMode( seconds: targetHour * 3600 + targetMinute * 60, callback: (type) { startFast(type); }, ); }); } Future startFast(type) async { int days = [1, 3, 5, 7][type]; int begin = DateTime.now().millisecondsSinceEpoch ~/ 1000; int end = begin + targetHour * 3600 + targetMinute * 60; Map data = await HttpUtils.post(Api.start, data: { "days": days, "mode": type == 0 ? 'SINGLE' : "CHALLENGER", "start_time": begin, "end_time": end }); FastBean bean = FastBean.fromJson(data); setState(() { status = type == 0 ? 1 : 2; beginTime = serverTime(); enterTimes = 6; currentFast = bean; }); Global().homePage.getDatas(); } void end() { if (Global().allowNotification == false && Global().pushEnable) { Util().showNotificationStatus(context); return; } if (status == 2) { showConfirm('确定现在退出\n"${currentFast!.days}天连续计划"吗?', () { Global().homePage.showCheckout(true); }); return; } int secondsCount = (serverTime().millisecondsSinceEpoch - beginTime.millisecondsSinceEpoch) ~/ 1000; int target = targetHour * 3600 + targetMinute * 60; if (secondsCount < 3600) { showConfirm('断食时长过短,将不被记录\n确定要结束吗?', () { giveupRecord(); }); } else if (target - secondsCount > 60) { showConfirm('还未到预定断食结束时间,\n确定要结束吗?', () { showRecordConfirm(currentFast!.startTime!, serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!); }); } else { showRecordConfirm(currentFast!.startTime!, serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!); } } void showRecordConfirm(int begin, int end, int target) { if (hasShowSingleConfirm || Global().currentFast == null) { //防止重复弹窗打开 // print("lalalal"); return; } hasShowSingleConfirm = true; showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, useSafeArea: false, builder: (BuildContext context) { return Record( fast: this, begin: begin, end: end, target: target, ); }); } void closeRecordConfirm() { hasShowSingleConfirm = false; } void showConfirm(String title, Function() confirmCallback) { showDialog( context: context, barrierDismissible: false, barrierColor: const Color(0xF2000D1F), builder: (BuildContext context) { return AlertWidget( title: title, confirm: '确定', confirmCallback: () { Navigator.of(context).pop(); confirmCallback(); }); }); } //单次模式 确认记录 Future confirmRecord(endTimeSeconds, isRightTime, enterTime) async { Map data = await HttpUtils.post(Api.end, data: {"real_end_time": endTimeSeconds, "real_entry_dt": enterTime}); if (data != null) {} reset(); Global().mainPage.showMe(); showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, builder: (BuildContext context) { return Toast( title: isRightTime ? '成功记录' : '完成记录', content: const SizedBox( width: 0, height: 0, ), ); }); } Future expandTime(seconds) async { Map data = await HttpUtils.post(Api.delay, data: {"seconds": seconds}); Global().homePage.getDatas(); String strTime = Util().betweenTimeBySeconds(seconds); showDialog( context: context, barrierDismissible: false, barrierColor: Colors.transparent, builder: (BuildContext context) { return Toast( title: '记录延时\n$strTime', content: const SizedBox( width: 0, height: 0, ), ); }); } //单次模式 放弃记录 Future giveupRecord() async { Map data = await HttpUtils.post(Api.giveUp, data: {}); if (data != null) {} reset(); } void reset() { setState(() { status = 0; currentAngel = 0; beginTime = DateTime.now(); hiddenTime = false; beginSecond = '00'; beginCount = '00:00'; }); Global().homePage.getDatas(); } void expand() {} void achieve() { if (status == 1) { showRecordConfirm(currentFast!.startTime!, currentFast!.endTime!, currentFast!.endTime!); } else if (status == 2) { Global().homePage.showCheckout(false); } } void setBeginTime(now) { // var now = DateTime.now(); var countMinutes = now.minute + (now.hour - 6) * 60; var millis = now.millisecondsSinceEpoch; millis = millis + (targetHour * 60 + targetMinute) * 60 * 1000; var end = DateTime.fromMillisecondsSinceEpoch(millis); var angel = countMinutes / (24 * 60) * 2 * pi; var temp = (end.minute + (end.hour - 6) * 60) / (24 * 60) * 2 * pi; var left = circleWidth / 2 + Global().progressWidth * cos(temp); var top = circleWidth / 2 + Global().progressWidth * sin(temp); setState(() { beginHour = now.hour.toString(); beginMinutes = now.minute.toString(); endHour = end.hour.toString(); endMinutes = end.minute.toString(); beginAngle = angel; arcAngle = temp; endLeft = left - Global().paintWidth / 2.0; endTop = top - Global().paintWidth / 2.0; }); } Future getAssetImage(String asset, {width, height}) async { ByteData data = await rootBundle.load(asset); ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width, targetHeight: height); ui.FrameInfo fi = await codec.getNextFrame(); return fi.image; } _getLocalImage() async { ui.Image imageFrame = await getAssetImage('assets/images/seek_start.png', width: 120, height: 120); ui.Image imageFrame2 = await getAssetImage('assets/images/ing.png', width: 120, height: 120); setState(() { startImage = imageFrame; progressImage = imageFrame2; isLoad = true; }); } void updateArc(dx, dy, width) { if (status != 0) { return; } double x = dx - circleWidth / 2; double y = dy - circleWidth / 2; double angel = atan2(y, x); if (angel < 0) { angel = 2 * pi + angel; } var left = circleWidth / 2 + Global().progressWidth * cos(angel); var top = circleWidth / 2 + Global().progressWidth * sin(angel); double endAngel = angel + pi / 2; if (endAngel >= 2 * pi) { endAngel -= 2 * pi; } int endCount = endAngel * 24 * 60 ~/ (2 * pi); int endHourTemp = endCount ~/ 60; int endMinuteTemp = (endCount % 60).toInt(); int nowHour = serverTime().hour; int nowMinute = serverTime().minute; if (endHourTemp < nowHour) { endHourTemp += 24; } else if (endHourTemp == nowHour && endMinuteTemp < nowMinute) { endHourTemp += 24; } int targetHourTemp, targetMinuteTemp; if (endMinuteTemp >= nowMinute) { targetMinuteTemp = endMinuteTemp - nowMinute; } else { targetMinuteTemp = 60 + endMinuteTemp - nowMinute; endHourTemp -= 1; } targetHourTemp = endHourTemp - nowHour; targetMinuteTemp = ((targetMinuteTemp % 60) / 15).floor() * 15; if (targetHourTemp * 60 + targetMinuteTemp < 60 || targetHourTemp * 60 + targetMinuteTemp > 23 * 60) { return; } int leftHourTemp = 24; int leftMinuteTemp = 0; if (targetMinuteTemp > 0) { leftHourTemp--; leftMinuteTemp = 60 - targetMinuteTemp; } leftHourTemp -= targetHourTemp; if (targetMinute == 0) { Vibrate.feedback(FeedbackType.success); } // if (targetMinute != targetMinuteTemp) { // Vibrate.feedback(FeedbackType.success); // } setState(() { arcAngle = angel; endLeft = left - 20.px; endTop = top - 20.px; endHour = endHourTemp.toString(); endMinutes = endMinuteTemp.toString(); targetHour = targetHourTemp; targetMinute = targetMinuteTemp; leftHour = leftHourTemp; leftMinute = leftMinuteTemp; }); } getTextWidget() { if (showBeginCount) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( foregroundDecoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xAA000D1F), Color(0x00000D1F)])), child: Text( beginCount, style: TextStyle( height: 1.0, fontFamily: 'Fast', color: kThemeColor, fontSize: 32.px), ), ), Text( beginSecond, style: TextStyle( color: kThemeColor, fontFamily: 'Fast', fontSize: 64.px, height: 1.0), ), SizedBox( height: 10.px, ) ], ); } else if (showSecondCountdown) { return Container( margin: EdgeInsets.only(bottom: 20.px), child: Text( strSecondCountdown, style: TextStyle( color: Colors.white, fontFamily: 'Exo2', fontWeight: FontWeight.w900, fontSize: 96.px), ), ); } return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( strMinuteCountdown, style: TextStyle( color: Colors.white, height: 1.0, fontFamily: 'Fast', fontSize: 40.px), ), Container( foregroundDecoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0x00000D1F), Color(0xFF000D1F)])), child: Text( beginFormateTime, style: TextStyle( color: kThemeColor, fontFamily: 'Fast', height: 1.0, fontSize: 28.px), ), ) ], ); } bubbleWidget(strHour, strMinute, width) { return Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( margin: EdgeInsets.only(top: 2.px, left: 0.px), child: Text(strHour, style: TextStyle( color: kThemeColor, fontFamily: "Exo2", fontWeight: FontWeight.w600, fontSize: 32.px)), ), Text(strMinute, style: TextStyle( color: kThemeColor, fontFamily: "Exo2", fontWeight: FontWeight.w600, fontSize: 32.px)) ], ).relationOne( // -40.px + width / 2.0 + 8.px, -40.px + width / 2.0, -35.px, BubbleWidget( 80.px, 42.px, kThemeColor, BubbleArrowDirection.bottom, arrAngle: 80.px, arrHeight: 8.px, child: Text( '断食中…', style: TextStyle( color: Colors.black, fontSize: 14.px, fontWeight: FontWeight.bold), ), )); } getTimeStatusWidget() { if (hiddenTime) { return Container( alignment: Alignment.center, child: RRectButton( status == 1 ? '结束断食' : '结束挑战', width: 144.px, height: 50.px, backgroundColor: const Color(0x1AC4CCDA), textColor: const Color(0xFFC4CCDA), radius: 25.px, fontSize: 16.px, onPressed: () => end(), )); } String strHour = targetMinute == 0 ? targetHour.toString() : targetHour.toString() + ':'; String strMinute = targetMinute == 0 ? '' : targetMinute.toString().padLeft(2, '0'); double width = Util() .boundingTextSize( strHour, TextStyle( fontFamily: 'Exo2', fontWeight: FontWeight.w600, fontSize: 32.px)) .width + Util() .boundingTextSize( strMinute, TextStyle( fontFamily: 'Exo2', fontWeight: FontWeight.w600, fontSize: 32.px)) .width; return Container( height: 50.px, alignment: Alignment.center, child: Row( children: [ Expanded( child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: SizedBox( width: 0.px, )), showTips ? const SizedBox( width: 0, height: 0, ).relationOne( 80.px - width, 0, bubbleWidget(strHour, strMinute, width)) : Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(strHour, style: TextStyle( color: kThemeColor, fontFamily: "Exo2", fontWeight: FontWeight.w600, fontSize: 32.px)), Text(strMinute, style: TextStyle( color: kThemeColor, fontFamily: "Exo2", fontWeight: FontWeight.w600, fontSize: 32.px)) ], ) ])), SizedBox( width: 10.px, ), Container( margin: EdgeInsets.only( top: 4.px, ), child: Image.asset( 'assets/images/seperate1.png', width: 8.px, height: 16.px, ), ), SizedBox( width: 10.px, ), Expanded( child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( leftMinute == 0 ? leftHour.toString() : leftHour.toString() + ':', style: TextStyle( color: Colors.white, fontFamily: "Exo2", fontSize: 32.px)), Text( leftMinute == 0 ? '' : leftMinute.toString().padLeft(2, '0'), style: TextStyle( color: Colors.white, fontFamily: "Exo2", fontSize: 32.px), ) ])), ], )); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; final width = size.width; var step = 0.171; var arcTemp = 0.0; // beginAngle = -pi/2.0; // arcAngle = pi+pi*2/3.0; if (beginAngle > pi) { beginAngle = beginAngle - 2 * pi; } if (arcAngle - beginAngle > 2 * pi) { arcAngle = arcAngle - 2 * pi; } if (beginAngle > 0 && arcAngle < 0) { arcAngle = arcAngle + 2 * pi; } if (beginAngle > arcAngle) { arcAngle = arcAngle + 2 * pi; } // if (beginAngle > pi) { // arcTemp = (beginAngle - 2 * pi) * (1 - animation3.value) + // arcAngle * animation3.value; // } else { // arcTemp = // beginAngle * (1 - animation3.value) + arcAngle * animation3.value; // } arcTemp = beginAngle * (1 - animation3.value) + arcAngle * animation3.value; return Stack(children: [ // SizedBox(width: 375.px,), if (status == 2) Positioned( right: 14.px, top: 0.px, child: Opacity( opacity: animation4.value, child: Container( width: 64.px, height: 54.px, padding: EdgeInsets.all(8.px), decoration: BoxDecoration( color: const Color(0x1AC4CCDA), borderRadius: BorderRadius.all(Radius.circular(12.px)), ), child: Column( children: [ Text( '${currentFast!.days}天挑战', style: TextStyle( color: const Color(0x66FFFFFF), fontSize: 10.px, height: 1.0), ), Container( height: 1.px, color: const Color(0x1AFFFFFF), margin: EdgeInsets.only(top: 5.px, bottom: 5.px), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '${currentFast!.finishDays}', style: TextStyle( color: const Color(0xFFC4CCDA), fontFamily: 'Exo2', fontWeight: FontWeight.w600, fontSize: 16.px, height: 1.0), ), SizedBox( width: 2.px, ), Image.asset( 'assets/images/seperate1.png', width: 4.px, height: 8.px, ), SizedBox( width: 2.px, ), Text( '${currentFast!.days}', style: TextStyle( color: const Color(0xFFC4CCDA), fontFamily: 'Exo2', fontWeight: FontWeight.w600, fontSize: 16.px, height: 1.0), ), ], ) ], ), ))), SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Stack( children: [ Opacity( opacity: animation0.value, child: Transform.scale( scale: animation0.value, child: Container( width: circleWidth, height: circleWidth, margin: EdgeInsets.fromLTRB(0, 0, 0, 16.px), decoration: const BoxDecoration( image: DecorationImage( alignment: Alignment.topLeft, image: AssetImage('assets/images/seek_bg.png'))), child: Opacity( opacity: animation2.value, child: CustomPaint( foregroundPainter: TargetPainter( lineColor: Colors.yellow, completeColor: Colors .orange, //[Colors.orange,Colors.blue], // arcAngle: arcAngle*animation3.value, arcAngle: arcTemp, beginAngle: beginAngle, width: paintWidth, localImage: isLoad ? startImage : null), ), ), ), ), ), // Positioned(child: Container(width: 40,height: 40,color: Colors.red,)), Opacity( opacity: animation3.value, child: Container( width: paintWidth, height: paintWidth, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(20.px)), color: const Color(0xFFC4CCDA)), margin: EdgeInsets.only(left: endLeft, top: endTop), ), ), if (status != 0) Transform.rotate( angle: beginAngle - step, child: SizedBox( width: circleWidth, height: circleWidth, child: CustomPaint( foregroundPainter: ProgressPainter( lineColor: Colors.yellow, completeColor: Colors.orange, //[Colors.orange,Colors.blue], arcAngle: currentAngel * animation3.value, beginAngle: beginAngle, width: paintWidth, localImage: isLoad ? progressImage : null), ), ), ), if (status != 0) ingImg(beginAngle, currentAngel), // Opacity( // opacity: animation4.value, // child: ingImg(beginAngle, currentAngel), // ), Opacity( opacity: animation4.value, child: Container( alignment: Alignment.center, child: status == 0 ? TextButton( onPressed: () => {start()}, child: Image( image: const AssetImage('assets/images/begin.png'), width: Global().paintWidth * 2, height: Global().paintWidth * 2, ), ) : getTextWidget(), width: circleWidth, height: circleWidth, ), ), startImg(beginAngle), Opacity( opacity: animation3.value, child: GestureDetector( onTapDown: (TapDownDetails details) { updateArc(details.localPosition.dx, details.localPosition.dy, width); }, onLongPressMoveUpdate: (details) { updateArc(details.localPosition.dx, details.localPosition.dy, width); }, onHorizontalDragUpdate: (details) { updateArc(details.localPosition.dx, details.localPosition.dy, width); }, onVerticalDragUpdate: (details) { updateArc(details.localPosition.dx, details.localPosition.dy, width); }, child: Container( margin: EdgeInsets.fromLTRB(endLeft, endTop, 0, 0), child: Image( image: const AssetImage('assets/images/seek_end.png'), width: paintWidth, height: paintWidth, ), ), ), ), ], ), Opacity( opacity: animation2.value, child: getTimeStatusWidget(), ) ], )) ]); } ingImg(beginAngle, currentAngel) { if (currentAngel == 0) { return const SizedBox( width: 0.0, height: 0.0, ); } var step = 0.171; var maxEnd = beginAngle + currentAngel; if (maxEnd > 2 * pi - step) { maxEnd = 2 * pi - 2 * step; } return Positioned( left: circleWidth / 2.0 + Global().progressWidth * cos(beginAngle + currentAngel) - paintWidth / 2.0, top: circleWidth / 2.0 + Global().progressWidth * sin(beginAngle + currentAngel) - paintWidth / 2.0, child: Transform.rotate( angle: beginAngle + currentAngel - pi / 2.0, child: Opacity( opacity: animation4.value, child: Container( color: Colors.transparent, width: paintWidth, height: paintWidth, child: FrameAnimationImage(assetList: const [ 'assets/images/a_00000.png', 'assets/images/a_00001.png', 'assets/images/a_00002.png', 'assets/images/a_00003.png', 'assets/images/a_00004.png', 'assets/images/a_00005.png', 'assets/images/a_00006.png', 'assets/images/a_00007.png', 'assets/images/a_00008.png', 'assets/images/a_00009.png', 'assets/images/a_00010.png', 'assets/images/a_00011.png', 'assets/images/a_00012.png', 'assets/images/a_00013.png', 'assets/images/a_00014.png', 'assets/images/a_00015.png', ], width: paintWidth, height: paintWidth, interval: 40), ), ), )); } startImg(beginAngle) { var step = 0.171; var maxEnd = beginAngle; if (maxEnd > 2 * pi - step) { maxEnd = 2 * pi - 2 * step; } return Positioned( left: circleWidth / 2.0 + Global().progressWidth * cos(beginAngle) - paintWidth / 2.0, top: circleWidth / 2.0 + Global().progressWidth * sin(beginAngle) - paintWidth / 2.0, child: Opacity( opacity: animation4.value, child: Container( color: Colors.transparent, width: paintWidth, height: paintWidth, child: Image.asset( 'assets/images/seek_start.png', width: paintWidth, height: paintWidth, ), ), ), ); } } // ignore: must_be_immutable class TextGradientColorWidget extends StatefulWidget { String data; TextStyle? style; List colors; TextGradientColorWidget({Key? key, required this.data, required this.colors}) : super(key: key); @override _TextGradientColorState createState() => _TextGradientColorState(); } class _TextGradientColorState extends State { WidgetsBinding? widgetsBinding = WidgetsBinding.instance; TextStyle? textStyle; @override void initState() { // TODO: implement initState super.initState(); widgetsBinding!.addPostFrameCallback((timeStamp) { final RenderBox? box = context.findRenderObject() as RenderBox; var left = 0.0; var width = 0.0; if (box != null) { final topLeftPosition = box.localToGlobal(Offset.zero); final size = box.size; left = topLeftPosition.dx; width = size.width; setState(() { textStyle = TextStyle( fontSize: 30.px, fontFamily: 'Fast', foreground: Paint() ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: widget.colors, ).createShader(Rect.fromLTWH(left - 20, topLeftPosition.dy - 260, width, size.height + 20))); }); //注意 TextStyle中color和foreground 中colors不能同时设置 } }); } @override Widget build(BuildContext context) { // TODO: implement build return Text(widget.data, style: textStyle); } }