fast.dart 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. import 'dart:async';
  2. import 'package:fast/extension/button.dart';
  3. import 'package:fast/extension/relation.dart';
  4. import 'package:fast/model/model.dart';
  5. import 'package:fast/utils/api.dart';
  6. import 'package:fast/utils/global.dart';
  7. import 'package:fast/utils/http_utils.dart';
  8. import 'package:fast/utils/size_fit.dart';
  9. import 'package:fast/utils/storage.dart';
  10. import 'package:fast/utils/util.dart';
  11. import 'package:fast/view/component/alert_widget.dart';
  12. import 'package:fast/view/component/challenge_checkout.dart';
  13. import 'package:fast/view/component/choose_mode.dart';
  14. import 'package:fast/view/component/frame_image.dart';
  15. import 'package:fast/view/component/guide.dart';
  16. import 'package:fast/view/component/record.dart';
  17. import 'package:fast/view/component/toast.dart';
  18. import 'package:flutter/material.dart';
  19. import 'dart:math';
  20. import 'dart:ui' as ui;
  21. import 'package:flutter/services.dart';
  22. import 'package:flutter_vibrate/flutter_vibrate.dart';
  23. import '../../constants.dart';
  24. import 'bubble_widget.dart';
  25. import 'progress_painter.dart';
  26. import 'target_painter.dart';
  27. class Fast extends StatefulWidget {
  28. const Fast({Key? key}) : super(key: key);
  29. @override
  30. FastState createState() => FastState();
  31. }
  32. class FastState extends State<Fast> with SingleTickerProviderStateMixin {
  33. double arcAngle = 0; //圆弧终点
  34. double beginAngle = 0.0; //圆弧起点
  35. double dy = 0; //起点y坐标
  36. double dx = 0; //起点x坐标
  37. late ui.Image startImage; //起点图标
  38. late ui.Image progressImage; //起点图标
  39. bool isLoad = false; //起点图片是否加载完毕,加载完毕后再用canvas画出
  40. double endLeft = 0; //终点x坐标
  41. double endTop = 0; //终点y坐标
  42. int status = 0; //0准备中 1单次模式 2挑战模式 3进食模式
  43. int targetHour = Global().fastHour; //目标小时
  44. int targetMinute = Global().fastMinute; //目标分钟
  45. int leftHour = 0; //进食小时
  46. int leftMinute = 0; //进食分钟
  47. String beginHour = ''; //开始时 点击开始按钮前,每分钟刷新
  48. String beginMinutes = ''; //开始分
  49. String endHour = ''; //结束时 开始时+目标时
  50. String endMinutes = ''; //结束分
  51. String currentHour = ''; //点击开始后,断食时长
  52. String currentMinute = '';
  53. DateTime beginTime = DateTime.now();
  54. String beginCount = '00:00';
  55. String beginSecond = '00';
  56. String beginFormateTime = "00:00:00";
  57. double rotate = 0; //当前位置箭头旋转的角度
  58. double currentAngel = 0; //当前位置的弧度
  59. bool showBeginCount = true; //是否显示正计时
  60. bool showMinuteCountdown = false; //1分钟倒计时
  61. bool showSecondCountdown = false; //10秒倒计时
  62. String strMinuteCountdown = '00:59'; //倒计时60秒
  63. String strSecondCountdown = '9'; //倒计时10秒
  64. bool hiddenTime = false; //开始断食后4秒钟,再隐藏时间
  65. bool showTips = false; //显示断食中的气泡
  66. int recordBegin = 0;
  67. int recordEnd = 0;
  68. int recordTarget = 0;
  69. int enterTimes = 0; //进入页面倒计时,如果进食中,显示tips 这个时间判断是否显示进行中气泡以及是否显示结束任务
  70. FastBean? currentFast;
  71. double circleWidth = Global().circleWidth;
  72. double paintWidth = Global().paintWidth;
  73. double progressWidth = 0.0;
  74. bool hasShowSingleConfirm = false;
  75. late AnimationController controller;
  76. late Animation animation0;
  77. late Animation animation1;
  78. late Animation animation2;
  79. late Animation animation3;
  80. late Animation animation4;
  81. Timer? _timer;
  82. @override
  83. void dispose() {
  84. controller.dispose();
  85. _timer!.cancel();
  86. super.dispose();
  87. }
  88. @override
  89. void initState() {
  90. super.initState();
  91. if (targetMinute > 0) {
  92. leftMinute = 60 - targetMinute;
  93. leftHour = 24 - targetHour - 1;
  94. } else {
  95. leftHour = 24 - targetHour;
  96. }
  97. setBeginTime(serverTime());
  98. _getLocalImage();
  99. const timeout = Duration(seconds: 1);
  100. if (_timer != null) {
  101. _timer!.cancel();
  102. }
  103. _timer = Timer.periodic(timeout, (timer) {
  104. if (status == 0) {
  105. setBeginTime(serverTime());
  106. } else {
  107. setBeginTime(beginTime);
  108. calculate();
  109. }
  110. });
  111. controller = AnimationController(
  112. vsync: this, duration: const Duration(milliseconds: 2200));
  113. controller.addListener(() {
  114. setState(() {});
  115. });
  116. animation0 = Tween(begin: 0.0, end: 1.0).animate(
  117. CurvedAnimation(parent: controller, curve: const Interval(0.3, 0.45)));
  118. animation1 = Tween(begin: 0.0, end: 1.0).animate(
  119. CurvedAnimation(parent: controller, curve: const Interval(0.45, 0.6)));
  120. animation2 = Tween(begin: 0.0, end: 1.0).animate(
  121. CurvedAnimation(parent: controller, curve: const Interval(0.6, 0.8)));
  122. animation3 = Tween(begin: 0.0, end: 1.0).animate(
  123. CurvedAnimation(parent: controller, curve: const Interval(0.8, 0.9)));
  124. animation4 = Tween(begin: 0.0, end: 1.0).animate(
  125. CurvedAnimation(parent: controller, curve: const Interval(0.9, 1.0)));
  126. Timer(const Duration(milliseconds: 200), () {
  127. controller.forward();
  128. setState(() {});
  129. });
  130. Timer(const Duration(milliseconds: 3000), () {
  131. showGuide();
  132. });
  133. }
  134. void updateProgress() {
  135. FastBean? fastBean = Global().currentFast;
  136. if (fastBean == null) {
  137. setState(() {
  138. status = 0;
  139. });
  140. return;
  141. }
  142. int status1 = 0;
  143. int targetDuration = (fastBean.endTime! - fastBean.startTime!) ~/ 60;
  144. int tH = targetDuration ~/ 60;
  145. int tM = (targetDuration % 60).toInt();
  146. List<int> result = leftHourAndMinute(tH, tM);
  147. int leftHour1 = result[0];
  148. int leftMinute1 = result[1];
  149. if (fastBean.ongoing == true) {
  150. if (fastBean.mode == 'SINGLE') {
  151. status1 = 1;
  152. } else if (fastBean.mode == 'CHALLENGER') {
  153. status1 = 2;
  154. getCheckinInfo();
  155. }
  156. }
  157. DateTime time = fastBean.ongoing == true
  158. ? DateTime.fromMillisecondsSinceEpoch(fastBean.startTime! * 1000)
  159. : serverTime();
  160. setState(() {
  161. currentFast = fastBean;
  162. targetHour = tH;
  163. targetMinute = tM;
  164. leftHour = leftHour1;
  165. leftMinute = leftMinute1;
  166. status = status1;
  167. beginTime = time;
  168. recordBegin = fastBean.startTime! * 1000;
  169. recordTarget = fastBean.endTime! * 1000;
  170. recordEnd = serverTime().millisecondsSinceEpoch;
  171. enterTimes = 6;
  172. });
  173. if (fastBean.ongoing == false ||
  174. fastBean.endTime! > serverTime().millisecondsSinceEpoch ~/ 1000) {
  175. return;
  176. }
  177. Timer(const Duration(seconds: 1), () {
  178. if (status == 2) {
  179. bool supertimeout = false;
  180. if (serverTime().millisecondsSinceEpoch ~/ 1000 - fastBean.startTime! >
  181. 24 * 3600) {
  182. supertimeout = true;
  183. }
  184. Global().homePage.showCheckout(false);
  185. return;
  186. }
  187. //show record page
  188. Timer(const Duration(seconds: 2), () {
  189. showRecordConfirm(currentFast!.startTime!,
  190. serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!);
  191. });
  192. });
  193. }
  194. List<int> leftHourAndMinute(int h, int m) {
  195. int leftHour1 = 24;
  196. int leftMinute1 = 0;
  197. if (m > 0) {
  198. leftHour1--;
  199. leftMinute1 = 60 - m;
  200. }
  201. leftHour1 -= h;
  202. return [leftHour1, leftMinute1];
  203. }
  204. void getCheckinInfo() {}
  205. DateTime serverTime() {
  206. int milliseconds = DateTime.now().millisecondsSinceEpoch;
  207. milliseconds = milliseconds + Global().timeSeconds * 1000;
  208. return DateTime.fromMillisecondsSinceEpoch(milliseconds);
  209. }
  210. void demo() {
  211. showDialog(
  212. context: context,
  213. barrierDismissible: false,
  214. barrierColor: Colors.transparent,
  215. useSafeArea: false,
  216. builder: (BuildContext context) {
  217. return Record(
  218. fast: this,
  219. begin: 0,
  220. end: 0,
  221. target: 0,
  222. );
  223. });
  224. }
  225. Future showGuide() async {
  226. bool? hasShowGuide = StorageUtil().prefs!.getBool('hasShowGuide');
  227. if (hasShowGuide != null && hasShowGuide) {
  228. return;
  229. }
  230. StorageUtil().prefs!.setBool('hasShowGuide', true);
  231. showDialog(
  232. context: context,
  233. barrierDismissible: false,
  234. barrierColor: const Color(0x99000D1F),
  235. useSafeArea: false,
  236. builder: (BuildContext context) {
  237. return Guide(
  238. endLeft: endLeft,
  239. endTop: endTop,
  240. left: circleWidth / 2.0 +
  241. Global().progressWidth * cos(beginAngle) -
  242. paintWidth / 2.0,
  243. top: circleWidth / 2.0 +
  244. Global().progressWidth * sin(beginAngle) -
  245. paintWidth / 2.0,
  246. );
  247. });
  248. }
  249. calculate() {
  250. enterTimes--;
  251. var count = (serverTime().millisecondsSinceEpoch -
  252. beginTime.millisecondsSinceEpoch) /
  253. 60000;
  254. var hour = count ~/ 60;
  255. var minute = (count % 60).toInt();
  256. var count1 = (serverTime().millisecondsSinceEpoch -
  257. beginTime.millisecondsSinceEpoch) /
  258. 1000;
  259. var second = (count1 % 3600 % 60).toInt();
  260. var secondCount = (serverTime().millisecondsSinceEpoch -
  261. beginTime.millisecondsSinceEpoch) /
  262. 1000;
  263. var millis = beginTime.millisecondsSinceEpoch;
  264. millis = millis + (targetHour * 60 + targetMinute) * 60 * 1000;
  265. var targetDate = DateTime.fromMillisecondsSinceEpoch(millis);
  266. var leftSeconds = ((targetDate.millisecondsSinceEpoch -
  267. serverTime().millisecondsSinceEpoch) /
  268. 1000)
  269. .floor();
  270. if (leftSeconds < 60) {
  271. leftSeconds++;
  272. }
  273. var show1 = false;
  274. var show2 = false;
  275. var show3 = false;
  276. if (leftSeconds < 0 || leftSeconds >= 60) {
  277. show1 = true;
  278. } else if (leftSeconds >= 10) {
  279. show2 = true;
  280. } else {
  281. show3 = true;
  282. }
  283. setState(() {
  284. beginCount = (hour.toString()).padLeft(2, '0') +
  285. ':' +
  286. (minute.toString()).padLeft(2, '0');
  287. beginSecond = second.toString().padLeft(2, '0');
  288. beginFormateTime = (hour.toString()).padLeft(2, '0') +
  289. ':' +
  290. (minute.toString()).padLeft(2, '0') +
  291. ':' +
  292. second.toString().padLeft(2, '0');
  293. currentAngel = count / (24 * 60) * 2 * pi;
  294. // rotate = 180
  295. showBeginCount = show1;
  296. showMinuteCountdown = show2;
  297. showSecondCountdown = show3;
  298. strSecondCountdown = leftSeconds.toString();
  299. strMinuteCountdown = '00:' + (leftSeconds.toString()).padLeft(2, '0');
  300. hiddenTime = secondCount >= 6 && (enterTimes < 0 || enterTimes > 6);
  301. showTips = (secondCount > 0 && secondCount < 4) ||
  302. (enterTimes > 1 && enterTimes < 4);
  303. });
  304. if (leftSeconds == 0) {
  305. achieve();
  306. }
  307. }
  308. void start() {
  309. showDialog(
  310. context: context,
  311. barrierDismissible: false,
  312. useSafeArea: false,
  313. barrierColor: const Color(0xFF000D1F),
  314. builder: (BuildContext context) {
  315. return ChooseMode(
  316. seconds: targetHour * 3600 + targetMinute * 60,
  317. callback: (type) {
  318. startFast(type);
  319. },
  320. );
  321. });
  322. }
  323. Future startFast(type) async {
  324. int days = [1, 3, 5, 7][type];
  325. int begin = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  326. int end = begin + targetHour * 3600 + targetMinute * 60;
  327. Map<String, dynamic> data = await HttpUtils.post(Api.start, data: {
  328. "days": days,
  329. "mode": type == 0 ? 'SINGLE' : "CHALLENGER",
  330. "start_time": begin,
  331. "end_time": end
  332. });
  333. FastBean bean = FastBean.fromJson(data);
  334. setState(() {
  335. status = type == 0 ? 1 : 2;
  336. beginTime = serverTime();
  337. enterTimes = 6;
  338. currentFast = bean;
  339. });
  340. Global().homePage.getDatas();
  341. }
  342. void end() {
  343. if (Global().allowNotification == false && Global().pushEnable) {
  344. Util().showNotificationStatus(context);
  345. return;
  346. }
  347. if (status == 2) {
  348. showConfirm('确定现在退出\n"${currentFast!.days}天连续计划"吗?', () {
  349. Global().homePage.showCheckout(true);
  350. });
  351. return;
  352. }
  353. int secondsCount = (serverTime().millisecondsSinceEpoch -
  354. beginTime.millisecondsSinceEpoch) ~/
  355. 1000;
  356. int target = targetHour * 3600 + targetMinute * 60;
  357. if (secondsCount < 3600) {
  358. showConfirm('断食时长过短,将不被记录\n确定要结束吗?', () {
  359. giveupRecord();
  360. });
  361. } else if (target - secondsCount > 60) {
  362. showConfirm('还未到预定断食结束时间,\n确定要结束吗?', () {
  363. showRecordConfirm(currentFast!.startTime!,
  364. serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!);
  365. });
  366. } else {
  367. showRecordConfirm(currentFast!.startTime!,
  368. serverTime().millisecondsSinceEpoch ~/ 1000, currentFast!.endTime!);
  369. }
  370. }
  371. void showRecordConfirm(int begin, int end, int target) {
  372. if (hasShowSingleConfirm || Global().currentFast == null) {
  373. //防止重复弹窗打开
  374. // print("lalalal");
  375. return;
  376. }
  377. hasShowSingleConfirm = true;
  378. showDialog(
  379. context: context,
  380. barrierDismissible: false,
  381. barrierColor: Colors.transparent,
  382. useSafeArea: false,
  383. builder: (BuildContext context) {
  384. return Record(
  385. fast: this,
  386. begin: begin,
  387. end: end,
  388. target: target,
  389. );
  390. });
  391. }
  392. void closeRecordConfirm() {
  393. hasShowSingleConfirm = false;
  394. }
  395. void showConfirm(String title, Function() confirmCallback) {
  396. showDialog(
  397. context: context,
  398. barrierDismissible: false,
  399. barrierColor: const Color(0xF2000D1F),
  400. builder: (BuildContext context) {
  401. return AlertWidget(
  402. title: title,
  403. confirm: '确定',
  404. confirmCallback: () {
  405. Navigator.of(context).pop();
  406. confirmCallback();
  407. });
  408. });
  409. }
  410. //单次模式 确认记录
  411. Future confirmRecord(endTimeSeconds, isRightTime, enterTime) async {
  412. Map<String, dynamic> data = await HttpUtils.post(Api.end,
  413. data: {"real_end_time": endTimeSeconds, "real_entry_dt": enterTime});
  414. if (data != null) {}
  415. reset();
  416. Global().mainPage.showMe();
  417. showDialog(
  418. context: context,
  419. barrierDismissible: false,
  420. barrierColor: Colors.transparent,
  421. builder: (BuildContext context) {
  422. return Toast(
  423. title: isRightTime ? '成功记录' : '完成记录',
  424. content: const SizedBox(
  425. width: 0,
  426. height: 0,
  427. ),
  428. );
  429. });
  430. }
  431. Future expandTime(seconds) async {
  432. Map<String, dynamic> data =
  433. await HttpUtils.post(Api.delay, data: {"seconds": seconds});
  434. Global().homePage.getDatas();
  435. String strTime = Util().betweenTimeBySeconds(seconds);
  436. showDialog(
  437. context: context,
  438. barrierDismissible: false,
  439. barrierColor: Colors.transparent,
  440. builder: (BuildContext context) {
  441. return Toast(
  442. title: '记录延时\n$strTime',
  443. content: const SizedBox(
  444. width: 0,
  445. height: 0,
  446. ),
  447. );
  448. });
  449. }
  450. //单次模式 放弃记录
  451. Future giveupRecord() async {
  452. Map<String, dynamic> data = await HttpUtils.post(Api.giveUp, data: {});
  453. if (data != null) {}
  454. reset();
  455. }
  456. void reset() {
  457. setState(() {
  458. status = 0;
  459. currentAngel = 0;
  460. beginTime = DateTime.now();
  461. hiddenTime = false;
  462. beginSecond = '00';
  463. beginCount = '00:00';
  464. });
  465. Global().homePage.getDatas();
  466. }
  467. void expand() {}
  468. void achieve() {
  469. if (status == 1) {
  470. showRecordConfirm(currentFast!.startTime!, currentFast!.endTime!,
  471. currentFast!.endTime!);
  472. } else if (status == 2) {
  473. Global().homePage.showCheckout(false);
  474. }
  475. }
  476. void setBeginTime(now) {
  477. // var now = DateTime.now();
  478. var countMinutes = now.minute + (now.hour - 6) * 60;
  479. var millis = now.millisecondsSinceEpoch;
  480. millis = millis + (targetHour * 60 + targetMinute) * 60 * 1000;
  481. var end = DateTime.fromMillisecondsSinceEpoch(millis);
  482. var angel = countMinutes / (24 * 60) * 2 * pi;
  483. var temp = (end.minute + (end.hour - 6) * 60) / (24 * 60) * 2 * pi;
  484. var left = circleWidth / 2 + Global().progressWidth * cos(temp);
  485. var top = circleWidth / 2 + Global().progressWidth * sin(temp);
  486. setState(() {
  487. beginHour = now.hour.toString();
  488. beginMinutes = now.minute.toString();
  489. endHour = end.hour.toString();
  490. endMinutes = end.minute.toString();
  491. beginAngle = angel;
  492. arcAngle = temp;
  493. endLeft = left - Global().paintWidth / 2.0;
  494. endTop = top - Global().paintWidth / 2.0;
  495. });
  496. }
  497. Future<ui.Image> getAssetImage(String asset, {width, height}) async {
  498. ByteData data = await rootBundle.load(asset);
  499. ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
  500. targetWidth: width, targetHeight: height);
  501. ui.FrameInfo fi = await codec.getNextFrame();
  502. return fi.image;
  503. }
  504. _getLocalImage() async {
  505. ui.Image imageFrame = await getAssetImage('assets/images/seek_start.png',
  506. width: 120, height: 120);
  507. ui.Image imageFrame2 =
  508. await getAssetImage('assets/images/ing.png', width: 120, height: 120);
  509. setState(() {
  510. startImage = imageFrame;
  511. progressImage = imageFrame2;
  512. isLoad = true;
  513. });
  514. }
  515. void updateArc(dx, dy, width) {
  516. if (status != 0) {
  517. return;
  518. }
  519. double x = dx - circleWidth / 2;
  520. double y = dy - circleWidth / 2;
  521. double angel = atan2(y, x);
  522. if (angel < 0) {
  523. angel = 2 * pi + angel;
  524. }
  525. var left = circleWidth / 2 + Global().progressWidth * cos(angel);
  526. var top = circleWidth / 2 + Global().progressWidth * sin(angel);
  527. double endAngel = angel + pi / 2;
  528. if (endAngel >= 2 * pi) {
  529. endAngel -= 2 * pi;
  530. }
  531. int endCount = endAngel * 24 * 60 ~/ (2 * pi);
  532. int endHourTemp = endCount ~/ 60;
  533. int endMinuteTemp = (endCount % 60).toInt();
  534. int nowHour = serverTime().hour;
  535. int nowMinute = serverTime().minute;
  536. if (endHourTemp < nowHour) {
  537. endHourTemp += 24;
  538. } else if (endHourTemp == nowHour && endMinuteTemp < nowMinute) {
  539. endHourTemp += 24;
  540. }
  541. int targetHourTemp, targetMinuteTemp;
  542. if (endMinuteTemp >= nowMinute) {
  543. targetMinuteTemp = endMinuteTemp - nowMinute;
  544. } else {
  545. targetMinuteTemp = 60 + endMinuteTemp - nowMinute;
  546. endHourTemp -= 1;
  547. }
  548. targetHourTemp = endHourTemp - nowHour;
  549. targetMinuteTemp = ((targetMinuteTemp % 60) / 15).floor() * 15;
  550. if (targetHourTemp * 60 + targetMinuteTemp < 60 ||
  551. targetHourTemp * 60 + targetMinuteTemp > 23 * 60) {
  552. return;
  553. }
  554. int leftHourTemp = 24;
  555. int leftMinuteTemp = 0;
  556. if (targetMinuteTemp > 0) {
  557. leftHourTemp--;
  558. leftMinuteTemp = 60 - targetMinuteTemp;
  559. }
  560. leftHourTemp -= targetHourTemp;
  561. if (targetMinute == 0) {
  562. Vibrate.feedback(FeedbackType.success);
  563. }
  564. // if (targetMinute != targetMinuteTemp) {
  565. // Vibrate.feedback(FeedbackType.success);
  566. // }
  567. setState(() {
  568. arcAngle = angel;
  569. endLeft = left - 20.px;
  570. endTop = top - 20.px;
  571. endHour = endHourTemp.toString();
  572. endMinutes = endMinuteTemp.toString();
  573. targetHour = targetHourTemp;
  574. targetMinute = targetMinuteTemp;
  575. leftHour = leftHourTemp;
  576. leftMinute = leftMinuteTemp;
  577. });
  578. }
  579. getTextWidget() {
  580. if (showBeginCount) {
  581. return Column(
  582. mainAxisAlignment: MainAxisAlignment.center,
  583. children: [
  584. Container(
  585. foregroundDecoration: const BoxDecoration(
  586. gradient: LinearGradient(
  587. begin: Alignment.topCenter,
  588. end: Alignment.bottomCenter,
  589. colors: [Color(0xAA000D1F), Color(0x00000D1F)])),
  590. child: Text(
  591. beginCount,
  592. style: TextStyle(
  593. height: 1.0,
  594. fontFamily: 'Fast',
  595. color: kThemeColor,
  596. fontSize: 32.px),
  597. ),
  598. ),
  599. Text(
  600. beginSecond,
  601. style: TextStyle(
  602. color: kThemeColor,
  603. fontFamily: 'Fast',
  604. fontSize: 64.px,
  605. height: 1.0),
  606. ),
  607. SizedBox(
  608. height: 10.px,
  609. )
  610. ],
  611. );
  612. } else if (showSecondCountdown) {
  613. return Container(
  614. margin: EdgeInsets.only(bottom: 20.px),
  615. child: Text(
  616. strSecondCountdown,
  617. style: TextStyle(
  618. color: Colors.white,
  619. fontFamily: 'Exo2',
  620. fontWeight: FontWeight.w900,
  621. fontSize: 96.px),
  622. ),
  623. );
  624. }
  625. return Column(
  626. mainAxisAlignment: MainAxisAlignment.center,
  627. children: [
  628. Text(
  629. strMinuteCountdown,
  630. style: TextStyle(
  631. color: Colors.white,
  632. height: 1.0,
  633. fontFamily: 'Fast',
  634. fontSize: 40.px),
  635. ),
  636. Container(
  637. foregroundDecoration: const BoxDecoration(
  638. gradient: LinearGradient(
  639. begin: Alignment.topCenter,
  640. end: Alignment.bottomCenter,
  641. colors: [Color(0x00000D1F), Color(0xFF000D1F)])),
  642. child: Text(
  643. beginFormateTime,
  644. style: TextStyle(
  645. color: kThemeColor,
  646. fontFamily: 'Fast',
  647. height: 1.0,
  648. fontSize: 28.px),
  649. ),
  650. )
  651. ],
  652. );
  653. }
  654. bubbleWidget(strHour, strMinute, width) {
  655. return Row(
  656. mainAxisSize: MainAxisSize.min,
  657. crossAxisAlignment: CrossAxisAlignment.end,
  658. children: [
  659. Container(
  660. margin: EdgeInsets.only(top: 2.px, left: 0.px),
  661. child: Text(strHour,
  662. style: TextStyle(
  663. color: kThemeColor,
  664. fontFamily: "Exo2",
  665. fontWeight: FontWeight.w600,
  666. fontSize: 32.px)),
  667. ),
  668. Text(strMinute,
  669. style: TextStyle(
  670. color: kThemeColor,
  671. fontFamily: "Exo2",
  672. fontWeight: FontWeight.w600,
  673. fontSize: 32.px))
  674. ],
  675. ).relationOne(
  676. // -40.px + width / 2.0 + 8.px,
  677. -40.px + width / 2.0,
  678. -35.px,
  679. BubbleWidget(
  680. 80.px,
  681. 42.px,
  682. kThemeColor,
  683. BubbleArrowDirection.bottom,
  684. arrAngle: 80.px,
  685. arrHeight: 8.px,
  686. child: Text(
  687. '断食中…',
  688. style: TextStyle(
  689. color: Colors.black,
  690. fontSize: 14.px,
  691. fontWeight: FontWeight.bold),
  692. ),
  693. ));
  694. }
  695. getTimeStatusWidget() {
  696. if (hiddenTime) {
  697. return Container(
  698. alignment: Alignment.center,
  699. child: RRectButton(
  700. status == 1 ? '结束断食' : '结束挑战',
  701. width: 144.px,
  702. height: 50.px,
  703. backgroundColor: const Color(0x1AC4CCDA),
  704. textColor: const Color(0xFFC4CCDA),
  705. radius: 25.px,
  706. fontSize: 16.px,
  707. onPressed: () => end(),
  708. ));
  709. }
  710. String strHour =
  711. targetMinute == 0 ? targetHour.toString() : targetHour.toString() + ':';
  712. String strMinute =
  713. targetMinute == 0 ? '' : targetMinute.toString().padLeft(2, '0');
  714. double width = Util()
  715. .boundingTextSize(
  716. strHour,
  717. TextStyle(
  718. fontFamily: 'Exo2',
  719. fontWeight: FontWeight.w600,
  720. fontSize: 32.px))
  721. .width +
  722. Util()
  723. .boundingTextSize(
  724. strMinute,
  725. TextStyle(
  726. fontFamily: 'Exo2',
  727. fontWeight: FontWeight.w600,
  728. fontSize: 32.px))
  729. .width;
  730. return Container(
  731. height: 50.px,
  732. alignment: Alignment.center,
  733. child: Row(
  734. children: [
  735. Expanded(
  736. child:
  737. Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
  738. Expanded(
  739. child: SizedBox(
  740. width: 0.px,
  741. )),
  742. showTips
  743. ? const SizedBox(
  744. width: 0,
  745. height: 0,
  746. ).relationOne(
  747. 80.px - width, 0, bubbleWidget(strHour, strMinute, width))
  748. : Row(
  749. mainAxisSize: MainAxisSize.min,
  750. crossAxisAlignment: CrossAxisAlignment.end,
  751. children: [
  752. Text(strHour,
  753. style: TextStyle(
  754. color: kThemeColor,
  755. fontFamily: "Exo2",
  756. fontWeight: FontWeight.w600,
  757. fontSize: 32.px)),
  758. Text(strMinute,
  759. style: TextStyle(
  760. color: kThemeColor,
  761. fontFamily: "Exo2",
  762. fontWeight: FontWeight.w600,
  763. fontSize: 32.px))
  764. ],
  765. )
  766. ])),
  767. SizedBox(
  768. width: 10.px,
  769. ),
  770. Container(
  771. margin: EdgeInsets.only(
  772. top: 4.px,
  773. ),
  774. child: Image.asset(
  775. 'assets/images/seperate1.png',
  776. width: 8.px,
  777. height: 16.px,
  778. ),
  779. ),
  780. SizedBox(
  781. width: 10.px,
  782. ),
  783. Expanded(
  784. child: Row(
  785. mainAxisSize: MainAxisSize.min,
  786. crossAxisAlignment: CrossAxisAlignment.end,
  787. children: [
  788. Text(
  789. leftMinute == 0
  790. ? leftHour.toString()
  791. : leftHour.toString() + ':',
  792. style: TextStyle(
  793. color: Colors.white,
  794. fontFamily: "Exo2",
  795. fontSize: 32.px)),
  796. Text(
  797. leftMinute == 0
  798. ? ''
  799. : leftMinute.toString().padLeft(2, '0'),
  800. style: TextStyle(
  801. color: Colors.white,
  802. fontFamily: "Exo2",
  803. fontSize: 32.px),
  804. )
  805. ])),
  806. ],
  807. ));
  808. }
  809. @override
  810. Widget build(BuildContext context) {
  811. final size = MediaQuery.of(context).size;
  812. final width = size.width;
  813. var step = 0.171;
  814. var arcTemp = 0.0;
  815. // beginAngle = -pi/2.0;
  816. // arcAngle = pi+pi*2/3.0;
  817. if (beginAngle > pi) {
  818. beginAngle = beginAngle - 2 * pi;
  819. }
  820. if (arcAngle - beginAngle > 2 * pi) {
  821. arcAngle = arcAngle - 2 * pi;
  822. }
  823. if (beginAngle > 0 && arcAngle < 0) {
  824. arcAngle = arcAngle + 2 * pi;
  825. }
  826. if (beginAngle > arcAngle) {
  827. arcAngle = arcAngle + 2 * pi;
  828. }
  829. // if (beginAngle > pi) {
  830. // arcTemp = (beginAngle - 2 * pi) * (1 - animation3.value) +
  831. // arcAngle * animation3.value;
  832. // } else {
  833. // arcTemp =
  834. // beginAngle * (1 - animation3.value) + arcAngle * animation3.value;
  835. // }
  836. arcTemp = beginAngle * (1 - animation3.value) + arcAngle * animation3.value;
  837. return Stack(children: [
  838. // SizedBox(width: 375.px,),
  839. if (status == 2)
  840. Positioned(
  841. right: 14.px,
  842. top: 0.px,
  843. child: Opacity(
  844. opacity: animation4.value,
  845. child: Container(
  846. width: 64.px,
  847. height: 54.px,
  848. padding: EdgeInsets.all(8.px),
  849. decoration: BoxDecoration(
  850. color: const Color(0x1AC4CCDA),
  851. borderRadius: BorderRadius.all(Radius.circular(12.px)),
  852. ),
  853. child: Column(
  854. children: [
  855. Text(
  856. '${currentFast!.days}天挑战',
  857. style: TextStyle(
  858. color: const Color(0x66FFFFFF),
  859. fontSize: 10.px,
  860. height: 1.0),
  861. ),
  862. Container(
  863. height: 1.px,
  864. color: const Color(0x1AFFFFFF),
  865. margin: EdgeInsets.only(top: 5.px, bottom: 5.px),
  866. ),
  867. Row(
  868. mainAxisAlignment: MainAxisAlignment.center,
  869. children: [
  870. Text(
  871. '${currentFast!.finishDays}',
  872. style: TextStyle(
  873. color: const Color(0xFFC4CCDA),
  874. fontFamily: 'Exo2',
  875. fontWeight: FontWeight.w600,
  876. fontSize: 16.px,
  877. height: 1.0),
  878. ),
  879. SizedBox(
  880. width: 2.px,
  881. ),
  882. Image.asset(
  883. 'assets/images/seperate1.png',
  884. width: 4.px,
  885. height: 8.px,
  886. ),
  887. SizedBox(
  888. width: 2.px,
  889. ),
  890. Text(
  891. '${currentFast!.days}',
  892. style: TextStyle(
  893. color: const Color(0xFFC4CCDA),
  894. fontFamily: 'Exo2',
  895. fontWeight: FontWeight.w600,
  896. fontSize: 16.px,
  897. height: 1.0),
  898. ),
  899. ],
  900. )
  901. ],
  902. ),
  903. ))),
  904. SingleChildScrollView(
  905. physics: const NeverScrollableScrollPhysics(),
  906. child: Column(
  907. mainAxisAlignment: MainAxisAlignment.start,
  908. children: [
  909. Stack(
  910. children: [
  911. Opacity(
  912. opacity: animation0.value,
  913. child: Transform.scale(
  914. scale: animation0.value,
  915. child: Container(
  916. width: circleWidth,
  917. height: circleWidth,
  918. margin: EdgeInsets.fromLTRB(0, 0, 0, 16.px),
  919. decoration: const BoxDecoration(
  920. image: DecorationImage(
  921. alignment: Alignment.topLeft,
  922. image:
  923. AssetImage('assets/images/seek_bg.png'))),
  924. child: Opacity(
  925. opacity: animation2.value,
  926. child: CustomPaint(
  927. foregroundPainter: TargetPainter(
  928. lineColor: Colors.yellow,
  929. completeColor: Colors
  930. .orange, //[Colors.orange,Colors.blue],
  931. // arcAngle: arcAngle*animation3.value,
  932. arcAngle: arcTemp,
  933. beginAngle: beginAngle,
  934. width: paintWidth,
  935. localImage: isLoad ? startImage : null),
  936. ),
  937. ),
  938. ),
  939. ),
  940. ),
  941. // Positioned(child: Container(width: 40,height: 40,color: Colors.red,)),
  942. Opacity(
  943. opacity: animation3.value,
  944. child: Container(
  945. width: paintWidth,
  946. height: paintWidth,
  947. decoration: BoxDecoration(
  948. borderRadius:
  949. BorderRadius.all(Radius.circular(20.px)),
  950. color: const Color(0xFFC4CCDA)),
  951. margin: EdgeInsets.only(left: endLeft, top: endTop),
  952. ),
  953. ),
  954. if (status != 0)
  955. Transform.rotate(
  956. angle: beginAngle - step,
  957. child: SizedBox(
  958. width: circleWidth,
  959. height: circleWidth,
  960. child: CustomPaint(
  961. foregroundPainter: ProgressPainter(
  962. lineColor: Colors.yellow,
  963. completeColor:
  964. Colors.orange, //[Colors.orange,Colors.blue],
  965. arcAngle: currentAngel * animation3.value,
  966. beginAngle: beginAngle,
  967. width: paintWidth,
  968. localImage: isLoad ? progressImage : null),
  969. ),
  970. ),
  971. ),
  972. if (status != 0) ingImg(beginAngle, currentAngel),
  973. // Opacity(
  974. // opacity: animation4.value,
  975. // child: ingImg(beginAngle, currentAngel),
  976. // ),
  977. Opacity(
  978. opacity: animation4.value,
  979. child: Container(
  980. alignment: Alignment.center,
  981. child: status == 0
  982. ? TextButton(
  983. onPressed: () => {start()},
  984. child: Image(
  985. image:
  986. const AssetImage('assets/images/begin.png'),
  987. width: Global().paintWidth * 2,
  988. height: Global().paintWidth * 2,
  989. ),
  990. )
  991. : getTextWidget(),
  992. width: circleWidth,
  993. height: circleWidth,
  994. ),
  995. ),
  996. startImg(beginAngle),
  997. Opacity(
  998. opacity: animation3.value,
  999. child: GestureDetector(
  1000. onTapDown: (TapDownDetails details) {
  1001. updateArc(details.localPosition.dx,
  1002. details.localPosition.dy, width);
  1003. },
  1004. onLongPressMoveUpdate: (details) {
  1005. updateArc(details.localPosition.dx,
  1006. details.localPosition.dy, width);
  1007. },
  1008. onHorizontalDragUpdate: (details) {
  1009. updateArc(details.localPosition.dx,
  1010. details.localPosition.dy, width);
  1011. },
  1012. onVerticalDragUpdate: (details) {
  1013. updateArc(details.localPosition.dx,
  1014. details.localPosition.dy, width);
  1015. },
  1016. child: Container(
  1017. margin: EdgeInsets.fromLTRB(endLeft, endTop, 0, 0),
  1018. child: Image(
  1019. image: const AssetImage('assets/images/seek_end.png'),
  1020. width: paintWidth,
  1021. height: paintWidth,
  1022. ),
  1023. ),
  1024. ),
  1025. ),
  1026. ],
  1027. ),
  1028. Opacity(
  1029. opacity: animation2.value,
  1030. child: getTimeStatusWidget(),
  1031. )
  1032. ],
  1033. ))
  1034. ]);
  1035. }
  1036. ingImg(beginAngle, currentAngel) {
  1037. if (currentAngel == 0) {
  1038. return const SizedBox(
  1039. width: 0.0,
  1040. height: 0.0,
  1041. );
  1042. }
  1043. var step = 0.171;
  1044. var maxEnd = beginAngle + currentAngel;
  1045. if (maxEnd > 2 * pi - step) {
  1046. maxEnd = 2 * pi - 2 * step;
  1047. }
  1048. return Positioned(
  1049. left: circleWidth / 2.0 +
  1050. Global().progressWidth * cos(beginAngle + currentAngel) -
  1051. paintWidth / 2.0,
  1052. top: circleWidth / 2.0 +
  1053. Global().progressWidth * sin(beginAngle + currentAngel) -
  1054. paintWidth / 2.0,
  1055. child: Transform.rotate(
  1056. angle: beginAngle + currentAngel - pi / 2.0,
  1057. child: Opacity(
  1058. opacity: animation4.value,
  1059. child: Container(
  1060. color: Colors.transparent,
  1061. width: paintWidth,
  1062. height: paintWidth,
  1063. child: FrameAnimationImage(assetList: const [
  1064. 'assets/images/a_00000.png',
  1065. 'assets/images/a_00001.png',
  1066. 'assets/images/a_00002.png',
  1067. 'assets/images/a_00003.png',
  1068. 'assets/images/a_00004.png',
  1069. 'assets/images/a_00005.png',
  1070. 'assets/images/a_00006.png',
  1071. 'assets/images/a_00007.png',
  1072. 'assets/images/a_00008.png',
  1073. 'assets/images/a_00009.png',
  1074. 'assets/images/a_00010.png',
  1075. 'assets/images/a_00011.png',
  1076. 'assets/images/a_00012.png',
  1077. 'assets/images/a_00013.png',
  1078. 'assets/images/a_00014.png',
  1079. 'assets/images/a_00015.png',
  1080. ], width: paintWidth, height: paintWidth, interval: 40),
  1081. ),
  1082. ),
  1083. ));
  1084. }
  1085. startImg(beginAngle) {
  1086. var step = 0.171;
  1087. var maxEnd = beginAngle;
  1088. if (maxEnd > 2 * pi - step) {
  1089. maxEnd = 2 * pi - 2 * step;
  1090. }
  1091. return Positioned(
  1092. left: circleWidth / 2.0 +
  1093. Global().progressWidth * cos(beginAngle) -
  1094. paintWidth / 2.0,
  1095. top: circleWidth / 2.0 +
  1096. Global().progressWidth * sin(beginAngle) -
  1097. paintWidth / 2.0,
  1098. child: Opacity(
  1099. opacity: animation4.value,
  1100. child: Container(
  1101. color: Colors.transparent,
  1102. width: paintWidth,
  1103. height: paintWidth,
  1104. child: Image.asset(
  1105. 'assets/images/seek_start.png',
  1106. width: paintWidth,
  1107. height: paintWidth,
  1108. ),
  1109. ),
  1110. ),
  1111. );
  1112. }
  1113. }
  1114. // ignore: must_be_immutable
  1115. class TextGradientColorWidget extends StatefulWidget {
  1116. String data;
  1117. TextStyle? style;
  1118. List<Color> colors;
  1119. TextGradientColorWidget({Key? key, required this.data, required this.colors})
  1120. : super(key: key);
  1121. @override
  1122. _TextGradientColorState createState() => _TextGradientColorState();
  1123. }
  1124. class _TextGradientColorState extends State<TextGradientColorWidget> {
  1125. WidgetsBinding? widgetsBinding = WidgetsBinding.instance;
  1126. TextStyle? textStyle;
  1127. @override
  1128. void initState() {
  1129. // TODO: implement initState
  1130. super.initState();
  1131. widgetsBinding!.addPostFrameCallback((timeStamp) {
  1132. final RenderBox? box = context.findRenderObject() as RenderBox;
  1133. var left = 0.0;
  1134. var width = 0.0;
  1135. if (box != null) {
  1136. final topLeftPosition = box.localToGlobal(Offset.zero);
  1137. final size = box.size;
  1138. left = topLeftPosition.dx;
  1139. width = size.width;
  1140. setState(() {
  1141. textStyle = TextStyle(
  1142. fontSize: 30.px,
  1143. fontFamily: 'Fast',
  1144. foreground: Paint()
  1145. ..shader = LinearGradient(
  1146. begin: Alignment.topCenter,
  1147. end: Alignment.bottomCenter,
  1148. colors: widget.colors,
  1149. ).createShader(Rect.fromLTWH(left - 20,
  1150. topLeftPosition.dy - 260, width, size.height + 20)));
  1151. });
  1152. //注意 TextStyle中color和foreground 中colors不能同时设置
  1153. }
  1154. });
  1155. }
  1156. @override
  1157. Widget build(BuildContext context) {
  1158. // TODO: implement build
  1159. return Text(widget.data, style: textStyle);
  1160. }
  1161. }