run_index.dart 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. import 'dart:io';
  2. import 'package:cached_network_image/cached_network_image.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_easyrefresh/easy_refresh.dart';
  5. import 'package:provider/provider.dart';
  6. import 'package:sport/pages/run/run_detail_page.dart';
  7. import 'package:sport/pages/run/run_list.dart';
  8. import 'package:sport/pages/run/run_page.dart';
  9. import 'package:sport/pages/run/run_start.dart';
  10. import 'package:sport/pages/run/run_target_page.dart';
  11. import 'package:sport/pages/run/setting_page.dart';
  12. import 'package:sport/provider/lib/provider_widget.dart';
  13. import 'package:sport/provider/lib/view_state_lifecycle.dart';
  14. import 'package:sport/provider/sport_index_model.dart';
  15. import 'package:sport/router/navigator_util.dart';
  16. import 'package:sport/utils/DateFormat.dart';
  17. import 'package:sport/utils/click.dart';
  18. import 'package:sport/utils/sport_utils.dart';
  19. import 'package:sport/widgets/decoration.dart';
  20. import 'package:sport/widgets/game_run.dart';
  21. import 'package:sport/widgets/image.dart';
  22. import 'package:sport/widgets/misc.dart';
  23. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  24. class RunIndexPage extends StatefulWidget {
  25. @override
  26. State<StatefulWidget> createState() {
  27. return _RunIndexState();
  28. }
  29. }
  30. bool runDelete = false;
  31. class _RunIndexState extends ViewStateLifecycle<RunIndexPage, SportIndexModel> with AutomaticKeepAliveClientMixin, RunSetting, TickerProviderStateMixin, WidgetsBindingObserver {
  32. double _runTargetDuration = 0;
  33. double _runTargetKm = 0;
  34. double _left = 0, _top = -25, _right = 0, _bottom = 0;
  35. double _progress = 1.0;
  36. late AnimationController _animationController;
  37. late Animation _animation;
  38. late AnimationController _animationController1;
  39. late Animation _animation1;
  40. ValueNotifier<int> _imageIndex = ValueNotifier(0);
  41. @override
  42. void initState() {
  43. super.initState();
  44. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  45. _initImages();
  46. });
  47. }
  48. @override
  49. void dispose() {
  50. _animationController.dispose();
  51. super.dispose();
  52. }
  53. _initImages() async {
  54. final int imageCount = 60;
  55. final int maxTime = 28 * imageCount;
  56. _animationController1 = new AnimationController(duration: Duration(milliseconds: maxTime), vsync: this);
  57. _animation1 = new Tween<double>(begin: 0, end: imageCount.toDouble()).animate(_animationController1)
  58. ..addListener(() {
  59. _imageIndex.value = _animation1.value.floor() % imageCount;
  60. });
  61. _animationController1.repeat();
  62. }
  63. @override
  64. SportIndexModel createModel() => Provider.of<SportIndexModel>(context, listen: false);
  65. @override
  66. Widget build(BuildContext context) {
  67. final Color priColor = const Color(0xffFFDD00);
  68. return Scaffold(
  69. body: Stack(
  70. children: [
  71. Positioned(
  72. left: 0,
  73. top: MediaQuery.of(context).padding.top + 90.0,
  74. child: Stack(
  75. clipBehavior: Clip.none,
  76. children: [
  77. Positioned(
  78. child: Opacity(opacity: _progress, child: Image.asset("lib/assets/img/run_bg_2.png")),
  79. top: _top,
  80. left: _left,
  81. ),
  82. Image.asset("lib/assets/img/run_bg_1.png"),
  83. ],
  84. ),
  85. ),
  86. SafeArea(
  87. child: ProviderWidget<SportIndexModel>(
  88. model: this.model,
  89. autoDispose: false,
  90. builder: (BuildContext context, SportIndexModel model, Widget? child) {
  91. return EasyRefresh.custom(
  92. onRefresh: () async {
  93. model.refresh();
  94. },
  95. header: buildClassicalHeader(triggerDistance: 120.0, extent: 120.0),
  96. slivers: <Widget>[
  97. SliverFillRemaining(
  98. child: Column(
  99. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  100. children: [
  101. Expanded(
  102. child: Center(
  103. child: GestureDetector(
  104. behavior: HitTestBehavior.opaque,
  105. onTap: () {
  106. NavigatorUtil.goPage(context, (context) => RunListPage()).then((value) {
  107. if (runDelete == true) {
  108. model.refresh();
  109. }
  110. });
  111. UmengCommonSdk.onEvent("run_history", {});
  112. },
  113. child: Column(
  114. mainAxisSize: MainAxisSize.min,
  115. children: [
  116. const SizedBox(
  117. height: 120,
  118. ),
  119. Text(
  120. "${formatNum((model.data?.sum?.distanceJog ?? 0) / 1000, 2)}",
  121. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 60.0, fontFamily: "DIN"),
  122. strutStyle: fixedLine,
  123. ),
  124. const SizedBox(
  125. height: 8,
  126. ),
  127. Row(
  128. mainAxisSize: MainAxisSize.min,
  129. children: [
  130. Text(
  131. "运动总公里",
  132. style: Theme.of(context).textTheme.subtitle1!,
  133. ),
  134. const SizedBox(
  135. width: 5,
  136. ),
  137. arrowRight(),
  138. ],
  139. ),
  140. ],
  141. ),
  142. ),
  143. ),
  144. ),
  145. Column(
  146. children: [
  147. Row(
  148. children: [
  149. Expanded(
  150. child: GestureDetector(
  151. onTap: () async {
  152. await NavigatorUtil.goPage(
  153. context,
  154. (context) => SettingPage(
  155. run: true,
  156. ));
  157. loadSetting().then((value) => setState(() {}));
  158. UmengCommonSdk.onEvent("run_setting", {});
  159. },
  160. child: Column(
  161. children: [
  162. Container(
  163. width: 44,
  164. height: 44,
  165. padding: const EdgeInsets.all(11.0),
  166. margin: const EdgeInsets.only(bottom: 5),
  167. decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
  168. child: Image.asset("lib/assets/img/setgoals_icon_set.png"),
  169. ),
  170. Text(
  171. "设置",
  172. style: Theme.of(context).textTheme.subtitle2!,
  173. )
  174. ],
  175. ),
  176. ),
  177. ),
  178. Expanded(
  179. child: GestureDetector(
  180. onTap: throttle(() async {
  181. bool check = await runCheck(context);
  182. if (check == true) {
  183. var result = await NavigatorUtil.goPage(
  184. context,
  185. (context) => RunStartPage(
  186. runTargetDuration: _runTargetDuration,
  187. runTargetKm: _runTargetKm,
  188. ));
  189. if (result == true) {
  190. model.refresh();
  191. }
  192. }
  193. }),
  194. child: Hero(
  195. tag: "run",
  196. child: Container(
  197. width: 110.0,
  198. height: 110.0,
  199. margin: const EdgeInsets.all(4),
  200. decoration: BoxDecoration(shape: BoxShape.circle, color: priColor, boxShadow: [BoxShadow(offset: Offset(0.0, 20.0), blurRadius: 15.0, spreadRadius: 0, color: priColor.withOpacity(0.21))]),
  201. child: Center(
  202. child: Column(
  203. mainAxisSize: MainAxisSize.min,
  204. children: [
  205. ValueListenableBuilder<int>(
  206. valueListenable: _imageIndex,
  207. builder: (context, snapshot, _) {
  208. return IndexedStack(
  209. index: snapshot,
  210. children: List<Image>.generate(
  211. 60,
  212. (e) => Image.asset(
  213. "assets/run/000${e.toString().padLeft(2, '0')}.png",
  214. width: 45.0,
  215. height: 45.0,
  216. )).toList(),
  217. );
  218. }),
  219. const SizedBox(
  220. height: 5,
  221. ),
  222. Text(
  223. "跑步",
  224. style: Theme.of(context).textTheme.subtitle1!,
  225. ),
  226. ],
  227. ),
  228. )),
  229. ),
  230. ),
  231. ),
  232. Expanded(
  233. child: GestureDetector(
  234. onTap: () async {
  235. var result = await NavigatorUtil.goPage(context, (context) => RunTargetPage(_runTargetKm > 0 ? _runTargetKm : runTargetKm, _runTargetDuration > 0 ? _runTargetDuration : runTargetDuration));
  236. if (result != null) {
  237. setState(() {
  238. _runTargetKm = result[0];
  239. _runTargetDuration = result[1];
  240. });
  241. UmengCommonSdk.onEvent("run_target", {});
  242. }
  243. },
  244. child: Column(
  245. children: [
  246. Container(
  247. width: 44,
  248. height: 44,
  249. margin: const EdgeInsets.only(bottom: 5),
  250. decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white),
  251. child: Image.asset("lib/assets/img/rung_icon_target.png"),
  252. ),
  253. Text(
  254. _runTargetKm > 0
  255. ? "目标 ${(_runTargetKm / 1000).toStringAsFixed(2)} 公里"
  256. : _runTargetDuration > 0
  257. ? "目标 ${(_runTargetDuration / 60).toStringAsFixed(2)} 分钟"
  258. : "设定目标",
  259. style: Theme.of(context).textTheme.subtitle2!,
  260. ),
  261. ],
  262. ),
  263. ),
  264. ),
  265. ],
  266. ),
  267. const SizedBox(
  268. height: 10,
  269. ),
  270. model.data?.jogLast != null
  271. ? Container(
  272. margin: const EdgeInsets.all(12.0),
  273. padding: const EdgeInsets.symmetric(horizontal: 17.0),
  274. decoration: circular(),
  275. child: GestureDetector(
  276. onTap: () {
  277. int id = model.data?.jogLastId ?? 0;
  278. if (id == 0) {
  279. NavigatorUtil.goPage(context, (context) => RunListPage());
  280. } else {
  281. NavigatorUtil.goPage(
  282. context,
  283. (context) => RunDetailPage(
  284. id: id,
  285. ));
  286. }
  287. },
  288. behavior: HitTestBehavior.opaque,
  289. child: Column(
  290. children: [
  291. Padding(
  292. padding: const EdgeInsets.symmetric(vertical: 12.0),
  293. child: Row(
  294. children: [
  295. Image.asset("lib/assets/img/run_icon_today.png"),
  296. const SizedBox(
  297. width: 5,
  298. ),
  299. Text(
  300. "最近记录",
  301. style: Theme.of(context).textTheme.subtitle1!,
  302. ),
  303. Expanded(child: Container()),
  304. Text(
  305. "${SportUtils.toDateTimeDay(DateTime.parse(model.data?.jogLast?.jogRecord?.begin ?? ""))} ${SportUtils.toEndTime(DateTime.parse(model.data?.jogLast?.jogRecord?.begin ?? ""))}-${SportUtils.toEndTime(DateTime.parse(model.data?.jogLast?.jogRecord?.end ?? ""))}",
  306. style: Theme.of(context).textTheme.bodyText1!,
  307. ),
  308. ],
  309. ),
  310. ),
  311. Divider(
  312. height: 1,
  313. ),
  314. Padding(
  315. padding: const EdgeInsets.symmetric(vertical: 14.0),
  316. child: Row(
  317. crossAxisAlignment: CrossAxisAlignment.end,
  318. children: [
  319. ClipRRect(
  320. child: model.data?.jogLast?.jogRecord?.trackThumb?.startsWith("/") == true
  321. ? Image.file(
  322. File(model.data?.jogLast?.jogRecord?.trackThumb ?? ""),
  323. fit: BoxFit.cover,
  324. width: 70.0,
  325. height: 70.0,
  326. )
  327. : CachedNetworkImage(
  328. imageUrl: "${model.data?.jogLast?.jogRecord?.trackThumb}",
  329. fit: BoxFit.cover,
  330. width: 70.0,
  331. height: 70.0,
  332. ),
  333. borderRadius: BorderRadius.circular(6),
  334. ),
  335. const SizedBox(
  336. width: 14.0,
  337. ),
  338. Expanded(
  339. child: Column(
  340. crossAxisAlignment: CrossAxisAlignment.start,
  341. children: [
  342. Row(
  343. children: [
  344. Text(
  345. "${formatNum((model.data?.jogLast?.distance ?? 0) / 1000.0, 2)}",
  346. style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 30.0, fontFamily: "DIN"),
  347. strutStyle: fixedLine,
  348. ),
  349. Text(
  350. " 公里",
  351. style: Theme.of(context).textTheme.bodyText1!,
  352. )
  353. ],
  354. crossAxisAlignment: CrossAxisAlignment.end,
  355. ),
  356. const SizedBox(
  357. height: 16,
  358. ),
  359. Row(
  360. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  361. children: [
  362. Row(
  363. children: [
  364. Image.asset(
  365. "lib/assets/img/trajectory_icon_1.png",
  366. color: Color(0xff999999),
  367. ),
  368. const SizedBox(
  369. width: 7,
  370. ),
  371. Text(
  372. "${DateFormat.toTime(model.data?.jogLast?.duration ?? 0)}",
  373. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontFamily: "DIN", fontSize: 16.0),
  374. )
  375. ],
  376. ),
  377. Row(
  378. children: [
  379. Image.asset(
  380. "lib/assets/img/trajectory_icon_2.png",
  381. color: Color(0xff999999),
  382. ),
  383. const SizedBox(
  384. width: 7,
  385. ),
  386. Row(
  387. children: [
  388. Text(
  389. "${SportUtils.pace11(SportUtils.calPace(model.data?.jogLast?.duration ?? 0, (model.data?.jogLast?.distance ?? 0) / 1000))}",
  390. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 16.0, fontFamily: "DIN"),
  391. ),
  392. Text(
  393. "′",
  394. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 16.0),
  395. ),
  396. Text(
  397. "${SportUtils.pace12(SportUtils.calPace(model.data?.jogLast?.duration ?? 0, (model.data?.jogLast?.distance ?? 0) / 1000))}",
  398. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 16.0, fontFamily: "DIN"),
  399. ),
  400. Text(
  401. "″",
  402. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 16.0),
  403. ),
  404. ],
  405. ),
  406. ],
  407. ),
  408. Row(
  409. children: [
  410. Image.asset(
  411. "lib/assets/img/trajectory_icon_3.png",
  412. color: Color(0xff999999),
  413. ),
  414. const SizedBox(
  415. width: 7,
  416. ),
  417. Text(
  418. "${model.data?.jogLast?.consume ?? 0}",
  419. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontFamily: "DIN", fontSize: 16.0),
  420. ),
  421. Text(
  422. " 大卡",
  423. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 10.0),
  424. ),
  425. ],
  426. ),
  427. ],
  428. ),
  429. ],
  430. ),
  431. ),
  432. ],
  433. ),
  434. ),
  435. // Padding(
  436. // padding: const EdgeInsets.symmetric(vertical: 12.0),
  437. // child: ((model.data?.today?.distanceJog ?? 0) == 0)
  438. // ? Padding(
  439. // padding: const EdgeInsets.all(20.0),
  440. // child: Text(
  441. // "今日还未开始运动",
  442. // style: Theme.of(context).textTheme.bodyText1!,
  443. // ),
  444. // )
  445. // : Row(
  446. // children: [
  447. // Expanded(
  448. // child: Column(
  449. // children: [
  450. // Text(
  451. // "${formatNum((model.data?.today?.distanceJog ?? 0) / 1000, 2)}",
  452. // style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  453. // ),
  454. // const SizedBox(
  455. // height: 5,
  456. // ),
  457. // Text(
  458. // "公里",
  459. // style: Theme.of(context).textTheme.bodyText1!,
  460. // )
  461. // ],
  462. // ),
  463. // ),
  464. // Expanded(
  465. // child: Column(
  466. // children: [
  467. // Text(
  468. // "${DateFormat.toTime(model.data?.today?.durationJog ?? 0)}",
  469. // style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  470. // ),
  471. // const SizedBox(
  472. // height: 5,
  473. // ),
  474. // Text(
  475. // "时长",
  476. // style: Theme.of(context).textTheme.bodyText1!,
  477. // )
  478. // ],
  479. // ),
  480. // ),
  481. // Expanded(
  482. // child: Column(
  483. // children: [
  484. // Text(
  485. // "${SportUtils.pace(SportUtils.calPace(model.data?.today?.durationJog ?? 0, (model.data?.today?.distanceJog ?? 0) / 1000))}",
  486. // style: Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 20.0, fontFamily: "DIN"),
  487. // ),
  488. // const SizedBox(
  489. // height: 5,
  490. // ),
  491. // Text(
  492. // "配速(每公里用时)",
  493. // style: Theme.of(context).textTheme.bodyText1!,
  494. // )
  495. // ],
  496. // ),
  497. // ),
  498. // ],
  499. // ),
  500. // ),
  501. ],
  502. ),
  503. ),
  504. )
  505. : Container(
  506. margin: const EdgeInsets.all(12.0),
  507. padding: const EdgeInsets.symmetric(horizontal: 17.0),
  508. decoration: circular(),
  509. child: Column(
  510. children: [
  511. Padding(
  512. padding: const EdgeInsets.symmetric(vertical: 12.0),
  513. child: Row(
  514. children: [
  515. Image.asset("lib/assets/img/run_icon_today.png"),
  516. const SizedBox(
  517. width: 5,
  518. ),
  519. Text(
  520. "最近记录",
  521. style: Theme.of(context).textTheme.subtitle1!,
  522. ),
  523. Expanded(child: Container()),
  524. ],
  525. ),
  526. ),
  527. Divider(
  528. height: 1,
  529. ),
  530. Padding(
  531. padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 20.0),
  532. child: Text(
  533. "还未开始运动",
  534. style: Theme.of(context).textTheme.bodyText1!,
  535. ),
  536. ),
  537. ],
  538. ),
  539. ),
  540. ],
  541. )
  542. ],
  543. ),
  544. )
  545. ]);
  546. }),
  547. ),
  548. ],
  549. ),
  550. );
  551. }
  552. @override
  553. bool get wantKeepAlive => true;
  554. }