strength_page.dart 21 KB

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