sport_history_page.dart 21 KB

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