strength_page.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import 'dart:math';
  2. import 'dart:ui';
  3. import 'dart:ui' as ui;
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter_swiper/flutter_swiper.dart';
  6. import 'package:sport/bean/sport_detail.dart';
  7. import 'package:sport/bean/sport_index.dart';
  8. import 'package:sport/services/api/inject_api.dart';
  9. import 'package:sport/services/api/resp.dart';
  10. import 'package:sport/widgets/appbar.dart';
  11. import 'package:sport/widgets/decoration.dart';
  12. import 'package:sport/widgets/misc.dart';
  13. import 'package:sport/widgets/progress_bar.dart';
  14. class StrengthPage extends StatefulWidget {
  15. @override
  16. State<StatefulWidget> createState() => _PageState();
  17. }
  18. class _PageState extends State<StrengthPage> with InjectApi {
  19. ValueNotifier<int> _valueNotifierIndex = ValueNotifier(26);
  20. SwiperController _swiperController;
  21. ScrollController _scrollController;
  22. @override
  23. void initState() {
  24. _swiperController = SwiperController();
  25. _scrollController = ScrollController();
  26. super.initState();
  27. }
  28. @override
  29. void dispose() {
  30. _swiperController?.dispose();
  31. _valueNotifierIndex?.dispose();
  32. _scrollController?.dispose();
  33. super.dispose();
  34. }
  35. @override
  36. Widget build(BuildContext context) {
  37. return Scaffold(
  38. body: CustomScrollView(
  39. slivers: <Widget>[
  40. buildSliverAppBar(context, "运动强度", backgroundColor: Theme.of(context).scaffoldBackgroundColor),
  41. SliverToBoxAdapter(
  42. child: Container(
  43. margin: const EdgeInsets.all(12.0),
  44. padding: const EdgeInsets.all(12.0),
  45. decoration: circular(),
  46. child: FutureBuilder(
  47. future: createFutureType(0),
  48. builder: (BuildContext context, AsyncSnapshot<RespData<SportDetailSimple>> snapshot) => Column(
  49. children: <Widget>[
  50. Center(
  51. child: CustomPaint(
  52. painter: _Bg(),
  53. child: Container(
  54. width: 200,
  55. height: 200,
  56. child: Center(
  57. child: CircularProgressBar(
  58. percent: strengthToValue(snapshot?.data?.data?.sum?.consume ?? 0) / 12.0,
  59. radius: 69.0,
  60. center: Column(
  61. children: <Widget>[
  62. SizedBox(
  63. height: 10.0,
  64. ),
  65. Text(
  66. "${strengthToValue(snapshot?.data?.data?.sum?.consume ?? 0).toStringAsFixed(1)}",
  67. style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
  68. strutStyle: fixedLine,
  69. ),
  70. Text(
  71. "卡/分",
  72. style: Theme.of(context).textTheme.subtitle2,
  73. strutStyle: fixedLine,
  74. )
  75. ],
  76. mainAxisSize: MainAxisSize.min,
  77. ),
  78. ),
  79. ),
  80. ),
  81. ),
  82. ),
  83. SizedBox(
  84. width: 15.0,
  85. ),
  86. Padding(
  87. padding: const EdgeInsets.symmetric(vertical: 12.0),
  88. child: Row(
  89. children: <Widget>[
  90. Row(
  91. children: <Widget>[
  92. Container(
  93. width: 10,
  94. height: 10,
  95. decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFFE600)),
  96. ),
  97. const SizedBox(
  98. width: 5.0,
  99. ),
  100. Text(
  101. "低强度",
  102. style: Theme.of(context).textTheme.bodyText1,
  103. strutStyle: fixedLine,
  104. ),
  105. const SizedBox(
  106. width: 12.0,
  107. ),
  108. Container(
  109. width: 10,
  110. height: 10,
  111. decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFFAA00)),
  112. ),
  113. const SizedBox(
  114. width: 5.0,
  115. ),
  116. Text(
  117. "强度适中",
  118. style: Theme.of(context).textTheme.bodyText1,
  119. strutStyle: fixedLine,
  120. ),
  121. const SizedBox(
  122. width: 12.0,
  123. ),
  124. Container(
  125. width: 10,
  126. height: 10,
  127. decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xffFF7323)),
  128. ),
  129. const SizedBox(
  130. width: 5.0,
  131. ),
  132. Text(
  133. "高强度",
  134. style: Theme.of(context).textTheme.bodyText1,
  135. strutStyle: fixedLine,
  136. )
  137. ],
  138. ),
  139. Text(
  140. "单位:卡/分钟",
  141. style: Theme.of(context).textTheme.bodyText1,
  142. strutStyle: fixedLine,
  143. )
  144. ],
  145. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  146. ),
  147. ),
  148. Divider(),
  149. Padding(
  150. padding: const EdgeInsets.symmetric(vertical: 16.0),
  151. child: Row(
  152. mainAxisAlignment: MainAxisAlignment.spaceAround,
  153. children: <Widget>[
  154. Column(
  155. children: <Widget>[
  156. Text(
  157. "${snapshot?.data?.data?.sum?.consume ?? 0}",
  158. style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
  159. ),
  160. Text(
  161. "运动消耗 (卡)",
  162. style: Theme.of(context).textTheme.bodyText2,
  163. ),
  164. ],
  165. mainAxisSize: MainAxisSize.min,
  166. crossAxisAlignment: CrossAxisAlignment.center,
  167. ),
  168. Column(
  169. children: <Widget>[
  170. Text(
  171. "${snapshot?.data?.data?.sum?.durationMinute ?? 0}",
  172. style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
  173. ),
  174. Text(
  175. "运动时长 (分钟)",
  176. style: Theme.of(context).textTheme.bodyText2,
  177. ),
  178. ],
  179. mainAxisSize: MainAxisSize.min,
  180. crossAxisAlignment: CrossAxisAlignment.center,
  181. )
  182. ],
  183. ),
  184. )
  185. ],
  186. ),
  187. ),
  188. ),
  189. ),
  190. SliverToBoxAdapter(
  191. child: Container(
  192. height: 140.0,
  193. margin: const EdgeInsets.only(top: 12.0),
  194. child: CustomPaint(
  195. painter: _ListBg(),
  196. child: FutureBuilder(
  197. future: createFutureType(1).asStream().map((event) => convert(event)).last,
  198. builder: (BuildContext context, AsyncSnapshot<List<_DataItem>> snapshot) {
  199. var list = snapshot?.data;
  200. if (list == null || list?.isEmpty == true) return Container();
  201. return ValueListenableBuilder(
  202. valueListenable: _valueNotifierIndex,
  203. builder: (BuildContext context, int value, Widget child) {
  204. return SingleChildScrollView(
  205. reverse: true,
  206. child: Padding(
  207. padding: EdgeInsets.only(top: 10.0),
  208. child: Row(
  209. children: list?.map((e) {
  210. var index = list.indexOf(e);
  211. return GestureDetector(
  212. behavior: HitTestBehavior.opaque,
  213. onTap: () {
  214. _valueNotifierIndex.value = index;
  215. var page = max(0, strengthArr.indexOf(strengthToLabel(list[index].data)));
  216. _swiperController?.move(page);
  217. },
  218. child: Column(
  219. mainAxisAlignment: MainAxisAlignment.end,
  220. children: <Widget>[
  221. CustomPaint(
  222. child: SizedBox(
  223. width: MediaQuery.of(context).size.width / 7.0,
  224. height: 100.0,
  225. ),
  226. painter: _Dot(list, index),
  227. ),
  228. Container(
  229. height: 30.0,
  230. decoration: index == value
  231. ? BoxDecoration(
  232. border: Border(bottom: BorderSide(color: Theme.of(context).accentColor, width: 3.0)),
  233. )
  234. : null,
  235. child: Center(
  236. child: Text(
  237. "${e.date}",
  238. style: e.date == "今日"
  239. ? Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor)
  240. : Theme.of(context).textTheme.subtitle1,
  241. ))),
  242. ],
  243. ));
  244. })?.toList(),
  245. )),
  246. scrollDirection: Axis.horizontal,
  247. );
  248. },
  249. );
  250. },
  251. ),
  252. )),
  253. ),
  254. SliverToBoxAdapter(
  255. child: Padding(
  256. padding: const EdgeInsets.only(top: 12.0),
  257. child: Container(
  258. width: double.infinity,
  259. height: 145.0,
  260. child: Swiper(
  261. controller: _swiperController,
  262. loop: false,
  263. itemBuilder: (BuildContext context, int index) {
  264. return Container(
  265. margin: const EdgeInsets.all(12.0),
  266. // decoration: circular(),
  267. // padding: const EdgeInsets.all(12.0),
  268. child: Column(
  269. crossAxisAlignment: CrossAxisAlignment.start,
  270. children: <Widget>[
  271. Text(
  272. index == 0 ? "低强度" : index == 1 ? "强度适中" : "高强度",
  273. style: Theme.of(context).textTheme.headline1,
  274. ),
  275. SizedBox(
  276. height: 5.0,
  277. ),
  278. Text(
  279. index == 0
  280. ? "感觉舒适,呼吸有节奏,能自由进行交谈。该级别适用于恢复和基础心血功能训练,可以提高心脏的泵血能力和肌肉使用氧气的能力"
  281. : index == 1 ? "该级别训练强度适中,较难以进行交谈,呼吸加重。可以适当增强心肺功能,获得更强耐力" : "感觉难受,且呼吸急促,难以长时间维持该强度训练。可以增强力量和肌肉耐力,提高厌氧能力和乳酸阈值",
  282. style: Theme.of(context).textTheme.bodyText2,
  283. ),
  284. ],
  285. ),
  286. );
  287. },
  288. itemCount: 3),
  289. ),
  290. ),
  291. )
  292. ],
  293. ),
  294. );
  295. }
  296. Future<RespData<SportDetailSimple>> createFutureType(int type) async {
  297. Future<RespData<SportDetailSimple>> data;
  298. var time = DateTime.now();
  299. switch (type) {
  300. case 0:
  301. data = api.getSportRecordListOneDay('${time.year}-${'${time.month}'.padLeft(2, '0')}-${'${time.day}'.padLeft(2, '0')}');
  302. break;
  303. case 1:
  304. DateTime end = DateTime(time.year, time.month, time.day + 3);
  305. DateTime start = DateTime(time.year, time.month, end.day - 30);
  306. data = api.getSportRecordListByDay('${start.year}-${'${start.month}'.padLeft(2, '0')}-${'${start.day}'.padLeft(2, '0')}',
  307. '${end.year}-${'${end.month}'.padLeft(2, '0')}-${'${end.day}'.padLeft(2, '0')}');
  308. break;
  309. }
  310. return data;
  311. }
  312. List<_DataItem> convert(RespData<SportDetailSimple> data) {
  313. List<_DataItem> items = [];
  314. List<RecordsToday> records = data?.data?.records ?? [];
  315. var time = DateTime.now();
  316. DateTime end = DateTime(time.year, time.month, time.day + 3);
  317. for (var i = 0; i < 30; i++) {
  318. var day = DateTime(end.year, end.month, end.day - i);
  319. String tag = '${day.year}-${'${day.month}'.padLeft(2, '0')}-${'${day.day}'.padLeft(2, '0')}';
  320. String date = (time.month == day.month && time.day == day.day) ? '今日' : (day.day == 1) ? '${day.month}.${day.day}' : '${day.day}';
  321. int total = 0;
  322. for (var record in records) {
  323. if (tag == record.createdAt.split(" ")[0]) {
  324. total += record.consume;
  325. }
  326. }
  327. items.add(_DataItem(tag, date, total));
  328. if (i == 3) {
  329. _swiperController?.move(max(0, strengthArr.indexOf(strengthToLabel(total))));
  330. }
  331. }
  332. return items.reversed.toList();
  333. }
  334. }
  335. class _DataItem {
  336. final String tag;
  337. final String date;
  338. final int data;
  339. _DataItem(this.tag, this.date, this.data);
  340. }
  341. class _Bg extends CustomPainter {
  342. final Paint _paint = Paint()
  343. ..color = const Color(0xffDCDCDC)
  344. ..strokeWidth = 0.5
  345. ..style = PaintingStyle.stroke
  346. ..isAntiAlias = true;
  347. final ParagraphStyle _valueStyle = ParagraphStyle(
  348. textAlign: TextAlign.center,
  349. fontSize: 12,
  350. );
  351. @override
  352. void paint(Canvas canvas, Size size) {
  353. final double cx = size.width / 2;
  354. final double cy = size.height / 2;
  355. final double radius = (size.width - 40) / 2;
  356. canvas.drawCircle(Offset(cx, cy), radius, _paint);
  357. canvas.save();
  358. for (int i = 0; i < 4; i++) {
  359. canvas.translate(cx, cy);
  360. canvas.rotate(pi * 2 / 3);
  361. canvas.translate(-cx, -cy);
  362. canvas.drawLine(Offset(cx, cy - radius), Offset(cx, cy - radius + 4), _paint);
  363. }
  364. canvas.restore();
  365. ParagraphBuilder pb = ParagraphBuilder(_valueStyle)
  366. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  367. ..addText("0");
  368. ParagraphConstraints constraints = ParagraphConstraints(width: 20.0);
  369. Paragraph paragraph = pb.build()..layout(constraints);
  370. paragraph.computeLineMetrics().forEach((element) {
  371. canvas.drawParagraph(paragraph, Offset(cx - element.width - 4, 2));
  372. });
  373. pb = ParagraphBuilder(_valueStyle)
  374. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  375. ..addText("4.0");
  376. constraints = ParagraphConstraints(width: 20.0);
  377. paragraph = pb.build()..layout(constraints);
  378. paragraph.computeLineMetrics().forEach((element) {
  379. canvas.drawParagraph(paragraph, Offset(size.width - 25, 140));
  380. });
  381. pb = ParagraphBuilder(_valueStyle)
  382. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  383. ..addText("8.0");
  384. constraints = ParagraphConstraints(width: 20.0);
  385. paragraph = pb.build()..layout(constraints);
  386. paragraph.computeLineMetrics().forEach((element) {
  387. canvas.drawParagraph(paragraph, Offset(6, 140.0));
  388. });
  389. }
  390. @override
  391. bool shouldRepaint(CustomPainter oldDelegate) => false;
  392. }
  393. class _ListBg extends CustomPainter {
  394. final Paint _paint = Paint()
  395. ..color = const Color(0xffDCDCDC)
  396. ..strokeWidth = 0.5
  397. ..isAntiAlias = true;
  398. final ParagraphStyle _valueStyle = ParagraphStyle(
  399. textAlign: TextAlign.right,
  400. fontSize: 12,
  401. );
  402. @override
  403. void paint(Canvas canvas, Size size) {
  404. final double height = size.height - 40;
  405. final int split = 3;
  406. final double splitHeight = height / split;
  407. double _startY = 10;
  408. for (var i = 0; i < split; i++) {
  409. canvas.drawLine(Offset(0, _startY), Offset(size.width, _startY), _paint);
  410. _startY += splitHeight;
  411. ParagraphBuilder pb = ParagraphBuilder(_valueStyle)
  412. ..pushStyle(ui.TextStyle(color: Color(0xff999999)))
  413. ..addText("${(8.0 - (4.0 * i)).toStringAsFixed(1)}");
  414. ParagraphConstraints constraints = ParagraphConstraints(width: size.width - 10);
  415. Paragraph paragraph = pb.build()..layout(constraints);
  416. paragraph.computeLineMetrics().forEach((element) {
  417. canvas.drawParagraph(paragraph, Offset(0, splitHeight * i + element.baseline / 2 + 14));
  418. });
  419. }
  420. canvas.drawLine(Offset(0, size.height - 30), Offset(size.width, size.height - 30), _paint);
  421. }
  422. @override
  423. bool shouldRepaint(CustomPainter oldDelegate) => false;
  424. }
  425. class _Dot extends CustomPainter {
  426. final List<_DataItem> items;
  427. final int index;
  428. static const Color _color = const Color(0xffFFC400);
  429. final Paint _paint = Paint()
  430. ..maskFilter = MaskFilter.blur(BlurStyle.solid, 3)
  431. ..isAntiAlias = true;
  432. final Paint _line = Paint()
  433. ..color = _color
  434. ..strokeWidth = 0.5
  435. ..isAntiAlias = true;
  436. final Paint _polygonal = Paint()
  437. ..color = _color
  438. ..strokeWidth = 1
  439. ..isAntiAlias = true;
  440. _Dot(this.items, this.index);
  441. @override
  442. void paint(Canvas canvas, Size size) {
  443. if (index > items.length - 4) return;
  444. double _height = size.height;
  445. final double cx = size.width / 2;
  446. final double progress = min(1.0, items[index].data / 60.0 / 12.0);
  447. final double cy = _height * (1.0 - progress);
  448. print("$progress $cy ${items[index].data}");
  449. final double radius = size.width * 0.15 / 2;
  450. _paint.color = _color;
  451. canvas.drawCircle(Offset(cx, cy), radius, _paint);
  452. if (index == items.length - 4) {
  453. double _h = cy;
  454. while (_h < _height) {
  455. canvas.drawLine(Offset(cx, _h), Offset(cx, min(_height, _h + 3)), _line);
  456. _h += 5;
  457. }
  458. }
  459. for (var i = index - 1; i < index + 1; i += 2) {
  460. if (i < 0) continue;
  461. if (i >= items.length) continue;
  462. double _cx = (cx - size.width) * (index - i);
  463. double _cy = size.height * (1.0 - min(1.0, items[i].data / 60.0 / 12.0));
  464. canvas.drawLine(Offset(_cx, _cy), Offset(cx, cy), _polygonal);
  465. }
  466. }
  467. @override
  468. bool shouldRepaint(CustomPainter oldDelegate) => false;
  469. }