strength_page.dart 21 KB

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