run_detail_page.dart 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. import 'dart:convert';
  2. import 'dart:math' as math;
  3. import 'dart:ui' as ui;
  4. import 'package:flutter/cupertino.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:provider/provider.dart';
  8. import 'package:sport/bean/jog/detail.dart';
  9. import 'package:sport/pages/run/location.dart';
  10. import 'package:sport/pages/run/map.dart';
  11. import 'package:sport/pages/run/map_replay_page.dart';
  12. import 'package:sport/pages/run/run_data.dart';
  13. import 'package:sport/pages/run/run_page.dart';
  14. import 'package:sport/pages/run/run_share.dart';
  15. import 'package:sport/pages/run/setting_page.dart';
  16. import 'package:sport/pages/run/statistics.dart';
  17. import 'package:sport/provider/user_model.dart';
  18. import 'package:sport/router/navigator_util.dart';
  19. import 'package:sport/services/api/inject_api.dart';
  20. import 'package:sport/services/app_lifecycle_state.dart';
  21. import 'package:sport/utils/DateFormat.dart';
  22. import 'package:sport/utils/sport_utils.dart';
  23. import 'package:sport/widgets/decoration.dart';
  24. import 'package:sport/widgets/image.dart';
  25. import 'package:sport/widgets/linear_progress_indicator.dart' as progress;
  26. import 'package:sport/widgets/loading.dart';
  27. import 'package:sport/widgets/misc.dart';
  28. import 'package:sport/widgets/popmenu_bg.dart';
  29. import 'chart.dart';
  30. class RunDetailPage extends StatefulWidget {
  31. final int id;
  32. final bool post;
  33. final bool share;
  34. final JogDetail? jogDetail;
  35. const RunDetailPage({Key? key, this.id = 0, this.post = false, this.share = false, this.jogDetail}) : super(key: key);
  36. @override
  37. State<StatefulWidget> createState() => _PageState();
  38. }
  39. class _PageState extends LifecycleState<RunDetailPage> with RunSetting, InjectApi, TickerProviderStateMixin {
  40. final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
  41. final GlobalKey<MapState> mapKey = GlobalKey();
  42. bool _loading = true;
  43. JogDetail? _jogDetail;
  44. double _opacity = 0;
  45. double _expandedHeight = 0;
  46. int _brightness = 0;
  47. ValueNotifier<MapNotification> _notifierMapNotification = ValueNotifier(MapNotification(0, 0, 0));
  48. @override
  49. bool autoLoadSetting() {
  50. return false;
  51. }
  52. @override
  53. void initState() {
  54. super.initState();
  55. refreshSetting();
  56. _showData();
  57. }
  58. _showData() async {
  59. List<Location> points = [];
  60. _jogDetail = widget.jogDetail;
  61. if (_jogDetail == null) {
  62. var result = await api.jogShowRecord(widget.id);
  63. if (result.data != null) {
  64. _jogDetail = result.data!;
  65. }
  66. }
  67. if (_jogDetail != null) {
  68. JogDetail data = _jogDetail!;
  69. totalDistance = data.distance?.toDouble() ?? 0;
  70. totalTime = data.duration ?? 0;
  71. totalConsume = data.consume ?? 0;
  72. totalStep = data.step ?? 0;
  73. met = data.met ?? 0;
  74. timeStart = DateTime.parse(data.begin ?? "");
  75. timeEnd = DateTime.parse(data.end ?? "");
  76. points = data.points ?? [];
  77. if (data.stepInfo != null) stepRateList = json.decode(data.stepInfo ?? "[]").cast<int>();
  78. if (data.altitudeInfo != null) altitudeList = json.decode(data.altitudeInfo ?? "[]").cast<double>();
  79. var runData = RunData(0);
  80. if (points.length > 1) {
  81. runData.calPoints(points);
  82. }
  83. _pointsKm.addAll(runData.pointsKm);
  84. _pointsKmTime.addAll(runData.pointsKmTime);
  85. totalAltitude = runData.totalAltitude;
  86. marathonHalf = runData.marathonHalf;
  87. marathonAll = runData.marathonAll;
  88. }
  89. if (stepRateList.isNotEmpty == true) stepMax = stepRateList.reduce((value, element) => value > element ? value : element);
  90. stepAvg = totalStep ~/ (totalTime / 60);
  91. if (_pointsKmTime.isNotEmpty == true) {
  92. kmTime = _pointsKmTime.values.fold(0, (value, element) => value + element.inSeconds);
  93. avg = kmTime ~/ _pointsKmTime.length;
  94. min = _pointsKmTime.values.reduce((value, element) => value.inSeconds < element.inSeconds ? value : element).inSeconds;
  95. max = _pointsKmTime.values.reduce((value, element) => value.inSeconds > element.inSeconds ? value : element).inSeconds;
  96. // print("1111111111111111 $_pointsKmTime 22222222222222222 avg $avg $min");
  97. }
  98. setState(() {
  99. _loading = false;
  100. this.points = points;
  101. });
  102. }
  103. final Map<int, Location> _pointsKm = {};
  104. final Map<int, Duration> _pointsKmTime = {};
  105. List<Location> points = <Location>[];
  106. List<int> stepRateList = [];
  107. List<double> altitudeList = [];
  108. double totalDistance = 0;
  109. double totalAltitude = 0;
  110. int totalTime = 0;
  111. int totalStep = 0;
  112. int stepAvg = 0;
  113. int stepMax = 0;
  114. int totalConsume = 0;
  115. double met = 0.0;
  116. int kmTime = 0;
  117. int avg = 0;
  118. double avgSpeed = 0.0;
  119. int min = 0;
  120. int max = 1;
  121. DateTime? timeStart;
  122. DateTime? timeEnd;
  123. int marathonHalf = 0;
  124. int marathonAll = 0;
  125. @override
  126. void dispose() {
  127. super.dispose();
  128. }
  129. _share() async {
  130. var path = await mapKey.currentState?.takeSnapshot();
  131. if (path == null) return;
  132. if (_jogDetail == null) return;
  133. NavigatorUtil.goPage(
  134. context,
  135. (context) => RunShare(
  136. detail: _jogDetail!,
  137. map: path,
  138. ));
  139. }
  140. @override
  141. Widget build(BuildContext context) {
  142. var maxKm = _pointsKmTime.isNotEmpty ? _pointsKmTime.keys.reduce((value, element) => value > element ? value : element) : 0;
  143. var map = points.isNotEmpty == true
  144. ? NotificationListener<MapNotification>(
  145. onNotification: (map) {
  146. _notifierMapNotification.value = map;
  147. return false;
  148. },
  149. child: MapWidget(
  150. key: mapKey,
  151. distance: totalDistance,
  152. runMapType: runMapType,
  153. runMapKm: runMapKm,
  154. points: points,
  155. pointsKm: _pointsKm,
  156. ),
  157. )
  158. : Container();
  159. const _padding = 16.0;
  160. var header = Consumer<UserModel>(
  161. builder: (_, model, __) => Container(
  162. margin: const EdgeInsets.fromLTRB(12.0, 30, 12.0, 0),
  163. decoration: BoxDecoration(
  164. color: Colors.white,
  165. borderRadius: BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)),
  166. ),
  167. child: Column(
  168. mainAxisSize: MainAxisSize.min,
  169. children: [
  170. Padding(
  171. padding: const EdgeInsets.symmetric(horizontal: _padding),
  172. child: Row(
  173. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  174. crossAxisAlignment: CrossAxisAlignment.end,
  175. children: [
  176. Column(
  177. mainAxisSize: MainAxisSize.min,
  178. crossAxisAlignment: CrossAxisAlignment.start,
  179. children: [
  180. Text(
  181. "趣动户外跑",
  182. style: Theme.of(context).textTheme.bodyText1!,
  183. ),
  184. const SizedBox(
  185. height: 45.0,
  186. ),
  187. Row(
  188. textBaseline: TextBaseline.alphabetic,
  189. mainAxisSize: MainAxisSize.min,
  190. children: [
  191. ValueListenableBuilder<MapNotification>(
  192. valueListenable: _notifierMapNotification,
  193. builder: (_, data, __) {
  194. return Text(
  195. "${formatNum(data.distance / 1000, 2)}",
  196. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 45.0, fontFamily: "DIN"),
  197. strutStyle: fixedLine,
  198. );
  199. },
  200. ),
  201. Text(" 公里", style: Theme.of(context).textTheme.subtitle1!),
  202. marathonAll > 0
  203. ? Container(
  204. decoration: BoxDecoration(borderRadius: BorderRadius.circular(3), color: COLOR_RUN_FAST),
  205. margin: const EdgeInsets.only(left: 10),
  206. padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
  207. child: Center(
  208. child: Text(
  209. "全马",
  210. style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white),
  211. )),
  212. )
  213. : marathonHalf > 0
  214. ? Container(
  215. decoration: BoxDecoration(borderRadius: BorderRadius.circular(3), color: COLOR_RUN_SLOW),
  216. margin: const EdgeInsets.only(left: 10),
  217. padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
  218. child: Center(
  219. child: Text(
  220. "半马",
  221. style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white),
  222. )),
  223. )
  224. : Container()
  225. ],
  226. ),
  227. ],
  228. ),
  229. Container(
  230. child: Column(
  231. crossAxisAlignment: CrossAxisAlignment.end,
  232. mainAxisSize: MainAxisSize.min,
  233. children: <Widget>[
  234. Container(
  235. decoration: BoxDecoration(color: Colors.grey, shape: BoxShape.circle),
  236. child: CircleAvatar(
  237. backgroundColor: Colors.black26,
  238. backgroundImage: userAvatarProvider(model.user.avatar),
  239. radius: 30.0,
  240. ),
  241. transform: Matrix4.translationValues(0, -24, 0),
  242. ),
  243. Text(model.user.name, style: Theme.of(context).textTheme.headline1!),
  244. const SizedBox(
  245. height: 12.0,
  246. ),
  247. Text(
  248. "${SportUtils.toDateTime(timeStart)} ${SportUtils.toEndTime(timeStart)} ~ ${SportUtils.toEndTime(timeEnd)}",
  249. style: Theme.of(context).textTheme.bodyText1!,
  250. )
  251. ],
  252. ),
  253. ),
  254. ],
  255. ),
  256. ),
  257. ],
  258. ),
  259. ),
  260. );
  261. double infoHeight = 230 * MediaQuery.of(context).textScaleFactor;
  262. final double _iconSize = 40.0;
  263. List<Widget> _kmList = [];
  264. if (_pointsKmTime.isNotEmpty) {
  265. List<int> keys = _pointsKmTime.keys.toList();
  266. int second = 0;
  267. int totalSecond = 0;
  268. for (var i = 0; i < keys.length; i++) {
  269. int e = keys[i];
  270. second += _pointsKmTime[e]?.inSeconds ?? 0;
  271. // print("11111111111111 --- $e $second");
  272. totalSecond += _pointsKmTime[e]?.inSeconds ?? 0;
  273. _kmList.add(Padding(
  274. padding: const EdgeInsets.symmetric(vertical: 1.0),
  275. child: Row(
  276. mainAxisSize: MainAxisSize.max,
  277. children: [
  278. ConstrainedBox(
  279. constraints: BoxConstraints(minWidth: 16),
  280. child: Padding(
  281. padding: const EdgeInsets.only(top: 1.0),
  282. child: Text(
  283. "${e.toString().padLeft(2, "0")}",
  284. style: Theme.of(context).textTheme.subtitle1!.copyWith(fontFamily: "DIN"),
  285. ),
  286. ),
  287. ),
  288. Expanded(
  289. child: Container(
  290. height: 12.0,
  291. color: Color(0xfff1f1f1),
  292. margin: const EdgeInsets.symmetric(horizontal: 10.0),
  293. child: CustomPaint(
  294. painter: progress.LinearProgressIndicator((_pointsKmTime[e]?.inSeconds ?? 0) == min ? [Color(0xffFFC57A), Color(0xffFF5B1D)] : [Color(0xffFFF4CE), Color(0xffFFC400)], (_pointsKmTime[e]?.inSeconds ?? 0) / max * 0.9),
  295. ),
  296. ),
  297. ),
  298. ConstrainedBox(
  299. constraints: BoxConstraints(minWidth: 50),
  300. child: Text(
  301. "${SportUtils.pace(SportUtils.calPace((_pointsKmTime[e]?.inSeconds ?? 0), 1))}",
  302. style: Theme.of(context).textTheme.subtitle1!,
  303. ),
  304. ),
  305. ],
  306. ),
  307. ));
  308. if ((i + 1) % 5 == 0) {
  309. String tips = "近5公里用时 ${DateFormat.toTime(second)}";
  310. if (i > 5) {
  311. tips += " ${i + 1}公里用时 ${DateFormat.toTime(totalSecond)}";
  312. }
  313. _kmList.add(Padding(
  314. padding: const EdgeInsets.fromLTRB(26, 5, 0, 15),
  315. child: Container(
  316. height: 18.0,
  317. child: Text(
  318. tips,
  319. maxLines: 1,
  320. style: Theme.of(context).textTheme.bodyText1?.copyWith(fontSize: 11),
  321. )),
  322. ));
  323. second = 0;
  324. }
  325. }
  326. }
  327. double mapHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - 24.0 - infoHeight;
  328. return AnnotatedRegion<SystemUiOverlayStyle>(
  329. value: SystemUiOverlayStyle.light,
  330. child: Scaffold(
  331. key: _scaffoldKey,
  332. body: LoadingWidget(
  333. loading: _loading,
  334. willPop: false,
  335. child: CustomScrollView(
  336. shrinkWrap: widget.share,
  337. physics: ClampingScrollPhysics(),
  338. slivers: [
  339. SliverAppBar(
  340. expandedHeight: widget.share? 300:mapHeight,
  341. pinned: true,
  342. forceElevated: true,
  343. automaticallyImplyLeading: false,
  344. title: widget.share
  345. ? null
  346. : Row(
  347. children: [
  348. GestureDetector(
  349. onTap: () {
  350. Navigator.maybePop(context);
  351. },
  352. child: Container(width: _iconSize, height: _iconSize, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: arrowBack()),
  353. ),
  354. Expanded(
  355. child: Container(
  356. height: 1,
  357. )),
  358. PopupMenuTheme(
  359. data: PopupMenuThemeData(shape: PopmenuShape(borderRadius: BorderRadius.all(Radius.circular(10.0)))),
  360. child: PopupMenuButton(
  361. offset: Offset(-10, kToolbarHeight / 2 + 25),
  362. onSelected: (val) {
  363. NavigatorUtil.goPage(
  364. context,
  365. (context) => MapReplayPage(
  366. distance: totalDistance,
  367. duration: totalTime,
  368. kcal: totalConsume,
  369. runMapType: runMapType,
  370. runMapKm: runMapKm,
  371. points: points,
  372. pointsKm: _pointsKm,
  373. begin: timeStart?.millisecondsSinceEpoch ?? 0,
  374. showType: val == "1" ? 1 : 0,
  375. ));
  376. },
  377. itemBuilder: (context) {
  378. return divideMenus([
  379. PopupMenuItem(
  380. value: "1",
  381. child: Center(
  382. child: Padding(
  383. padding: const EdgeInsets.symmetric(vertical: 12.0),
  384. child: Text(
  385. "生成视频(20秒)",
  386. style: Theme.of(context).textTheme.subtitle1!,
  387. ),
  388. )),
  389. ),
  390. menuDivider(),
  391. PopupMenuItem(
  392. value: "0",
  393. child: Center(
  394. child: Padding(
  395. padding: const EdgeInsets.symmetric(vertical: 12.0),
  396. child: Text(
  397. "生成视频(${totalDistance ~/ 1000 * 4 + 4}秒)",
  398. style: Theme.of(context).textTheme.subtitle1!,
  399. ),
  400. )),
  401. ),
  402. ]);
  403. },
  404. child: Container(width: _iconSize, height: _iconSize, margin: EdgeInsets.only(right: 12.0), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/topbar_icon_trajectory.png"))),
  405. ),
  406. GestureDetector(
  407. onTap: () {
  408. _share();
  409. },
  410. child: Container(width: _iconSize, height: _iconSize, margin: EdgeInsets.only(right: 12.0), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/bbs_icon_share.png")),
  411. ),
  412. GestureDetector(
  413. onTap: () async {
  414. await NavigatorUtil.goPage(context, (context) => SettingPage());
  415. refreshSetting();
  416. },
  417. child: Container(width: _iconSize, height: _iconSize, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: Image.asset("lib/assets/img/setgoals_icon_set.png")),
  418. ),
  419. ],
  420. ),
  421. flexibleSpace: FlexibleSpaceBar(
  422. collapseMode: CollapseMode.pin,
  423. background: Container(
  424. color: Theme.of(context).scaffoldBackgroundColor,
  425. child: Stack(
  426. children: [Positioned(top: 0, left: 0, right: 0, bottom: 50.0, child: map), Align(alignment: Alignment.bottomCenter, child: header)],
  427. ),
  428. )),
  429. ),
  430. SliverToBoxAdapter(
  431. child: Container(
  432. height: infoHeight,
  433. margin: const EdgeInsets.fromLTRB(12.0, 0, 12.0, 12.0),
  434. decoration: BoxDecoration(
  435. color: Colors.white,
  436. borderRadius: BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)),
  437. ),
  438. child: Column(
  439. children: [
  440. Container(
  441. margin: const EdgeInsets.symmetric(vertical: 20.0),
  442. height: 20,
  443. child: Stack(
  444. fit: StackFit.expand,
  445. children: [
  446. Center(
  447. child: Container(
  448. height: 3,
  449. width: double.infinity,
  450. decoration: BoxDecoration(
  451. gradient: LinearGradient(
  452. begin: Alignment.centerLeft,
  453. end: Alignment.centerRight,
  454. colors: [
  455. COLOR_RUN_FAST,
  456. COLOR_RUN_MIDDLE,
  457. COLOR_RUN_SLOW,
  458. ],
  459. ),
  460. ),
  461. ),
  462. ),
  463. if (min != 0)
  464. Positioned(
  465. top: 0,
  466. bottom: 0,
  467. left: _padding,
  468. child: Container(
  469. color: Colors.white,
  470. height: double.infinity,
  471. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  472. child: Center(
  473. child: Text(
  474. // "${SportUtils.toEndTime(timeStart)}开始",
  475. "快 ${SportUtils.pace(SportUtils.calPace((min), 1))}",
  476. style: Theme.of(context).textTheme.bodyText1!,
  477. ),
  478. ),
  479. ),
  480. ),
  481. if (max != 0)
  482. Positioned(
  483. top: 0,
  484. bottom: 0,
  485. right: _padding,
  486. child: Container(
  487. color: Colors.white,
  488. height: double.infinity,
  489. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  490. child: Center(
  491. child: Text(
  492. // "${SportUtils.toEndTime(timeEnd)}结束",
  493. "慢 ${SportUtils.pace(SportUtils.calPace((max), 1))}",
  494. style: Theme.of(context).textTheme.bodyText1!,
  495. ),
  496. ),
  497. ),
  498. ),
  499. ],
  500. ),
  501. ),
  502. Padding(
  503. padding: const EdgeInsets.only(left: 20.0),
  504. child: Row(
  505. crossAxisAlignment: CrossAxisAlignment.end,
  506. children: [
  507. Expanded(
  508. flex: 4,
  509. child: Column(
  510. crossAxisAlignment: CrossAxisAlignment.start,
  511. children: [
  512. Text(
  513. "${DateFormat.toTime(totalTime)}",
  514. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  515. ),
  516. Text(
  517. "时长",
  518. style: Theme.of(context).textTheme.bodyText1!,
  519. )
  520. ],
  521. ),
  522. ),
  523. Expanded(
  524. flex: 3,
  525. child: Column(
  526. crossAxisAlignment: CrossAxisAlignment.start,
  527. children: [
  528. Text(
  529. "${met.toStringAsFixed(1)}",
  530. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  531. ),
  532. Text(
  533. "MET值",
  534. style: Theme.of(context).textTheme.bodyText1!,
  535. )
  536. ],
  537. ),
  538. ),
  539. Expanded(
  540. flex: 4,
  541. child: Column(
  542. crossAxisAlignment: CrossAxisAlignment.start,
  543. children: [
  544. Row(
  545. children: [
  546. Text(
  547. "${SportUtils.pace11(SportUtils.calPace(totalTime, totalDistance / 1000))}",
  548. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  549. ),
  550. Text(
  551. "′",
  552. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0),
  553. ),
  554. Text(
  555. "${SportUtils.pace12(SportUtils.calPace(totalTime, totalDistance / 1000))}",
  556. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  557. ),
  558. Text(
  559. "″",
  560. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0),
  561. ),
  562. ],
  563. ),
  564. Text(
  565. "配速(每公里用时)",
  566. style: Theme.of(context).textTheme.bodyText1!,
  567. )
  568. ],
  569. ),
  570. ),
  571. ],
  572. ),
  573. ),
  574. const SizedBox(
  575. height: 20.0,
  576. ),
  577. Padding(
  578. padding: const EdgeInsets.only(left: 20.0),
  579. child: Row(
  580. children: [
  581. Expanded(
  582. flex: 4,
  583. child: Column(
  584. crossAxisAlignment: CrossAxisAlignment.start,
  585. children: [
  586. Text(
  587. "$totalConsume",
  588. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  589. ),
  590. Text(
  591. "消耗(大卡)",
  592. style: Theme.of(context).textTheme.bodyText1!,
  593. )
  594. ],
  595. ),
  596. ),
  597. Expanded(
  598. flex: 3,
  599. child: Column(
  600. crossAxisAlignment: CrossAxisAlignment.start,
  601. children: [
  602. Text(
  603. totalStep == 0 ? "0" : "${totalStep ~/ (totalTime / 60)}",
  604. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  605. ),
  606. Text(
  607. "步频(步/分钟)",
  608. style: Theme.of(context).textTheme.bodyText1!,
  609. ),
  610. ],
  611. ),
  612. ),
  613. Expanded(
  614. flex: 4,
  615. child: Column(
  616. crossAxisAlignment: CrossAxisAlignment.start,
  617. children: [
  618. Text(
  619. "${totalTime == 0 ? 0 : (totalDistance / 1000.0 / totalTime * 3600).toStringAsFixed(1)}",
  620. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  621. ),
  622. Text(
  623. "时速(公里/小时)",
  624. style: Theme.of(context).textTheme.bodyText1!,
  625. )
  626. ],
  627. ),
  628. ),
  629. ],
  630. ),
  631. ),
  632. const SizedBox(
  633. height: 20.0,
  634. ),
  635. Center(
  636. child: Container(
  637. decoration: BoxDecoration(color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(radius)),
  638. margin: EdgeInsets.symmetric(vertical: 10.0),
  639. padding: EdgeInsets.fromLTRB(30.0, 2, 30.0, 0),
  640. child: Row(
  641. mainAxisSize: MainAxisSize.min,
  642. children: [
  643. Image.asset(
  644. "lib/assets/img/run_icon_today.png",
  645. color: Color(0xffC5C5C5),
  646. ),
  647. SizedBox(
  648. width: 12.0,
  649. ),
  650. Row(
  651. children: [
  652. Text(
  653. "步幅 ",
  654. style: Theme.of(context).textTheme.subtitle2!,
  655. ),
  656. Text(
  657. totalStep == 0 ? "0" : "${(totalDistance / totalStep * 100).toInt()}",
  658. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  659. ),
  660. Text(
  661. " 厘米",
  662. style: Theme.of(context).textTheme.bodyText1!,
  663. ),
  664. ],
  665. ),
  666. Container(
  667. width: 1,
  668. height: 20.0,
  669. color: Color(0xffdcdcdc),
  670. margin: EdgeInsets.symmetric(horizontal: 20.0),
  671. ),
  672. Row(
  673. children: [
  674. Text(
  675. "步数 ",
  676. style: Theme.of(context).textTheme.subtitle2!,
  677. ),
  678. Text(
  679. "${totalStep}",
  680. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 22.0, fontFamily: "DIN"),
  681. ),
  682. Text(
  683. " 步",
  684. style: Theme.of(context).textTheme.bodyText1!,
  685. ),
  686. ],
  687. ),
  688. ],
  689. ),
  690. ),
  691. ),
  692. ],
  693. ),
  694. ),
  695. ),
  696. if (_pointsKmTime.isNotEmpty == true)
  697. SliverToBoxAdapter(
  698. child: BoxWidget(
  699. title: "配速",
  700. icon: "run_icon_tile1.png",
  701. child: Column(
  702. mainAxisSize: MainAxisSize.min,
  703. children: [
  704. if (marathonHalf > 0)
  705. Align(
  706. alignment: Alignment.centerLeft,
  707. child: Padding(
  708. padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
  709. child: Column(
  710. crossAxisAlignment: CrossAxisAlignment.start,
  711. children: [
  712. Text(
  713. "${DateFormat.toTime(marathonHalf)}",
  714. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"),
  715. ),
  716. Text(
  717. "半马总用时",
  718. style: Theme.of(context).textTheme.bodyText1!,
  719. )
  720. ],
  721. mainAxisSize: MainAxisSize.min,
  722. ),
  723. ),
  724. ),
  725. if (marathonAll > 0)
  726. Align(
  727. alignment: Alignment.centerLeft,
  728. child: Padding(
  729. padding: const EdgeInsets.only(bottom: 16.0, top: 8.0),
  730. child: Column(
  731. crossAxisAlignment: CrossAxisAlignment.start,
  732. children: [
  733. Text(
  734. "${DateFormat.toTime(marathonHalf)}",
  735. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"),
  736. ),
  737. Text(
  738. "全马总用时",
  739. style: Theme.of(context).textTheme.bodyText1!,
  740. )
  741. ],
  742. mainAxisSize: MainAxisSize.min,
  743. ),
  744. ),
  745. ),
  746. CustomPaint(
  747. foregroundPainter: AvgPainter(context, avg * 1.0 / max * 0.9, avg, _pointsKmTime.length, MediaQuery.of(context).textScaleFactor),
  748. child: Column(
  749. crossAxisAlignment: CrossAxisAlignment.start,
  750. children: [
  751. Row(
  752. mainAxisSize: MainAxisSize.max,
  753. crossAxisAlignment: CrossAxisAlignment.start,
  754. children: [
  755. Text(
  756. "公里",
  757. style: Theme.of(context).textTheme.bodyText1!,
  758. ),
  759. Expanded(
  760. child: Container(height: 18,),
  761. ),
  762. Text(
  763. "配速",
  764. style: Theme.of(context).textTheme.bodyText1!,
  765. ),
  766. const SizedBox(
  767. width: 20.0,
  768. ),
  769. ],
  770. ),
  771. const SizedBox(
  772. height: 12.0,
  773. ),
  774. Column(
  775. crossAxisAlignment: CrossAxisAlignment.start,
  776. children: _kmList,
  777. ),
  778. // if (_pointsKmTime.length > 0 && totalTime > 0)
  779. // Row(
  780. // mainAxisAlignment: MainAxisAlignment.spaceAround,
  781. // children: [
  782. // RichText(
  783. // text: TextSpan(children: [TextSpan(text: "$maxKm公里累计用时 ", style: Theme.of(context).textTheme.bodyText1!), TextSpan(text: "${DateFormat.toTime(kmTime)}", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Theme.of(context).accentColor))]),
  784. // ),
  785. // if (totalTime - kmTime > 0)
  786. // RichText(
  787. // text: TextSpan(children: [TextSpan(text: "最后不足1公里用时 ", style: Theme.of(context).textTheme.bodyText1!), TextSpan(text: "${DateFormat.toTime((totalTime) - kmTime)}", style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Theme.of(context).accentColor))]),
  788. // ),
  789. // ],
  790. // ),
  791. if (_pointsKmTime.length > 0 && totalTime > 0 && (totalTime - kmTime > 20))
  792. Padding(
  793. padding: const EdgeInsets.fromLTRB(26, 5, 0, 15),
  794. child: Container(
  795. height: 18.0,
  796. child: Text(
  797. "最后不足1公里用时 ${DateFormat.toTime((totalTime) - kmTime)}",
  798. style: Theme.of(context).textTheme.bodyText1?.copyWith(fontSize: 11),
  799. )),
  800. ),
  801. const SizedBox(
  802. height: 16.0,
  803. ),
  804. ],
  805. ),
  806. ),
  807. ],
  808. )),
  809. ),
  810. SliverToBoxAdapter(
  811. child: BoxWidget(
  812. title: "步频",
  813. icon: "run_icon_tile2.png",
  814. child: Column(
  815. children: [
  816. Row(
  817. children: [
  818. Column(
  819. crossAxisAlignment: CrossAxisAlignment.start,
  820. children: [
  821. Text(
  822. "$stepAvg",
  823. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"),
  824. ),
  825. Text(
  826. "平均步频(步/分钟)",
  827. style: Theme.of(context).textTheme.bodyText1!,
  828. )
  829. ],
  830. ),
  831. const SizedBox(
  832. width: 37.0,
  833. ),
  834. Column(
  835. crossAxisAlignment: CrossAxisAlignment.start,
  836. children: [
  837. Text(
  838. "$stepMax",
  839. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"),
  840. ),
  841. Text(
  842. "最高步频(步/分钟)",
  843. style: Theme.of(context).textTheme.bodyText1!,
  844. )
  845. ],
  846. ),
  847. ],
  848. ),
  849. RunChart(
  850. values: stepRateList.map((e) => e.toDouble()).toList(),
  851. gradient: false,
  852. ),
  853. ],
  854. )),
  855. ),
  856. SliverToBoxAdapter(
  857. child: BoxWidget(
  858. title: "运动海拔",
  859. icon: "run_icon_tile3.png",
  860. child: Column(
  861. children: [
  862. Row(
  863. children: [
  864. Column(
  865. crossAxisAlignment: CrossAxisAlignment.start,
  866. children: [
  867. Text(
  868. "${(totalAltitude).toStringAsFixed(1)}",
  869. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 25.0, fontFamily: "DIN"),
  870. ),
  871. Text(
  872. "累计爬升(米)",
  873. style: Theme.of(context).textTheme.bodyText1!,
  874. )
  875. ],
  876. ),
  877. ],
  878. ),
  879. RunChart(
  880. values: altitudeList.map((e) => e.toDouble()).toList(),
  881. // values: [-10,20,0,0,1,2,3,4,5,6,8,9,8,10,-6,-5,-20, 20, 30],
  882. gradient: true,
  883. ),
  884. ],
  885. )),
  886. ),
  887. SliverToBoxAdapter(
  888. child: Container(
  889. height: 50,
  890. ),
  891. ),
  892. ],
  893. ),
  894. ),
  895. ),
  896. );
  897. }
  898. }
  899. class AvgPainter extends CustomPainter {
  900. final BuildContext context;
  901. final int pace;
  902. final double progress;
  903. final int length;
  904. final double dpi;
  905. AvgPainter(this.context, this.progress, this.pace, this.length, this.dpi);
  906. final Paint _paint = Paint()
  907. ..color = Color(0xff999999)
  908. ..strokeWidth = 0.5
  909. ..isAntiAlias = true;
  910. final ui.ParagraphStyle _valueStyle = ui.ParagraphStyle(
  911. textAlign: TextAlign.left,
  912. fontSize: 10,
  913. );
  914. @override
  915. void paint(Canvas canvas, Size size) {
  916. if (length == 0) return;
  917. double dpr = ui.window.devicePixelRatio;
  918. double p = math.min(.9, progress);
  919. double startX = 26 + (size.width - 86) * p;
  920. double startY = 34;
  921. double endY = size.height - 8 * dpr;
  922. var dashWidth = 3;
  923. var dashSpace = 3;
  924. final space = (dashSpace + dashWidth);
  925. double height = (endY - startY);
  926. int split = length ~/ 5;
  927. double lineHeight = (height - split * 38 / 2.0 * dpr) / length;
  928. double lineSplit = lineHeight * 5;
  929. // print("111111111111111 height: $height, sss: $split lineHeight: $lineHeight");
  930. // canvas.drawRect(Rect.fromLTWH(26, startY + lineHeight * 5, 100, 38), _paint);
  931. // canvas.clipRect(Rect.fromLTWH(26, startY + lineHeight * 5, 1000, 38));
  932. double ss = 0;
  933. while (startY < endY) {
  934. if (ss > lineSplit - 5) {
  935. startY += 42;
  936. ss = 0;
  937. }
  938. canvas.drawLine(Offset(startX, startY), Offset(startX, math.min(startY + dashWidth, endY)), _paint);
  939. startY += space;
  940. ss += space;
  941. }
  942. var text = TextPainter(
  943. textAlign: TextAlign.center,
  944. text: TextSpan(children: <InlineSpan>[
  945. TextSpan(text: "平均配速", style: Theme.of(context).textTheme.bodyText1!),
  946. TextSpan(text: "\n${SportUtils.pace(SportUtils.calPace((this.pace), 1))}", style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 10)),
  947. ]),
  948. textDirection: TextDirection.ltr)
  949. ..layout(maxWidth: size.width);
  950. text.paint(canvas, Offset(startX - text.minIntrinsicWidth / 2, 0));
  951. }
  952. @override
  953. bool shouldRepaint(covariant CustomPainter oldDelegate) {
  954. return false;
  955. }
  956. }