consume_page.dart 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. import 'dart:math';
  2. import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' as extended;
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
  5. import 'package:sport/application.dart';
  6. import 'package:sport/bean/share_info.dart';
  7. import 'package:sport/bean/sport_detail.dart';
  8. import 'package:sport/bean/sport_index.dart';
  9. import 'package:sport/pages/home/step_page.dart';
  10. import 'package:sport/pages/social/share_webview.dart';
  11. import 'package:sport/router/navigator_util.dart';
  12. import 'package:sport/services/api/inject_api.dart';
  13. import 'package:sport/services/api/resp.dart';
  14. import 'package:sport/utils/date.dart';
  15. import 'package:sport/utils/toast.dart';
  16. import 'package:sport/widgets/appbar.dart';
  17. import 'package:sport/widgets/chart.dart';
  18. import 'package:sport/widgets/decoration.dart';
  19. import 'package:sport/widgets/dialog/request_dialog.dart';
  20. import 'package:sport/widgets/image.dart';
  21. import 'package:sport/widgets/loading.dart';
  22. import 'package:sport/widgets/misc.dart';
  23. import 'package:sport/widgets/persistent_header.dart';
  24. class ConsumePage extends StatefulWidget {
  25. @override
  26. State<StatefulWidget> createState() => _PageState();
  27. }
  28. class _PageState extends State<ConsumePage> with InjectApi {
  29. Color _color = Color(0xffFFC400);
  30. ValueNotifier<String> _tab = ValueNotifier<String>("日");
  31. ValueNotifier<SportDetail?> _valueNotifierSportDetail = ValueNotifier(null);
  32. ValueNotifier<DateTime> _valueNotifierDate = ValueNotifier(DateTime.now());
  33. ValueNotifier<DateTime> _valueNotifierNow = ValueNotifier(DateTime.now());
  34. late PageController _pageController;
  35. late ScrollController _scrollController;
  36. @override
  37. void initState() {
  38. super.initState();
  39. _pageController = PageController(initialPage: 0);
  40. _scrollController = ScrollController();
  41. changeTab();
  42. }
  43. @override
  44. void dispose() {
  45. _pageController.dispose();
  46. super.dispose();
  47. _tab.dispose();
  48. _valueNotifierSportDetail.dispose();
  49. _valueNotifierDate.dispose();
  50. _valueNotifierNow.dispose();
  51. _scrollController.dispose();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. final double tabHeader = 90.0;
  56. final double statusBarHeight = MediaQuery.of(context).padding.top;
  57. final double pinnedHeaderHeight = tabHeader;
  58. final double headerHeight = 240.0;
  59. return Scaffold(
  60. backgroundColor: Colors.white,
  61. body: SafeArea(
  62. child: Stack(
  63. children: <Widget>[
  64. extended.ExtendedNestedScrollView(
  65. controller: _scrollController,
  66. pinnedHeaderSliverHeightBuilder: () {
  67. return pinnedHeaderHeight;
  68. },
  69. headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
  70. return <Widget>[
  71. SliverToBoxAdapter(
  72. child: Container(
  73. width: 240.0,
  74. height: headerHeight,
  75. padding: EdgeInsets.only(top: 16.0),
  76. child: Align(
  77. alignment: Alignment.center,
  78. child: CustomPaint(
  79. painter: _Bg(),
  80. child: Container(
  81. width: 180.0,
  82. height: 180.0,
  83. child: Center(
  84. child: Column(
  85. children: <Widget>[
  86. Text("消耗卡路里", style: Theme.of(context).textTheme.subtitle1!),
  87. SizedBox(
  88. height: 26.0,
  89. ),
  90. Row(
  91. children: <Widget>[
  92. Text(" ", style: Theme.of(context).textTheme.subtitle2),
  93. ValueListenableBuilder(
  94. builder: (BuildContext context, value, Widget? child) => FutureBuilder(
  95. future: createFutureType(0, _valueNotifierNow.value),
  96. builder: (BuildContext context, AsyncSnapshot<SportDetail?> snapshot) => Text(
  97. "${snapshot.data?.recordsTodaySum?.consume ?? 0}",
  98. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 40.0, fontFamily: "DIN"),
  99. strutStyle: fixedLine,
  100. ),
  101. ),
  102. valueListenable: _valueNotifierNow,
  103. ),
  104. Text(" 大卡", style: Theme.of(context).textTheme.subtitle2),
  105. ],
  106. mainAxisSize: MainAxisSize.min,
  107. crossAxisAlignment: CrossAxisAlignment.end,
  108. ),
  109. SizedBox(
  110. height: 8,
  111. ),
  112. GestureDetector(
  113. onTap: () async {
  114. var result = await showDatePicker(
  115. context: context,
  116. initialDate: _valueNotifierNow.value,
  117. lastDate: DateTime.now(),
  118. firstDate: DateTime(2020),
  119. );
  120. if (result != null) {
  121. var diff = DateTime.now().difference(result);
  122. _valueNotifierDate.value = result;
  123. _valueNotifierNow.value = result;
  124. int type = toType();
  125. // if (type == 0) {
  126. // _pageController.jumpToPage(diff.inDays);
  127. // } else {
  128. // _pageController = PageController(initialPage: diff.inDays);
  129. // }
  130. _tab.value = TABS.first;
  131. _pageController.jumpToPage(diff.inDays);
  132. print("$type -- ${diff.inDays}");
  133. }
  134. },
  135. child: Row(
  136. children: <Widget>[
  137. ValueListenableBuilder(
  138. valueListenable: _valueNotifierNow,
  139. builder: (BuildContext context, DateTime value, Widget? child) => Text("${value.month}.${value.day}", style: Theme.of(context).textTheme.subtitle1!),
  140. ),
  141. SizedBox(
  142. width: 6.0,
  143. ),
  144. Image.asset("lib/assets/img/setgoals_icon_date.png"),
  145. ],
  146. mainAxisSize: MainAxisSize.min,
  147. ),
  148. behavior: HitTestBehavior.opaque,
  149. )
  150. ],
  151. mainAxisSize: MainAxisSize.min,
  152. ),
  153. ),
  154. ),
  155. ),
  156. ),
  157. ),
  158. ),
  159. SliverPersistentHeader(
  160. pinned: true,
  161. delegate: PersistentHeader(
  162. min: pinnedHeaderHeight,
  163. max: pinnedHeaderHeight,
  164. child: Container(
  165. color: Colors.white,
  166. child: ValueListenableBuilder(
  167. valueListenable: _tab,
  168. builder: (BuildContext context, String value, Widget? child) {
  169. return Column(
  170. children: <Widget>[
  171. Padding(
  172. padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
  173. child: Row(
  174. mainAxisAlignment: MainAxisAlignment.center,
  175. children: ["日", "/", "周", "/", "月", "/", "年"]
  176. .map((e) => e == "/"
  177. ? Container(
  178. margin: const EdgeInsets.fromLTRB(5, 0, 1, 0),
  179. color: const Color(0xffdcdcdc),
  180. width: 0.5,
  181. height: 14,
  182. transform: Matrix4.rotationZ(0.35),
  183. )
  184. : InkWell(
  185. onTap: () {
  186. if (_valueNotifierSportDetail.value == null) return;
  187. _scrollController.animateTo(headerHeight, duration: Duration(milliseconds: 500), curve: Curves.linear);
  188. _tab.value = e;
  189. _valueNotifierDate.value = DateTime.now();
  190. _pageController.jumpToPage(0);
  191. // _pageController= PageController(initialPage: 0);
  192. changeTab();
  193. },
  194. child: Container(
  195. margin: EdgeInsets.symmetric(horizontal: 12.0),
  196. padding: EdgeInsets.all(8.0),
  197. decoration: value == e ? BoxDecoration(color: _color, shape: BoxShape.circle) : null,
  198. child: Text(
  199. "$e",
  200. style: value == e ? Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white) : Theme.of(context).textTheme.subtitle1!,
  201. ),
  202. )))
  203. .toList(),
  204. ),
  205. ),
  206. const SizedBox(
  207. height: 10.0,
  208. ),
  209. Center(
  210. child: ValueListenableBuilder<DateTime>(
  211. valueListenable: _valueNotifierDate,
  212. builder: (_, time, ___) {
  213. int type = toType();
  214. String text = "";
  215. if (type == 0) {
  216. text = "${time.year}.${'${time.month}'.padLeft(2, '0')}.${'${time.day}'.padLeft(2, '0')} 6:00 - 24:00 ";
  217. } else if (type == 1) {
  218. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  219. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  220. print("$time ${time.weekday} == $start $end");
  221. text = "${start.year}.${'${start.month}'.padLeft(2, '0')}.${'${start.day}'.padLeft(2, '0')} ~ ${end.year}.${'${end.month}'.padLeft(2, '0')}.${'${end.day}'.padLeft(2, '0')}";
  222. } else if (type == 2) {
  223. text = ("${time.year}年${'${time.month}'.padLeft(2, '0')}月");
  224. } else if (type == 3) {
  225. text = ("${time.year}年");
  226. }
  227. return Row(
  228. mainAxisSize: MainAxisSize.min,
  229. children: <Widget>[
  230. GestureDetector(
  231. behavior: HitTestBehavior.opaque,
  232. onTap: () {
  233. _pageController.nextPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
  234. },
  235. child: Padding(
  236. padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 10.0),
  237. child: arrowLeft(),
  238. ),
  239. ),
  240. Text(
  241. text,
  242. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Color(0xff333333)),
  243. strutStyle: fixedLine,
  244. ),
  245. GestureDetector(
  246. behavior: HitTestBehavior.opaque,
  247. onTap: () {
  248. if (_pageController.page == 0.0) {
  249. ToastUtil.show("没有数据了");
  250. return;
  251. }
  252. _pageController.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
  253. },
  254. child: Padding(
  255. padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 10.0),
  256. child: arrowRight(),
  257. ),
  258. ),
  259. ],
  260. );
  261. }),
  262. ),
  263. ],
  264. );
  265. },
  266. ),
  267. ),
  268. ),
  269. ),
  270. ];
  271. },
  272. body: ValueListenableBuilder(
  273. valueListenable: _tab,
  274. builder: (BuildContext context, String value, Widget? child) => PageView.builder(
  275. reverse: true,
  276. itemCount: 10240,
  277. controller: _pageController,
  278. onPageChanged: (page) {
  279. rollDate(-page);
  280. },
  281. itemBuilder: (context, index) {
  282. int type = toType();
  283. DateTime time = offsetDate(type, -index);
  284. // print("$index $type --2222 ${time}");
  285. return FutureBuilder<SportDetail?>(
  286. key: PageStorageKey<String>('Tab$index'),
  287. future: createFuture(time),
  288. builder: (BuildContext context, AsyncSnapshot<SportDetail?> snapshot) {
  289. var _value = snapshot.data;
  290. if (_value == null) return Container();
  291. var _items = _createItems(type, _value.recordsTodaySum!);
  292. return snapshot.connectionState != ConnectionState.done
  293. ? RequestLoadingWidget()
  294. : SingleChildScrollView(
  295. child: Column(
  296. children: <Widget>[
  297. SizedBox(
  298. height: 30.0,
  299. ),
  300. CustomPaint(
  301. painter: Chart(type: TABS.indexOf(_tab.value), records: (_value.recordsToday ?? []).map((e) => ChartItem(type == 3 ? "${e.month}" : e.createdAt ?? "", e.consume)).toList(), dateTime: time, drawMax: true, unit: "大卡")..initData(maxValue: 3500.0 * (type + 1), valueSplit: 500),
  302. child: Container(
  303. height: 200,
  304. ),
  305. ),
  306. Padding(
  307. padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
  308. child: StaggeredGrid.extent(
  309. maxCrossAxisExtent: (MediaQuery.of(context).size.width - 32.0) / 2,
  310. mainAxisSpacing: 12.0,
  311. crossAxisSpacing: 12.0,
  312. children: _items
  313. .map((e) => Container(
  314. decoration: card(),
  315. padding: const EdgeInsets.fromLTRB(20.0, 20.0, 0, 20.0),
  316. child: Row(
  317. children: <Widget>[
  318. Image.asset(
  319. e.icon,
  320. width: 36.0,
  321. ),
  322. const SizedBox(
  323. width: 10.0,
  324. ),
  325. Column(
  326. crossAxisAlignment: CrossAxisAlignment.start,
  327. children: <Widget>[
  328. Row(
  329. crossAxisAlignment: CrossAxisAlignment.end,
  330. children: <Widget>[
  331. Text(
  332. e.title,
  333. style: e.unit != "" ? Theme.of(context).textTheme.headline1!.copyWith(fontSize: 20.0) : Theme.of(context).textTheme.headline1!.copyWith(fontSize: 16.0),
  334. strutStyle: fixedLine,
  335. ),
  336. Text(" ${e.unit}", style: Theme.of(context).textTheme.subtitle2),
  337. ],
  338. ),
  339. const SizedBox(
  340. width: 4.0,
  341. ),
  342. Text(e.subtitle, style: Theme.of(context).textTheme.bodyText1!)
  343. ],
  344. )
  345. ],
  346. ),
  347. ))
  348. .toList(),
  349. ),
  350. ),
  351. if (type != 0 && _value.recordsTodayAvg != null)
  352. Padding(
  353. padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 20.0),
  354. child: Column(
  355. crossAxisAlignment: CrossAxisAlignment.start,
  356. children: <Widget>[
  357. Text(
  358. "日均数据",
  359. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 16.0),
  360. ),
  361. SizedBox(
  362. height: 16.0,
  363. ),
  364. Container(
  365. padding: const EdgeInsets.fromLTRB(14.0, 21.0, 14.0, 21.0),
  366. decoration: card(),
  367. child: Column(
  368. children: <Widget>[
  369. Row(
  370. children: <Widget>[
  371. Image.asset(
  372. "lib/assets/img/day_icon_duration.png",
  373. width: 19.0,
  374. ),
  375. const SizedBox(
  376. width: 8.0,
  377. ),
  378. Expanded(
  379. child: Text(
  380. "日均时长",
  381. style: Theme.of(context).textTheme.subtitle1!,
  382. ),
  383. ),
  384. SizedBox(
  385. width: 60.0,
  386. child: Text(
  387. "${(_value.recordsTodayAvg?.duration ?? 0) ~/ 60}分钟",
  388. style: Theme.of(context).textTheme.subtitle1!,
  389. ),
  390. ),
  391. ],
  392. ),
  393. const SizedBox(
  394. height: 20.0,
  395. ),
  396. Row(
  397. children: <Widget>[
  398. Image.asset(
  399. "lib/assets/img/day_icon_consume.png",
  400. width: 19.0,
  401. ),
  402. const SizedBox(
  403. width: 8.0,
  404. ),
  405. Expanded(
  406. child: Text(
  407. "日均消耗",
  408. style: Theme.of(context).textTheme.subtitle1!,
  409. ),
  410. ),
  411. SizedBox(
  412. width: 60.0,
  413. child: Text(
  414. "${_value.recordsTodayAvg?.consume ?? 0}大卡",
  415. style: Theme.of(context).textTheme.subtitle1!,
  416. ),
  417. ),
  418. ],
  419. ),
  420. const SizedBox(
  421. height: 20.0,
  422. ),
  423. Row(
  424. children: <Widget>[
  425. Image.asset(
  426. "lib/assets/img/day_icon_frequency.png",
  427. width: 19.0,
  428. ),
  429. const SizedBox(
  430. width: 8.0,
  431. ),
  432. Expanded(
  433. child: Text(
  434. "日均运动次数",
  435. style: Theme.of(context).textTheme.subtitle1!,
  436. ),
  437. ),
  438. SizedBox(
  439. width: 60.0,
  440. child: Text(
  441. "${_value.recordsTodayAvg?.times ?? 0}次",
  442. style: Theme.of(context).textTheme.subtitle1!,
  443. ),
  444. ),
  445. ],
  446. ),
  447. const SizedBox(
  448. height: 20.0,
  449. ),
  450. Row(
  451. children: <Widget>[
  452. Image.asset(
  453. "lib/assets/img/day_icon_steps.png",
  454. width: 19.0,
  455. ),
  456. const SizedBox(
  457. width: 8.0,
  458. ),
  459. Expanded(
  460. child: Text(
  461. "日均运动步数",
  462. style: Theme.of(context).textTheme.subtitle1!,
  463. ),
  464. ),
  465. SizedBox(
  466. width: 60.0,
  467. child: Text(
  468. "${_value.recordsTodayAvg?.step ?? 0}",
  469. style: Theme.of(context).textTheme.subtitle1!,
  470. ),
  471. ),
  472. ],
  473. )
  474. ],
  475. ),
  476. )
  477. ],
  478. ),
  479. )
  480. ],
  481. ),
  482. );
  483. });
  484. },
  485. ),
  486. ),
  487. ),
  488. Positioned(
  489. child: buildBackButton(context),
  490. ),
  491. Positioned(
  492. right: 0,
  493. child: IconButton(
  494. icon: Image.asset("lib/assets/img/bbs_icon_share.png"),
  495. onPressed: () async {
  496. String? hash;
  497. print("------------------------------------------------------------");
  498. await request(context, () async {
  499. ShareInfo? _info = (await api.getshareCreateSport("day", 50.0)).data;
  500. hash = _info?.hash;
  501. });
  502. if (hash != null) {
  503. NavigatorUtil.goPage(
  504. context,
  505. (context) => WebViewSharePage(
  506. shareUrl,
  507. hash: hash,
  508. openShare: true,
  509. ));
  510. }
  511. },
  512. ),
  513. ),
  514. ],
  515. ),
  516. ),
  517. );
  518. }
  519. int toType() {
  520. return TABS.indexOf(_tab.value);
  521. }
  522. void changeTab() async {
  523. _valueNotifierSportDetail.value = null;
  524. if (_valueNotifierDate.value == null) return;
  525. Future<SportDetail?> data = createFuture(_valueNotifierDate.value);
  526. if (data != null) {
  527. data.then((value) => _valueNotifierSportDetail.value = value);
  528. }
  529. }
  530. Future<SportDetail?> createQueryFuture(int offset) {
  531. int type = toType();
  532. DateTime next = offsetDate(type, offset);
  533. return createFuture(next);
  534. }
  535. Future<SportDetail?> createFuture(DateTime time) async {
  536. int type = toType();
  537. return createFutureType(type, time);
  538. }
  539. Future<SportDetail?> createFutureType(int type, DateTime time) async {
  540. Future<RespData<SportDetailSimple>>? data;
  541. switch (type) {
  542. case 0:
  543. data = api.getSportRecordListOneDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}');
  544. break;
  545. case 1:
  546. DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
  547. DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
  548. data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  549. break;
  550. case 2:
  551. DateTime start = DateTime(time.year, time.month, 1);
  552. DateTime end = DateTime(time.year, time.month + 1, 0);
  553. data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}', '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  554. break;
  555. case 3:
  556. data = api.getSportRecordListByMonth(time.year);
  557. break;
  558. }
  559. if (data != null) {
  560. var simple = await data;
  561. if (simple.code == 0) {
  562. return SportDetail(recordsTodaySum: simple.data?.sum ?? RecordsTodaySum(consume: 0, duration: 0, crouch: 0, jump: 0), recordsTodayAvg: simple.data?.avg ?? RecordsTodaySum(consume: 0, duration: 0, crouch: 0, jump: 0, times: 0, step: 0), recordsToday: simple.data?.records);
  563. }
  564. }
  565. return null;
  566. }
  567. void rollDate(int offset) {
  568. if (_valueNotifierSportDetail.value == null) return;
  569. int type = toType();
  570. DateTime next = offsetDate(type, offset);
  571. _valueNotifierDate.value = next;
  572. // changeTab();
  573. }
  574. List<DataItem> _createItems(int type, RecordsTodaySum sum) {
  575. var lable = "";
  576. if (type == 1) {
  577. lable = "周";
  578. } else if (type == 2) {
  579. lable = "月";
  580. } else if (type == 3) {
  581. lable = "年";
  582. }
  583. return [
  584. DataItem("lib/assets/img/data_icon_consume.png", "${sum.consume}", "大卡", "$lable总消耗"),
  585. DataItem("lib/assets/img/data_icon_duration.png", "${(sum.duration) ~/ 60}", "分钟", "$lable总时长"),
  586. DataItem("lib/assets/img/data_icon_frequency.png", "${sum.times }", "次", "$lable运动次数"),
  587. DataItem("lib/assets/img/data_icon_steps.png", "${sum.step}", "歩", "$lable运动步数"),
  588. DataItem("lib/assets/img/data_icon_squat.png", "${sum.crouchRate.toStringAsFixed(0)}", "次/分钟", "下蹲频率"),
  589. DataItem("lib/assets/img/data_icon_jump.png", "${sum.jumpRate.toStringAsFixed(0)}", "次/分钟", "跳跃频率"),
  590. if (type == 0) DataItem("lib/assets/img/data_icon_strength.png", "${strengthToLabel(sum.consume, sum.duration)}", "", "强度评级")
  591. ];
  592. }
  593. }
  594. class DataItem {
  595. final String icon, title, unit, subtitle;
  596. DataItem(this.icon, this.title, this.unit, this.subtitle);
  597. }
  598. class _Bg extends CustomPainter {
  599. final Paint _paint = Paint()..isAntiAlias = true;
  600. Color _color = const Color(0xffFFD736);
  601. @override
  602. void paint(Canvas canvas, Size size) {
  603. final Offset center = Offset(size.width / 2, size.height / 2);
  604. double radius = size.width / 2;
  605. // print("$size $center $radius");
  606. _paint.color = _color.withOpacity(.2);
  607. canvas.drawCircle(center, radius, _paint);
  608. _paint.color = _color.withOpacity(.5);
  609. canvas.drawCircle(center, radius - 10, _paint);
  610. _paint.color = _color;
  611. canvas.drawCircle(center, radius - 20, _paint);
  612. }
  613. @override
  614. bool shouldRepaint(CustomPainter oldDelegate) => false;
  615. }