map_replay_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/scheduler.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:flutter_screen_recording/flutter_screen_recording.dart';
  7. import 'package:image_gallery_saver/image_gallery_saver.dart';
  8. import 'package:provider/provider.dart';
  9. import 'package:share_plus/share_plus.dart';
  10. import 'package:shared_preferences/shared_preferences.dart';
  11. import 'package:sport/pages/run/location.dart';
  12. import 'package:sport/pages/run/map.dart';
  13. import 'package:sport/pages/run/run_page.dart';
  14. import 'package:sport/provider/user_model.dart';
  15. import 'package:sport/utils/DateFormat.dart';
  16. import 'package:sport/utils/click.dart';
  17. import 'package:sport/utils/sport_utils.dart';
  18. import 'package:sport/utils/toast.dart';
  19. import 'package:sport/widgets/button_primary.dart';
  20. import 'package:sport/widgets/decoration.dart';
  21. import 'package:sport/widgets/dialog/request_dialog.dart';
  22. import 'package:sport/widgets/image.dart';
  23. import 'package:sport/widgets/misc.dart';
  24. class MapReplayPage extends StatefulWidget {
  25. final double distance;
  26. final int duration;
  27. final int kcal;
  28. final int runMapType;
  29. final bool runMapKm;
  30. final List<Location> points;
  31. final Map<int, Location> pointsKm;
  32. final int begin;
  33. final int showType;
  34. const MapReplayPage({Key? key, this.showType = 0, this.distance = 0, this.duration = 0, this.kcal = 0, this.runMapType = 0, this.runMapKm = true, required this.points, required this.pointsKm, this.begin = 0}) : super(key: key);
  35. @override
  36. State<StatefulWidget> createState() => MapReplayState();
  37. }
  38. class MapReplayState extends State<MapReplayPage> {
  39. final GlobalKey<MapState> mapKey = GlobalKey();
  40. ValueNotifier<MapNotification> _notifierMapNotification = ValueNotifier(MapNotification(0, 0, 0));
  41. double weight = 60;
  42. bool _finish = false;
  43. List<Location> points = [];
  44. String? path;
  45. final int second = 4;
  46. @override
  47. void initState() {
  48. super.initState();
  49. SystemChrome.setEnabledSystemUIOverlays([]);
  50. _loadWeight();
  51. SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
  52. requestPermissions().then((value) {
  53. if (value) {
  54. } else {
  55. ToastUtil.show("录制失败!");
  56. Navigator.pop(context);
  57. }
  58. });
  59. });
  60. }
  61. _loadWeight() async {
  62. var preferences = await SharedPreferences.getInstance();
  63. weight = preferences.getDouble("weight") ?? 0.0;
  64. }
  65. Future<bool> requestPermissions() async {
  66. // Map<Permission, PermissionStatus> statuses = await [
  67. // Permission.photos,
  68. // Permission.storage,
  69. // ].request();
  70. // if (statuses.values.any((element) => element.isGranted != true)) {
  71. // ToastUtil.show("请授权后使用该功能!");
  72. // Navigator.maybeOf(context);
  73. // return;
  74. // }
  75. //
  76. setState(() {
  77. this.points = widget.points;
  78. });
  79. await Future.delayed(Duration(seconds: 1));
  80. bool start = await FlutterScreenRecording.startRecordScreen("趣动户外运动_${DateTime.now()}");
  81. if (start) {
  82. await Future.delayed(Duration(seconds: 2));
  83. mapKey.currentState?.initAnimation();
  84. }
  85. return start;
  86. }
  87. _finishRecord() async {
  88. await Future.delayed(const Duration(milliseconds: 2000));
  89. path = await FlutterScreenRecording.stopRecordScreen;
  90. setState(() {
  91. _finish = true;
  92. });
  93. }
  94. @override
  95. void dispose() {
  96. super.dispose();
  97. SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]);
  98. FlutterScreenRecording.stopRecordScreen.then((value) {
  99. if (value.isNotEmpty == true) {
  100. File(value).deleteSync();
  101. }
  102. });
  103. if (path != null) {
  104. File(path!).deleteSync();
  105. }
  106. }
  107. @override
  108. Widget build(BuildContext context) {
  109. var map = points.isNotEmpty == true
  110. ? NotificationListener<MapNotification>(
  111. onNotification: (map) {
  112. _notifierMapNotification.value = map;
  113. if (map.finish == true) {
  114. if (_finish == false) {
  115. _finishRecord();
  116. }
  117. }
  118. return false;
  119. },
  120. child: MapWidget(
  121. key: mapKey,
  122. distance: widget.distance,
  123. runMapType: widget.runMapType,
  124. runMapKm: widget.runMapKm,
  125. points: points,
  126. pointsKm: widget.pointsKm,
  127. record: true,
  128. anim: true,
  129. showType: widget.showType,
  130. ),
  131. )
  132. : Container(
  133. color: Colors.black,
  134. );
  135. return AnnotatedRegion<SystemUiOverlayStyle>(
  136. value: SystemUiOverlayStyle.light,
  137. child: Scaffold(
  138. backgroundColor: Colors.black,
  139. body: Stack(
  140. children: [
  141. map,
  142. Container(
  143. height: 250.0,
  144. decoration: BoxDecoration(
  145. gradient: LinearGradient(
  146. begin: Alignment.topCenter,
  147. end: Alignment.bottomCenter,
  148. colors: [
  149. Colors.black.withOpacity(.62),
  150. Colors.black.withOpacity(.0),
  151. ],
  152. )),
  153. child: Column(
  154. children: [
  155. Container(
  156. height: 70,
  157. child: _finish
  158. ? Padding(
  159. padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 0),
  160. child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
  161. GestureDetector(
  162. onTap: () {
  163. Navigator.maybePop(context);
  164. },
  165. child: Container(width: 40, height: 40, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)), child: arrowBack()),
  166. ),
  167. Row(
  168. children: [
  169. GestureDetector(
  170. onTap: throttle(() async {
  171. if (path?.isNotEmpty == true) {
  172. var result = await request(context, () async {
  173. await Future.delayed(Duration(seconds: 1));
  174. final result = await ImageGallerySaver.saveFile(path!);
  175. return result != null && result["isSuccess"] == true;
  176. });
  177. if (result == true) {
  178. showDialog(
  179. context: context,
  180. barrierDismissible: true,
  181. builder: (BuildContext context) => Dialog(
  182. child: Container(
  183. padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 25.0),
  184. decoration: circular(),
  185. child: Column(
  186. mainAxisSize: MainAxisSize.min,
  187. children: [
  188. Center(
  189. child: Text(
  190. "视频已保存至相册",
  191. style: Theme.of(context).textTheme.headline1,
  192. ),
  193. ),
  194. Padding(
  195. padding: const EdgeInsets.only(top: 20.0),
  196. child: PrimaryButton(
  197. width: 120,
  198. callback: () {
  199. Navigator.maybePop(context);
  200. },
  201. content: "知道了"),
  202. ),
  203. ],
  204. ),
  205. ),
  206. ));
  207. }
  208. }
  209. }),
  210. child: Container(
  211. height: 40,
  212. padding: EdgeInsets.symmetric(horizontal: 30.0),
  213. decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(50.0)),
  214. child: Row(
  215. children: [
  216. Image.asset(
  217. "lib/assets/img/topbar_icon_trajectory_save.png",
  218. width: 16,
  219. height: 16,
  220. ),
  221. const SizedBox(
  222. width: 7,
  223. ),
  224. Text(
  225. "保存(${widget.showType == 1 ? "20秒" : "${widget.distance ~/ 1000 * second + 4}秒"})",
  226. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 14),
  227. )
  228. ],
  229. )),
  230. ),
  231. const SizedBox(
  232. width: 12.0,
  233. ),
  234. GestureDetector(
  235. onTap: throttle(() async {
  236. if (path?.isNotEmpty == true) {
  237. ToastUtil.show("正在分享,请稍候...");
  238. Share.shareFiles([path!], subject: "我的户外跑轨迹");
  239. }
  240. // showModalBottomSheet(
  241. // context: context,
  242. // backgroundColor: Colors.white,
  243. // elevation: 10,
  244. // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  245. // builder: (context) => MenuShareBottomContent(
  246. // "Video",
  247. // hasDownload: false,
  248. // file: path,
  249. // app: false,
  250. // ),
  251. // );
  252. }),
  253. child: Container(
  254. height: 40,
  255. padding: EdgeInsets.symmetric(horizontal: 30.0),
  256. decoration: BoxDecoration(color: Color(0xffFFDD00), borderRadius: BorderRadius.circular(50.0)),
  257. child: Row(
  258. children: [
  259. Image.asset(
  260. "lib/assets/img/bbs_icon_share.png",
  261. width: 16,
  262. height: 16,
  263. ),
  264. const SizedBox(
  265. width: 7,
  266. ),
  267. Text(
  268. "分享",
  269. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 14),
  270. )
  271. ],
  272. )),
  273. ),
  274. ],
  275. ),
  276. ]),
  277. )
  278. : Container(),
  279. ),
  280. Padding(
  281. padding: const EdgeInsets.all(30.0),
  282. child: Row(
  283. children: [
  284. Consumer<UserModel>(
  285. builder: (_, model, __) => Row(
  286. children: [
  287. Container(
  288. margin: const EdgeInsets.fromLTRB(0.0, 0, 12, 0),
  289. padding: const EdgeInsets.all(2),
  290. decoration: BoxDecoration(color: Colors.white, shape: BoxShape.circle),
  291. child: CircleAvatar(
  292. backgroundColor: Colors.black26,
  293. backgroundImage: userAvatarProvider(model.user.avatar),
  294. radius: 20.0,
  295. ),
  296. ),
  297. Text(model.user.name, style: Theme.of(context).textTheme.headline4!),
  298. ],
  299. )),
  300. Row(
  301. mainAxisAlignment: MainAxisAlignment.end,
  302. children: [
  303. Image.asset(
  304. "lib/assets/img/logo_img_white_30.png",
  305. width: 30.0,
  306. height: 30.0,
  307. ),
  308. const SizedBox(width: 7),
  309. Text("趣动", style: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white)),
  310. ],
  311. )
  312. ],
  313. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  314. ),
  315. )
  316. ],
  317. ),
  318. ),
  319. IgnorePointer(
  320. child: Align(
  321. alignment: Alignment.bottomCenter,
  322. child: Container(
  323. height: 250.0,
  324. alignment: Alignment.bottomCenter,
  325. padding: const EdgeInsets.all(30.0),
  326. decoration: BoxDecoration(
  327. gradient: LinearGradient(
  328. begin: Alignment.topCenter,
  329. end: Alignment.bottomCenter,
  330. colors: [
  331. Colors.black.withOpacity(.0),
  332. Colors.black,
  333. ],
  334. )),
  335. child: ValueListenableBuilder<MapNotification>(
  336. valueListenable: _notifierMapNotification,
  337. builder: (context, data, __) {
  338. return Column(
  339. crossAxisAlignment: CrossAxisAlignment.start,
  340. mainAxisSize: MainAxisSize.min,
  341. children: [
  342. Row(
  343. children: [
  344. Container(
  345. constraints: BoxConstraints(minWidth: 72.0),
  346. child: Text(
  347. "${formatNum((data.distance) / 1000.0, 2)}",
  348. style: Theme.of(context).textTheme.headline4!.copyWith(fontSize: 50.0, fontFamily: "DIN"),
  349. strutStyle: fixedLine,
  350. ),
  351. ),
  352. Text(
  353. "公里",
  354. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Colors.white),
  355. ),
  356. Expanded(child: Container()),
  357. Text(
  358. "${SportUtils.toDateTimeFull(DateTime.fromMillisecondsSinceEpoch(widget.begin + (data.duration * 1000)))}",
  359. style: Theme.of(context).textTheme.subtitle2!.copyWith(color: Colors.white),
  360. ),
  361. ],
  362. crossAxisAlignment: CrossAxisAlignment.end,
  363. ),
  364. const Divider(
  365. color: Colors.white,
  366. height: 40,
  367. ),
  368. Row(
  369. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  370. children: [
  371. Container(
  372. constraints: BoxConstraints(minWidth: 88.0),
  373. child: Row(
  374. children: [
  375. Image.asset("lib/assets/img/trajectory_icon_1.png"),
  376. const SizedBox(
  377. width: 7,
  378. ),
  379. Text(
  380. "${DateFormat.toTime(min(widget.duration, data.duration))}",
  381. style: Theme.of(context).textTheme.headline4!.copyWith(fontFamily: "DIN", fontSize: 22.0),
  382. )
  383. ],
  384. )),
  385. Container(
  386. constraints: BoxConstraints(minWidth: 88.0),
  387. child: Row(
  388. children: [
  389. Image.asset("lib/assets/img/trajectory_icon_2.png"),
  390. const SizedBox(
  391. width: 7,
  392. ),
  393. Text(
  394. "${SportUtils.pace(SportUtils.calPace(min(widget.duration, data.duration), (min(widget.distance, data.distance)) / 1000))}",
  395. style: Theme.of(context).textTheme.headline4!.copyWith(fontFamily: "DIN", fontSize: 22.0),
  396. ),
  397. ],
  398. ),
  399. ),
  400. Row(
  401. children: [
  402. Image.asset("lib/assets/img/trajectory_icon_3.png"),
  403. const SizedBox(
  404. width: 7,
  405. ),
  406. Text(
  407. "${widget.kcal}",
  408. style: Theme.of(context).textTheme.headline4!.copyWith(fontFamily: "DIN", fontSize: 22.0),
  409. ),
  410. Padding(
  411. padding: const EdgeInsets.only(top: 4.0),
  412. child: Text(
  413. "kcal",
  414. style: Theme.of(context).textTheme.bodyText1!.copyWith(color: Colors.white),
  415. ),
  416. )
  417. ],
  418. ),
  419. ],
  420. ),
  421. ],
  422. );
  423. }),
  424. ),
  425. ),
  426. ),
  427. ],
  428. ),
  429. ));
  430. }
  431. }