sport_detail.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:animated_text_kit/animated_text_kit.dart';
  4. import 'package:audio_session/audio_session.dart';
  5. import 'package:dotted_line/dotted_line.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/services.dart';
  8. import 'package:get_it/get_it.dart';
  9. import 'package:just_audio/just_audio.dart';
  10. import 'package:sport/provider/bluetooth.dart';
  11. import 'package:sport/services/app_subscription_state.dart';
  12. import 'package:sport/utils/DateFormat.dart';
  13. import 'package:sport/widgets/button_cancel.dart';
  14. import 'package:sport/widgets/dialog/game_alert_dialog.dart';
  15. import 'package:sport/widgets/image.dart';
  16. import 'package:video_player/video_player.dart';
  17. /**
  18. * @TODO 一、服务器接口
  19. * 1、数据详情接口
  20. * 2、运动提交接口、保存的内容、所需要回显的位置及具体内容需提前规则
  21. * 3、排行榜(修改)升序
  22. *
  23. * 二、算法
  24. * 1、目前只使用了在三轮车游戏中使用的开合跳,来测试demo
  25. * 2、具体需要支持哪些动作?识别动作算法需重新编辑
  26. *
  27. * 三、运动的配置
  28. * 1、动作的次数,难度选择等
  29. * 2、运动过程中是否要添加其它提示语音、引导语音等
  30. */
  31. class SportDetail extends StatefulWidget {
  32. final int type;
  33. const SportDetail({Key? key, required this.type}) : super(key: key);
  34. @override
  35. State<StatefulWidget> createState() => _State();
  36. }
  37. class _State extends State<SportDetail> with SubscriptionState {
  38. late VideoPlayerController _videoPlayerController;
  39. late AudioPlayer _audioPlayer;
  40. Timer? _timer;
  41. final List<String> groupLabel = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
  42. List<Mission> missions = List.generate(5, (i) => Mission());
  43. int _index = 0;
  44. int _missionIndex = 0;
  45. int _restIndex = -1;
  46. Duration _duration = Duration.zero;
  47. late Bluetooth _bluetooth;
  48. final GlobalKey dialogKey = GlobalKey();
  49. bool _startui = false;
  50. var _buttonIndex = 0;
  51. @override
  52. void initState() {
  53. super.initState();
  54. _videoPlayerController = VideoPlayerController.network("https://static.ouj.com/hiyd_cms/file/4bb98b9c7cb0441cbea0a96ff73bae93.mp4");
  55. _videoPlayerController.setLooping(true);
  56. _videoPlayerController.initialize().then((value) {
  57. setState(() {
  58. // print("111111111111111111111111111 ${_videoPlayerController.value.duration}");
  59. _duration = _videoPlayerController.value.duration;
  60. setState(() {
  61. _startui = true;
  62. });
  63. // _startMission();
  64. });
  65. });
  66. _videoPlayerController.addListener(() {
  67. // print("11111111111111111111 ${_videoPlayerController.value.isPlaying} ${_videoPlayerController.value}");
  68. // if(_videoPlayerController.value.position == _videoPlayerController.value.duration){
  69. // _videoPlayerController.seekTo(Duration.zero);
  70. // _videoPlayerController.play();
  71. // }
  72. });
  73. _audioPlayer = AudioPlayer(handleInterruptions: false);
  74. AudioSession.instance.then((audioSession) async {
  75. await audioSession.configure(AudioSessionConfiguration.music().copyWith(avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.mixWithOthers | AVAudioSessionCategoryOptions.duckOthers, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation, androidAudioFocusGainType: AndroidAudioFocusGainType.gainTransientMayDuck));
  76. addSubscription(_audioPlayer.playerStateStream.listen((event) {
  77. if (event.processingState == ProcessingState.completed) {}
  78. if (Platform.isAndroid) {
  79. audioSession.setActive(event.processingState == ProcessingState.ready);
  80. }
  81. }));
  82. });
  83. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  84. SystemChrome.setPreferredOrientations(Platform.isIOS ? [DeviceOrientation.landscapeRight] : [DeviceOrientation.landscapeLeft]);
  85. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
  86. });
  87. _bluetooth = GetIt.I<Bluetooth>();
  88. _bluetooth.gameInit(6);
  89. _bluetooth.setupGameMode4h5(true);
  90. addSubscription(_bluetooth.sdkMotionStream.listen((event) {
  91. List<int> result = event;
  92. for (var i in result) {
  93. if (i == 14) {
  94. setState(() {
  95. var mission = missions[_missionIndex];
  96. _index = mission.count += 1;
  97. _playAudio(["number/${_index}"]);
  98. if (_index == mission.target) {
  99. if (_missionIndex < missions.length - 1) {
  100. _startRest();
  101. } else {
  102. _finishMission();
  103. }
  104. }
  105. });
  106. break;
  107. }
  108. }
  109. }));
  110. }
  111. @override
  112. void dispose() {
  113. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  114. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
  115. _timer?.cancel();
  116. _videoPlayerController.dispose();
  117. _audioPlayer.dispose();
  118. _bluetooth.setupGameMode4h5(false);
  119. super.dispose();
  120. }
  121. _startMission() {
  122. if (_missionIndex >= missions.length) {
  123. Navigator.pop(context);
  124. return;
  125. }
  126. _index = 0;
  127. setState(() {
  128. _restIndex = -1;
  129. _startui = false;
  130. });
  131. _timer?.cancel();
  132. _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
  133. setState(() {
  134. missions[_missionIndex].time += 1;
  135. // var mission = missions[_missionIndex];
  136. // _index = mission.count += 1;
  137. // if (_index == mission.target) {
  138. // timer.cancel();
  139. // if (_missionIndex < missions.length - 1) {
  140. // _startRest();
  141. // } else {
  142. // _finishMission();
  143. // }
  144. // }
  145. });
  146. });
  147. _videoPlayerController.play();
  148. }
  149. _startRest() {
  150. _timer?.cancel();
  151. setState(() {
  152. _restIndex = 0;
  153. });
  154. _videoPlayerController.pause();
  155. _videoPlayerController.seekTo(Duration.zero);
  156. _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  157. ++_restIndex;
  158. if (_restIndex > 10) {
  159. timer.cancel();
  160. setState(() {
  161. _missionIndex++;
  162. _startMission();
  163. });
  164. } else {
  165. setState(() {});
  166. }
  167. });
  168. }
  169. _finishMission() {
  170. showDialog(
  171. context: context,
  172. builder: (context) => CustomGameAlertDialog(
  173. title: '是否退出教程',
  174. key: dialogKey,
  175. ok: () => Navigator.of(context).pop(true),
  176. custom: Center(
  177. child: Column(
  178. mainAxisSize: MainAxisSize.min,
  179. children: [
  180. Container(
  181. decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(11.0)),
  182. padding: const EdgeInsets.all(20.0),
  183. child: Row(
  184. mainAxisSize: MainAxisSize.min,
  185. children: [
  186. Column(
  187. children: [
  188. Text(
  189. "恭喜你已完成训练",
  190. style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Theme.of(context).colorScheme.secondary, fontSize: 20.0),
  191. ),
  192. Text(
  193. "完成动作",
  194. style: Theme.of(context).textTheme.subtitle1,
  195. ),
  196. Text(
  197. "运动消耗",
  198. style: Theme.of(context).textTheme.subtitle1,
  199. ),
  200. Text(
  201. "运动时长",
  202. style: Theme.of(context).textTheme.subtitle1,
  203. )
  204. ],
  205. ),
  206. SizedBox(height: 140, child:
  207. DottedLine( direction: Axis.vertical,lineLength: double.infinity, dashColor: Theme.of(context).dividerColor,),),
  208. Column(children: [
  209. Text(
  210. "排行榜",
  211. style: Theme.of(context).textTheme.subtitle1,
  212. )
  213. ],),
  214. ],
  215. ),
  216. ),
  217. const SizedBox(
  218. height: 24,
  219. ),
  220. Center(
  221. child: Row(
  222. mainAxisSize: MainAxisSize.min,
  223. children: <Widget>[
  224. _buttonIndex == 0
  225. ? CancelButton(
  226. backgroundColor: Theme.of(context).colorScheme.secondary,
  227. height: 35,
  228. width: 135,
  229. textColor: Colors.white,
  230. callback: () {
  231. Navigator.of(context).pop(false);
  232. },
  233. content: "再来一次")
  234. : CancelButton(
  235. height: 35,
  236. width: 135,
  237. callback: () {
  238. Navigator.of(context).pop(false);
  239. },
  240. content: "再来一次"),
  241. const SizedBox(
  242. width: 16,
  243. ),
  244. _buttonIndex == 1 ? CancelButton(backgroundColor: Theme.of(context).colorScheme.secondary, height: 35, width: 135, textColor: Colors.white, callback: () => Navigator.of(context).pop(true), content: "结束运动") : CancelButton(height: 35, width: 135, callback: () => Navigator.of(context).pop(true), content: "结束运动")
  245. ],
  246. ),
  247. ),
  248. ],
  249. ),
  250. ),
  251. ),
  252. useSafeArea: false,
  253. ).then((value) {
  254. if (value == true) {
  255. Navigator.of(context).pop();
  256. }else{
  257. setState(() {
  258. _index = 0;
  259. _missionIndex = 0;
  260. _restIndex = -1;
  261. missions.clear();
  262. missions = List.generate(5, (i) => Mission());
  263. _startMission();
  264. });
  265. }
  266. });
  267. }
  268. _playAudio(List<String> audios) {
  269. _audioPlayer.setAudioSource(ConcatenatingAudioSource(children: audios.map((e) => AudioSource.uri(Uri.parse("asset:///assets/audio/$e.mp3"))).toList())).then((value) => _audioPlayer.play());
  270. }
  271. @override
  272. Widget build(BuildContext context) {
  273. return WillPopScope(
  274. onWillPop: () async {
  275. return false;
  276. },
  277. child: Scaffold(
  278. backgroundColor: Colors.black,
  279. body: Center(
  280. child: AspectRatio(
  281. aspectRatio: _videoPlayerController.value.aspectRatio,
  282. child: Stack(
  283. fit: StackFit.expand,
  284. children: [
  285. VideoPlayer(_videoPlayerController),
  286. Align(
  287. alignment: Alignment.bottomCenter,
  288. child: Row(
  289. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  290. children: [
  291. for (var i = 0; i < missions.length; i++)
  292. Expanded(
  293. child: Container(
  294. padding: i > 0 ? const EdgeInsets.only(left: 6) : EdgeInsets.zero,
  295. child: Column(
  296. children: [
  297. Padding(
  298. padding: const EdgeInsets.all(6.0),
  299. child: Text(
  300. "第${groupLabel[i]}组",
  301. style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white),
  302. ),
  303. ),
  304. LinearProgressIndicator(
  305. value: _missionIndex > i
  306. ? 1.0
  307. : _missionIndex == i && missions[i].count > 0
  308. ? missions[i].count / missions[i].target
  309. : 0.0,
  310. backgroundColor: Colors.grey,
  311. ),
  312. ],
  313. mainAxisSize: MainAxisSize.min,
  314. ),
  315. ),
  316. )
  317. ],
  318. ),
  319. ),
  320. Positioned(
  321. top: 20,
  322. right: 0,
  323. child: Container(
  324. decoration: BoxDecoration(borderRadius: BorderRadius.only(topLeft: Radius.circular(50.0), bottomLeft: Radius.circular(50.0)), color: Colors.black.withOpacity(.5)),
  325. child: InkWell(
  326. onTap: () {
  327. Navigator.pop(context);
  328. },
  329. child: Padding(
  330. padding: const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 10.0),
  331. child: Row(
  332. children: [
  333. arrowBackShoe(),
  334. Text(
  335. "左踮脚 · 返回",
  336. style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white),
  337. )
  338. ],
  339. ),
  340. ),
  341. ),
  342. )),
  343. Positioned(
  344. top: 30,
  345. child: SafeArea(
  346. child: Container(
  347. constraints: BoxConstraints(maxWidth: 185),
  348. padding: const EdgeInsets.all(27.0),
  349. child: Column(
  350. mainAxisSize: MainAxisSize.min,
  351. children: [
  352. Container(
  353. decoration: BoxDecoration(borderRadius: BorderRadius.circular(8.0), color: Colors.black.withOpacity(.5)),
  354. padding: const EdgeInsets.all(16.0),
  355. child: Center(
  356. child: Text(
  357. "开合跳",
  358. style: Theme.of(context).textTheme.headline4?.copyWith(fontSize: 18.0),
  359. ),
  360. ),
  361. ),
  362. Container(
  363. width: double.infinity,
  364. decoration: BoxDecoration(borderRadius: BorderRadius.circular(8.0), color: Colors.black.withOpacity(.5)),
  365. margin: const EdgeInsets.only(top: 13.0),
  366. padding: const EdgeInsets.all(16.0),
  367. child: Column(
  368. children: [
  369. Padding(
  370. padding: const EdgeInsets.only(bottom: 8.0),
  371. child: Text(
  372. "第${groupLabel[_missionIndex]}组",
  373. style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white),
  374. ),
  375. ),
  376. Row(
  377. mainAxisSize: MainAxisSize.min,
  378. children: [
  379. Text(
  380. "${missions[_missionIndex].count.toString().padLeft(2, "0")}",
  381. style: Theme.of(context).textTheme.headline4?.copyWith(fontSize: 30.0, color: Theme.of(context).colorScheme.secondary),
  382. ),
  383. Text(
  384. " / ",
  385. style: Theme.of(context).textTheme.headline4?.copyWith(fontSize: 30.0),
  386. ),
  387. Text(
  388. "${missions[_missionIndex].target}",
  389. style: Theme.of(context).textTheme.headline4?.copyWith(
  390. fontSize: 30.0,
  391. ),
  392. ),
  393. ],
  394. ),
  395. Padding(
  396. padding: const EdgeInsets.only(top: 8.0),
  397. child: Text(DateFormat.toVideoTime(missions[_missionIndex].time), style: Theme.of(context).textTheme.subtitle1?.copyWith(fontSize: 18.0, color: Colors.white)),
  398. )
  399. ],
  400. )),
  401. ],
  402. ),
  403. ),
  404. ),
  405. ),
  406. if (_startui == true)
  407. Container(
  408. color: Colors.black54,
  409. child: Center(
  410. child: Column(
  411. mainAxisSize: MainAxisSize.min,
  412. children: [
  413. SizedBox(
  414. height: 120,
  415. child: DefaultTextStyle(
  416. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 120.0, fontFamily: "DIN", color: Colors.white),
  417. child: AnimatedTextKit(
  418. totalRepeatCount: 1,
  419. pause: Duration.zero,
  420. onFinished: () => _startMission(),
  421. animatedTexts: [
  422. for (var i = 5; i > 0; i--) ScaleAnimatedText('$i', duration: const Duration(milliseconds: 1000)),
  423. ScaleAnimatedText('GO', duration: const Duration(milliseconds: 1000)),
  424. ],
  425. onTap: () {},
  426. ),
  427. ),
  428. ),
  429. Padding(
  430. padding: const EdgeInsets.only(top: 27.0, bottom: 8.0),
  431. child: Text("运动即将开始", style: Theme.of(context).textTheme.headline4?.copyWith(fontSize: 18.0)),
  432. ),
  433. Text("请做好拉伸准备,避免肌肉拉伤", style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white)),
  434. ],
  435. ),
  436. ),
  437. ),
  438. if (_restIndex > -1)
  439. Container(
  440. color: Colors.black54,
  441. child: Center(
  442. child: Column(
  443. mainAxisSize: MainAxisSize.min,
  444. children: [
  445. SizedBox(
  446. width: 100.0,
  447. height: 100.0,
  448. child: Stack(
  449. fit: StackFit.expand,
  450. children: [
  451. Center(
  452. child: Padding(
  453. padding: const EdgeInsets.only(top: 10.0),
  454. child: Text(
  455. "${10 - _restIndex}",
  456. style: Theme.of(context).textTheme.headline1!.copyWith(fontSize: 60.0, fontFamily: "DIN", color: Theme.of(context).colorScheme.secondary),
  457. ),
  458. ),
  459. ),
  460. CircularProgressIndicator(
  461. value: _restIndex / 10.0,
  462. strokeWidth: 6,
  463. backgroundColor: Theme.of(context).colorScheme.secondary,
  464. valueColor: AlwaysStoppedAnimation(
  465. Color(0xff82785c),
  466. ))
  467. ],
  468. ),
  469. ),
  470. Padding(
  471. padding: const EdgeInsets.only(top: 27.0, bottom: 8.0),
  472. child: Text("第${groupLabel[_missionIndex + 1]}组运动即将开始", style: Theme.of(context).textTheme.headline4?.copyWith(fontSize: 18.0)),
  473. ),
  474. Text("请做好充分休息,让肌肉放松下", style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white)),
  475. ],
  476. ),
  477. ),
  478. ),
  479. ],
  480. ),
  481. ),
  482. ),
  483. ),
  484. );
  485. }
  486. }
  487. class Mission {
  488. int state = 0;
  489. int count = 0;
  490. int target = 10;
  491. int time = 0;
  492. }