sport_history_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import 'dart:math' as math;
  2. import 'dart:ui';
  3. import 'package:cached_network_image/cached_network_image.dart';
  4. import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' as extended;
  5. import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
  6. import 'package:flutter/cupertino.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/painting.dart';
  9. import 'package:flutter_timeline/flutter_timeline.dart';
  10. import 'package:flutter_timeline/indicator_position.dart';
  11. import 'package:sport/bean/game.dart';
  12. import 'package:sport/bean/game_record.dart';
  13. import 'package:sport/bean/sport_detail.dart';
  14. import 'package:sport/provider/lib/provider_widget.dart';
  15. import 'package:sport/provider/lib/view_state_lifecycle.dart';
  16. import 'package:sport/provider/sport_history_model.dart';
  17. import 'package:sport/services/api/inject_api.dart';
  18. import 'package:sport/services/api/resp.dart';
  19. import 'package:sport/utils/DateFormat.dart';
  20. import 'package:sport/widgets/appbar.dart';
  21. import 'package:sport/widgets/decoration.dart';
  22. import 'package:sport/widgets/loading.dart';
  23. import 'package:sport/widgets/misc.dart';
  24. import 'package:sport/widgets/space.dart';
  25. class SportHistoryPage extends StatefulWidget {
  26. final GameInfoData details;
  27. SportHistoryPage(this.details);
  28. @override
  29. State<StatefulWidget> createState() => _PageState();
  30. }
  31. class _PageState extends State<SportHistoryPage> with TickerProviderStateMixin {
  32. ScrollController _controller;
  33. double _expandedHeight = 230.0;
  34. int _brightness = 0;
  35. @override
  36. void initState() {
  37. super.initState();
  38. _controller = ScrollController()
  39. ..addListener(() {
  40. print("${_controller.position.pixels} --- ${_expandedHeight}");
  41. if (_controller.position.pixels >= _expandedHeight / 2) {
  42. if (_brightness == 0) {
  43. setState(() {
  44. _brightness = 1;
  45. });
  46. }
  47. } else {
  48. if (_brightness == 1) {
  49. setState(() {
  50. _brightness = 0;
  51. });
  52. }
  53. }
  54. });
  55. }
  56. @override
  57. Widget build(BuildContext context) {
  58. final double tabHeader = 40.0;
  59. final double statusBarHeight = MediaQuery.of(context).padding.top;
  60. final double pinnedHeaderHeight =
  61. //statusBar height
  62. statusBarHeight +
  63. //pinned SliverAppBar height in header
  64. kToolbarHeight +
  65. tabHeader;
  66. return Scaffold(
  67. backgroundColor: Colors.white,
  68. body: DefaultTabController(
  69. length: 2,
  70. child: extended.NestedScrollView(
  71. controller: _controller,
  72. pinnedHeaderSliverHeightBuilder: () {
  73. return pinnedHeaderHeight;
  74. },
  75. innerScrollPositionKeyBuilder: () {
  76. TabController tabController = DefaultTabController.of(context);
  77. String index = 'Tab${tabController.index}';
  78. return Key(index);
  79. },
  80. headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
  81. return <Widget>[
  82. SliverAppBar(
  83. pinned: true,
  84. expandedHeight: _expandedHeight,
  85. backgroundColor: Colors.white,
  86. forceElevated: innerBoxIsScrolled,
  87. titleSpacing: 0,
  88. elevation: 0,
  89. iconTheme: IconThemeData(color: _brightness == 0 ? Colors.white : Colors.black),
  90. brightness: _brightness == 0 ? Brightness.dark : Brightness.light,
  91. leading: IconButton(
  92. icon: Image.asset("lib/assets/img/topbar_return${_brightness == 0 ? "_white" : ""}.png"),
  93. onPressed: () {
  94. Navigator.of(context).maybePop();
  95. },
  96. ),
  97. title: _brightness == 0
  98. ? Text("")
  99. : Text(
  100. "${widget.details.name}",
  101. style: titleStyle,
  102. ),
  103. flexibleSpace: FlexibleSpaceBar(
  104. collapseMode: CollapseMode.pin,
  105. background: Container(
  106. child: Stack(
  107. fit: StackFit.expand,
  108. children: <Widget>[
  109. CachedNetworkImage(
  110. imageUrl: widget.details.coverHorizontal,
  111. fit: BoxFit.fill,
  112. ),
  113. BackdropFilter(
  114. filter: ImageFilter.blur(sigmaX: 30.0, sigmaY: 30.0),
  115. child: Center(
  116. child: Container(
  117. color: Colors.black.withOpacity(.3),
  118. ),
  119. ),
  120. ),
  121. Center(
  122. child: Column(
  123. children: <Widget>[
  124. ClipRRect(
  125. child: CachedNetworkImage(
  126. width: 90.0,
  127. height: 90.0,
  128. imageUrl: widget.details.cover,
  129. fit: BoxFit.cover,
  130. ),
  131. borderRadius: new BorderRadius.all(Radius.circular(6.0)),
  132. ),
  133. Padding(
  134. padding: const EdgeInsets.all(12.0),
  135. child: Text(
  136. widget.details.name,
  137. style: Theme.of(context).textTheme.headline4,
  138. ),
  139. )
  140. ],
  141. mainAxisSize: MainAxisSize.min,
  142. ),
  143. ),
  144. // Positioned(
  145. // left: 0,
  146. // right: 0,
  147. // bottom: 48,
  148. // child: Container(
  149. // height: 10,
  150. // decoration: BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(10)), color: Colors.white),
  151. // ),
  152. // ),
  153. ],
  154. ),
  155. )),
  156. bottom: PreferredSize(
  157. preferredSize: Size.fromHeight(40.0),
  158. child: Container(
  159. // color: Colors.white,
  160. // transform: Matrix4.translationValues(0, -10, 0),
  161. decoration: BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(10)), color: Colors.white),
  162. child: Center(
  163. child: Container(
  164. margin: EdgeInsets.only(top: 10.0),
  165. height: 36.0,
  166. child: TabBar(
  167. isScrollable: true,
  168. labelPadding: EdgeInsets.symmetric(horizontal: 20),
  169. indicatorWeight: 3,
  170. indicatorPadding: EdgeInsets.symmetric(horizontal: 6),
  171. tabs: <Widget>[
  172. Tab(
  173. text: '数据',
  174. ),
  175. Tab(text: '记录')
  176. ],
  177. ),
  178. ),
  179. )),
  180. ),
  181. )
  182. ];
  183. },
  184. body: TabBarView(children: [
  185. NestedScrollViewInnerScrollPositionKeyWidget(
  186. const Key('Tab0'),
  187. _DataPage(details: widget.details),
  188. ),
  189. NestedScrollViewInnerScrollPositionKeyWidget(
  190. const Key('Tab1'),
  191. _ListPage(details: widget.details),
  192. )
  193. ]),
  194. ),
  195. ),
  196. );
  197. }
  198. }
  199. class _DataPage extends StatefulWidget {
  200. final GameInfoData details;
  201. const _DataPage({Key key, this.details}) : super(key: key);
  202. @override
  203. State<StatefulWidget> createState() => _DataPageState();
  204. }
  205. class _DataPageState extends State<_DataPage> with InjectApi {
  206. Future<RespData<GameRecord>> _future;
  207. @override
  208. void initState() {
  209. super.initState();
  210. _future = api.getGameRecord(widget.details.id);
  211. }
  212. @override
  213. Widget build(BuildContext context) {
  214. return SingleChildScrollView(
  215. child: FutureBuilder(
  216. future: _future,
  217. builder: (BuildContext context, AsyncSnapshot<RespData<GameRecord>> snapshot) {
  218. if (snapshot.connectionState != ConnectionState.done) return RequestLoadingWidget();
  219. var _data = snapshot.data?.data;
  220. if (_data == null) return Container();
  221. return Column(
  222. children: <Widget>[_group("今日运动", _data.today), _group("累计运动", _data.sum)],
  223. );
  224. },
  225. ),
  226. );
  227. }
  228. Widget _group(String name, RecordsTodaySum sum) {
  229. return Padding(
  230. padding: const EdgeInsets.symmetric(vertical: 12.0),
  231. child: Column(children: <Widget>[
  232. Container(
  233. width: double.infinity,
  234. height: 24.0,
  235. child: Padding(
  236. padding: const EdgeInsets.symmetric(horizontal: 12.0),
  237. child: Align(
  238. alignment: Alignment.centerLeft,
  239. child: Text(
  240. name,
  241. style: Theme.of(context).textTheme.bodyText2,
  242. ),
  243. ),
  244. ),
  245. color: Color(0xfff1f1f1),
  246. ),
  247. const SizedBox(
  248. height: 12.0,
  249. ),
  250. _row("datasummary_icon_duration", "累计时长", "${sum.durationMinute}", "分钟"),
  251. _row("datasummary_icon_score", "最高评分", "${sum.scoreMax}", ""),
  252. _row("datasummary_icon_frequency", "累计次数", "${sum.times}", "次"),
  253. _row("datasummary_icon_consume", "累计消耗", "${sum.consume}", "卡"),
  254. _row("datasummary_icon_greaseefficiency", "燃脂效率", "${sum.consumeRate.toStringAsFixed(1)}", "卡/分钟"),
  255. Divider(),
  256. _row("datasummary_icon_squat", "下蹲频率", "${sum.crouchRate}", "次/分钟"),
  257. _row("datasummary_icon_jump", "跳跃频率", "${sum.jumpRate}", "次/分钟"),
  258. _row("datasummary_icon_steps", "游戏步数", "${sum.stepRate}", "步"),
  259. ]),
  260. );
  261. }
  262. Widget _row(String icon, String name, String value, String unit) {
  263. return Padding(
  264. padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
  265. child: Row(
  266. children: <Widget>[
  267. Image.asset("lib/assets/img/$icon.png"),
  268. const SizedBox(
  269. width: 8.0,
  270. ),
  271. Expanded(child: Text(name, style: Theme.of(context).textTheme.subtitle1)),
  272. ConstrainedBox(
  273. constraints: BoxConstraints(minWidth: 100.0),
  274. child: Row(
  275. crossAxisAlignment: CrossAxisAlignment.end,
  276. children: <Widget>[
  277. Text(value, style: Theme.of(context).textTheme.headline1),
  278. Text(unit, style: Theme.of(context).textTheme.subtitle2),
  279. ],
  280. ))
  281. ],
  282. ),
  283. );
  284. }
  285. }
  286. class _ListPage extends StatefulWidget {
  287. final GameInfoData details;
  288. const _ListPage({Key key, this.details}) : super(key: key);
  289. @override
  290. State<StatefulWidget> createState() => _ListPageState();
  291. }
  292. class _ListPageState extends ViewStateLifecycle<_ListPage, SportHistoryModel> {
  293. @override
  294. void initState() {
  295. super.initState();
  296. }
  297. @override
  298. Widget build(BuildContext context) {
  299. var color = const Color(0xffFFC400);
  300. var dot = Container(
  301. width: 7.0,
  302. height: 7.0,
  303. decoration: BoxDecoration(color: color, shape: BoxShape.circle, boxShadow: [BoxShadow(color: color, blurRadius: 5, offset: Offset(0, 0))]),
  304. );
  305. return Scaffold(
  306. backgroundColor: Colors.white,
  307. body: ProviderWidget<SportHistoryModel>(
  308. model: model,
  309. onModelReady: (model) => model.initData(),
  310. builder: (_, model, __) {
  311. var list = model.list;
  312. var map = Map.fromIterable(list,
  313. key: (key) => key.tag,
  314. value: (value) {
  315. return list.where((item) => item.tag == value.tag).toList();
  316. });
  317. // print("$list");
  318. // print("$map");
  319. return TimelineTheme(
  320. data: TimelineThemeData(lineColor: const Color(0xffFFC400), strokeWidth: 1),
  321. child: Timeline(
  322. padding: EdgeInsets.all(12.0),
  323. indicatorSize: 12.0,
  324. events: map.keys.map((e) {
  325. var gameList = map[e];
  326. return TimelineEventDisplay(
  327. child: Column(
  328. crossAxisAlignment: CrossAxisAlignment.start,
  329. children: <Widget>[
  330. Padding(
  331. padding: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 6.0),
  332. child: Text(
  333. e,
  334. style: Theme.of(context).textTheme.bodyText2,
  335. strutStyle: fixedLine,
  336. ),
  337. ),
  338. Column(
  339. children: gameList
  340. .map((item) => Container(
  341. margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
  342. padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
  343. decoration: card(),
  344. child: Row(
  345. children: <Widget>[
  346. ConstrainedBox(
  347. constraints: BoxConstraints(minWidth: 80.0),
  348. child: Column(
  349. children: <Widget>[
  350. Text(
  351. item.mode == 0 ? "闯关模式" : item.mode == 1 ? "匹配模式" : "好友模式",
  352. style: Theme.of(context).textTheme.headline3,
  353. ),
  354. const SizedBox(
  355. height: 3.0,
  356. ),
  357. Text(
  358. "${DateFormat.formatCreateAtHHmm(item.createdAt)}",
  359. style: Theme.of(context).textTheme.bodyText2,
  360. ),
  361. ],
  362. ),
  363. ),
  364. Container(
  365. margin: const EdgeInsets.symmetric(horizontal: 16.0),
  366. height: 70.0,
  367. child: DashedRect(color: Color(0xff979797), strokeWidth: 0.5, gap: 3.0),
  368. ),
  369. Column(
  370. crossAxisAlignment: CrossAxisAlignment.start,
  371. children: <Widget>[
  372. Text(
  373. "评分:${item.score}",
  374. style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor),
  375. ),
  376. const SizedBox(
  377. height: 3.0,
  378. ),
  379. Text(
  380. "时长:${(item.duration ??0) ~/ 60}分${(item.duration ??0) % 60}秒",
  381. style: Theme.of(context).textTheme.subtitle1,
  382. ),
  383. const SizedBox(
  384. height: 3.0,
  385. ),
  386. Text(
  387. "消耗:${item.consume} 卡",
  388. style: Theme.of(context).textTheme.subtitle1,
  389. ),
  390. ],
  391. )
  392. ],
  393. ),
  394. ))
  395. .toList(),
  396. )
  397. ],
  398. ),
  399. indicatorSize: 9.0,
  400. indicator: dot);
  401. }).toList(),
  402. anchor: IndicatorPosition.top,
  403. ));
  404. }),
  405. );
  406. }
  407. Widget _buildHistoryItemWidget(RecordsTodaySum item) {
  408. return Container(
  409. color: Colors.white,
  410. padding: const EdgeInsets.fromLTRB(12.0, 6, 12.0, 6),
  411. child: Row(
  412. children: <Widget>[
  413. Expanded(
  414. child: Column(
  415. crossAxisAlignment: CrossAxisAlignment.start,
  416. children: <Widget>[
  417. Text(
  418. item.screen == 1 ? "投屏运动" : "手机运动",
  419. style: Theme.of(context).textTheme.subtitle1,
  420. ),
  421. Space(
  422. height: 4,
  423. ),
  424. Text(
  425. "日期:${item.createdAt}",
  426. style: Theme.of(context).textTheme.bodyText1,
  427. )
  428. ],
  429. ),
  430. ),
  431. Text(
  432. "${item.score.toStringAsFixed(1)}",
  433. style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor),
  434. ),
  435. Text("分", style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 12)),
  436. ],
  437. ),
  438. );
  439. }
  440. @override
  441. SportHistoryModel createModel() => SportHistoryModel(widget.details.id);
  442. }
  443. class DashedRect extends StatelessWidget {
  444. final Color color;
  445. final double strokeWidth;
  446. final double gap;
  447. DashedRect({this.color = Colors.black, this.strokeWidth = 1.0, this.gap = 5.0});
  448. @override
  449. Widget build(BuildContext context) {
  450. return Container(
  451. child: Padding(
  452. padding: EdgeInsets.all(strokeWidth / 2),
  453. child: CustomPaint(
  454. painter: DashRectPainter(color: color, strokeWidth: strokeWidth, gap: gap),
  455. ),
  456. ),
  457. );
  458. }
  459. }
  460. class DashRectPainter extends CustomPainter {
  461. double strokeWidth;
  462. Color color;
  463. double gap;
  464. DashRectPainter({this.strokeWidth = 5.0, this.color = Colors.red, this.gap = 5.0});
  465. @override
  466. void paint(Canvas canvas, Size size) {
  467. Paint dashedPaint = Paint()
  468. ..color = color
  469. ..strokeWidth = strokeWidth
  470. ..style = PaintingStyle.stroke;
  471. double x = size.width;
  472. double y = size.height;
  473. Path _topPath = getDashedPath(
  474. a: math.Point(0, 0),
  475. b: math.Point(x, 0),
  476. gap: gap,
  477. );
  478. Path _rightPath = getDashedPath(
  479. a: math.Point(x, 0),
  480. b: math.Point(x, y),
  481. gap: gap,
  482. );
  483. Path _bottomPath = getDashedPath(
  484. a: math.Point(0, y),
  485. b: math.Point(x, y),
  486. gap: gap,
  487. );
  488. Path _leftPath = getDashedPath(
  489. a: math.Point(0, 0),
  490. b: math.Point(0.001, y),
  491. gap: gap,
  492. );
  493. canvas.drawPath(_topPath, dashedPaint);
  494. canvas.drawPath(_rightPath, dashedPaint);
  495. canvas.drawPath(_bottomPath, dashedPaint);
  496. canvas.drawPath(_leftPath, dashedPaint);
  497. }
  498. Path getDashedPath({
  499. @required math.Point<double> a,
  500. @required math.Point<double> b,
  501. @required gap,
  502. }) {
  503. Size size = Size(b.x - a.x, b.y - a.y);
  504. Path path = Path();
  505. path.moveTo(a.x, a.y);
  506. bool shouldDraw = true;
  507. math.Point currentPoint = math.Point(a.x, a.y);
  508. num radians = math.atan(size.height / size.width);
  509. num dx = math.cos(radians) * gap < 0 ? math.cos(radians) * gap * -1 : math.cos(radians) * gap;
  510. num dy = math.sin(radians) * gap < 0 ? math.sin(radians) * gap * -1 : math.sin(radians) * gap;
  511. while (currentPoint.x <= b.x && currentPoint.y <= b.y) {
  512. shouldDraw ? path.lineTo(currentPoint.x, currentPoint.y) : path.moveTo(currentPoint.x, currentPoint.y);
  513. shouldDraw = !shouldDraw;
  514. currentPoint = math.Point(
  515. currentPoint.x + dx,
  516. currentPoint.y + dy,
  517. );
  518. }
  519. return path;
  520. }
  521. @override
  522. bool shouldRepaint(CustomPainter oldDelegate) {
  523. return false;
  524. }
  525. }