sport_list_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import 'dart:io';
  2. import 'dart:math';
  3. import 'package:cached_network_image/cached_network_image.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
  7. import 'package:get_it/get_it.dart';
  8. import 'package:sport/application.dart';
  9. import 'package:sport/bean/game.dart';
  10. import 'package:sport/pages/game/game_detail.dart';
  11. import 'package:sport/pages/game/game_guide.dart';
  12. import 'package:sport/pages/game/game_info.dart';
  13. import 'package:sport/provider/bluetooth.dart';
  14. import 'package:sport/provider/game_model.dart';
  15. import 'package:sport/router/navigator_util.dart';
  16. import 'package:sport/services/api/inject_api.dart';
  17. import 'package:sport/services/app_subscription_state.dart';
  18. import 'package:sport/services/inject_route_aware.dart';
  19. import 'package:sport/widgets/appbar.dart';
  20. import 'package:sport/widgets/dialog/ble_wait_dialog.dart';
  21. import 'package:sport/widgets/image.dart';
  22. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  23. import 'package:wakelock/wakelock.dart';
  24. class SportListPage extends StatefulWidget {
  25. final int index;
  26. final int runUserCount;
  27. final List<GameInfoData> games;
  28. SportListPage({this.index = 0, this.runUserCount = 0, required this.games});
  29. @override
  30. State<StatefulWidget> createState() => _PageState();
  31. }
  32. class _PageState extends State<SportListPage> with InjectApi, SubscriptionState, InjectRouteAware, WidgetsBindingObserver {
  33. List<GameInfoData> _games = [];
  34. late PageController _pageController;
  35. late Bluetooth _bluetooth;
  36. late GameModel _gameModel;
  37. late Function() gameModeListener;
  38. GameInfoData? game;
  39. int gameIndex = 0;
  40. bool gameMode = false;
  41. bool gamePlaying = false;
  42. @override
  43. void initState() {
  44. super.initState();
  45. WidgetsBinding.instance?.addObserver(this);
  46. _gameModel = GetIt.I<GameModel>();
  47. _gameModel.noCheck = true;
  48. _bluetooth = GetIt.I<Bluetooth>();
  49. _bluetooth.h5gameRNotifier.addListener(gameModeListener = () {
  50. gameMode = _bluetooth.h5gameRNotifier.value;
  51. });
  52. addSubscription(_bluetooth.sdkCmdStream.listen((event) {
  53. if (gameMode && !gamePlaying) {
  54. int cmd = event;
  55. if (cmd > -1) {
  56. print("sdk -- cmd $cmd");
  57. if (cmd == 3) {
  58. _previous();
  59. }
  60. if (cmd == 4) {
  61. _next();
  62. }
  63. if (cmd == 6) {
  64. _back();
  65. _bluetooth.vibrate(200, leftOrRight: 1);
  66. }
  67. if (cmd == 5) {
  68. _ok();
  69. _bluetooth.vibrate(200, leftOrRight: 2);
  70. }
  71. }
  72. }
  73. }));
  74. WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
  75. _checkInstall();
  76. });
  77. }
  78. @override
  79. void dispose() {
  80. WidgetsBinding.instance?.removeObserver(this);
  81. _gameModel.noCheck = false;
  82. _bluetooth.setupGameMode4h5(false);
  83. _bluetooth.h5gameRNotifier.removeListener(gameModeListener);
  84. gameMode = false;
  85. Wakelock.disable();
  86. super.dispose();
  87. // horizontal.value = false;
  88. // horizontal.dispose();
  89. }
  90. @override
  91. void didChangeAppLifecycleState(AppLifecycleState state) {
  92. super.didChangeAppLifecycleState(state);
  93. switch (state) {
  94. case AppLifecycleState.resumed:
  95. if (game?.h5 == 0) {
  96. _bluetooth.gameInit(game?.id ?? 0);
  97. gamePlaying = false;
  98. if(gameMode == true)
  99. showWaitDialog(context);
  100. }
  101. Stream.periodic(const Duration(seconds: 2)).take(5).forEach((element) {
  102. if (mounted) {
  103. _bluetooth.setupGameMode4h5(gameMode);
  104. }
  105. });
  106. break;
  107. case AppLifecycleState.paused:
  108. // if (mounted) {
  109. // _bluetooth.setupGameMode4h5(false);
  110. // }
  111. break;
  112. default:
  113. break;
  114. }
  115. }
  116. @override
  117. void didPop() {
  118. gamePlaying = false;
  119. }
  120. @override
  121. void didPopNext() {
  122. gamePlaying = false;
  123. }
  124. @override
  125. void didPushNext() {
  126. gamePlaying = true;
  127. }
  128. _checkInstall() async {
  129. List<GameInfoData> games = widget.games.isNotEmpty == true ? List.from(widget.games) : (await api.getGameAll()).results;
  130. for (var i = 0; i < games.length; i++) {
  131. games[i].isLocal = await isInstalled(games[i]) == 0;
  132. }
  133. games.sort((a, b) => a.isLocal == true ? -1 : 1);
  134. setState(() {
  135. _games = games;
  136. });
  137. }
  138. _back() {
  139. Navigator.pop(context, false);
  140. }
  141. _ok() {
  142. if (_games.isNotEmpty == true && gamePlaying != true && appLifecycleState == AppLifecycleState.resumed) {
  143. gamePlaying = true;
  144. // _bluetooth.setupGameMode4h5(gameMode = true);
  145. _gameModel.gameCount++;
  146. game = _games[gameIndex];
  147. startGame(context, game, launch4GameCenter: true).then((value) => gamePlaying = false);
  148. UmengCommonSdk.onEvent("game_start_sport_list", {"id": "${game?.id}"});
  149. }
  150. }
  151. _previous() {
  152. setState(() {
  153. gameIndex = --gameIndex % _games.length;
  154. _switchPage(gameIndex);
  155. });
  156. }
  157. _next() {
  158. setState(() {
  159. gameIndex = ++gameIndex % _games.length;
  160. _switchPage(gameIndex);
  161. });
  162. }
  163. _switchPage(int index) {
  164. if (!mounted) return;
  165. int page = _pageController.page?.toInt() ?? 0;
  166. int currentPage = index ~/ 10;
  167. if (currentPage != page) {
  168. _pageController.animateToPage(currentPage, duration: Duration(milliseconds: 500), curve: Curves.linear);
  169. }
  170. }
  171. @override
  172. Widget build(BuildContext context) {
  173. final _padding = const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0);
  174. List<GameInfoData> games = _games;
  175. return OrientationBuilder(
  176. builder: (BuildContext context, Orientation orientation) {
  177. final bool landscape = orientation == Orientation.landscape;
  178. return Scaffold(
  179. backgroundColor: landscape ? Colors.black : Colors.black.withOpacity(0.9),
  180. appBar: AppBar(
  181. automaticallyImplyLeading: false,
  182. centerTitle: false,
  183. titleSpacing: landscape ? 50 : 16,
  184. systemOverlayStyle: SystemUiOverlayStyle.light,
  185. backgroundColor: Colors.transparent,
  186. title: Row(
  187. children: [
  188. Text(
  189. "运动列表",
  190. style: titleStyle.copyWith(color: Colors.white),
  191. ),
  192. const SizedBox(
  193. width: 20.0,
  194. ),
  195. TextButton(
  196. onPressed: () {
  197. if (landscape) {
  198. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  199. _bluetooth.setupGameMode4h5(gameMode = false);
  200. } else {
  201. SystemChrome.setPreferredOrientations(Platform.isIOS ? [DeviceOrientation.landscapeRight] : [DeviceOrientation.landscapeLeft]);
  202. _bluetooth.setupGameMode4h5(gameMode = true);
  203. }
  204. },
  205. child: Row(
  206. children: [
  207. Row(children: [
  208. Image.asset(
  209. landscape ? "lib/assets/img/icon_pop_vertical_screen.png" : "lib/assets/img/icon_pop_horizontal_screen.png",
  210. height: 20.0,
  211. ),
  212. const SizedBox(
  213. width: 8.0,
  214. ),
  215. Text(
  216. landscape ? "竖屏切换" : "横屏切换",
  217. style: Theme.of(context).textTheme.headline6!.copyWith(color: Colors.white),
  218. )
  219. ]),
  220. ],
  221. )),
  222. ],
  223. ),
  224. actions: [
  225. landscape ? InkWell(
  226. onTap: _back,
  227. child: Padding(
  228. padding: const EdgeInsets.fromLTRB(8.0,5.0,12.0, 5.0),
  229. child: Row(
  230. children: [
  231. arrowBackShoe(),
  232. Text(
  233. "左踮脚 · 返回",
  234. style: Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.white),
  235. )
  236. ],
  237. ),
  238. ),
  239. ):IconButton(
  240. icon: Image.asset(
  241. "lib/assets/img/btn_close_white.png",
  242. height: 20.0,
  243. ),
  244. onPressed: () =>_back())
  245. ],
  246. ),
  247. body: landscape
  248. ? Column(
  249. children: [
  250. Expanded(
  251. child: PageView.builder(
  252. controller: _pageController = PageController(initialPage: gameIndex ~/ 10),
  253. scrollDirection: Axis.vertical,
  254. itemCount: ((games.length - 1) ~/ 10) + 1,
  255. itemBuilder: (context, index) {
  256. return Padding(
  257. padding: const EdgeInsets.symmetric(horizontal: 30.0, vertical: 12.0),
  258. child: LayoutBuilder(
  259. builder: (BuildContext context, BoxConstraints constraints) {
  260. double width = constraints.biggest.width;
  261. double height = constraints.biggest.height;
  262. double mainAxisSpacing = 12.0;
  263. double crossAxisSpacing = 10.0;
  264. int crossAxisCount = 5;
  265. double boxWidth = width / 5 - crossAxisSpacing / 4;
  266. double boxHeight = (height / 2.0) - mainAxisSpacing;
  267. return AlignedGridView.count(
  268. crossAxisCount: crossAxisCount,
  269. shrinkWrap: true,
  270. physics: NeverScrollableScrollPhysics(),
  271. itemCount: min(10, games.length - index * 10),
  272. crossAxisSpacing: 10.0,
  273. mainAxisSpacing: mainAxisSpacing,
  274. itemBuilder: (context, i) {
  275. int _index = index * 10 + i;
  276. GameInfoData e = games[_index];
  277. return InkWell(
  278. onTap: () {
  279. setState(() {
  280. gameIndex = _index;
  281. _ok();
  282. });
  283. },
  284. child: Container(
  285. width: boxWidth,
  286. height: boxHeight,
  287. alignment: Alignment.center,
  288. child: Column(
  289. mainAxisSize: MainAxisSize.min,
  290. children: [
  291. AnimatedContainer(
  292. duration: Duration(milliseconds: 300),
  293. width: min(boxWidth, boxHeight) * (_index == gameIndex ? .7 : .6),
  294. height: min(boxWidth, boxHeight) * (_index == gameIndex ? .7 : .6),
  295. decoration: BoxDecoration(
  296. image: DecorationImage(
  297. image: CachedNetworkImageProvider(
  298. "${e.cover}",
  299. ),
  300. fit: BoxFit.cover),
  301. borderRadius: BorderRadius.circular(10.0),
  302. border: Border.all(width: 4, color: _index == gameIndex ? Theme.of(context).colorScheme.secondary : Colors.transparent)),
  303. ),
  304. const SizedBox(
  305. height: 8.0,
  306. ),
  307. Text(
  308. "${e.name}",
  309. maxLines: 1,
  310. style: Theme.of(context).textTheme.subtitle1!.copyWith(color: _index == gameIndex ? Theme.of(context).colorScheme.secondary : Colors.white),
  311. )
  312. ],
  313. ),
  314. ),
  315. );
  316. });
  317. },
  318. ),
  319. );
  320. }),
  321. ),
  322. Container(
  323. color: Colors.white.withOpacity(.1),
  324. padding: const EdgeInsets.symmetric(vertical: 8.0),
  325. child: Center(
  326. child: Row(
  327. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  328. children: [
  329. Row(
  330. children: [
  331. Row(children: [
  332. Image.asset(
  333. "lib/assets/img/gamepop_leftarrow_notes.png",
  334. height: 20.0,
  335. ),
  336. const SizedBox(
  337. width: 8.0,
  338. ),
  339. Text(
  340. "左踏 · 上一个",
  341. style: Theme.of(context).textTheme.headline6!,
  342. )
  343. ]),
  344. ],
  345. ),
  346. Row(
  347. children: [
  348. Row(children: [
  349. Image.asset(
  350. "lib/assets/img/gamepop_rihgtarrow_notes.png",
  351. height: 20.0,
  352. ),
  353. const SizedBox(
  354. width: 8.0,
  355. ),
  356. Text(
  357. "右踏 · 下一个",
  358. style: Theme.of(context).textTheme.headline6!,
  359. )
  360. ]),
  361. ],
  362. ),
  363. Row(
  364. children: [
  365. Row(children: [
  366. Image.asset(
  367. "lib/assets/img/gamepop_icon_right_notes.png",
  368. height: 20.0,
  369. ),
  370. const SizedBox(
  371. width: 8.0,
  372. ),
  373. Text(
  374. "右踮脚 · 确认",
  375. style: Theme.of(context).textTheme.headline6!,
  376. )
  377. ]),
  378. ],
  379. ),
  380. ],
  381. ),
  382. ),
  383. )
  384. ],
  385. )
  386. : CustomScrollView(
  387. slivers: [
  388. SliverToBoxAdapter(
  389. child: InkWell(
  390. onTap: () {
  391. NavigatorUtil.goPage(
  392. context,
  393. (context) => GameGuide(
  394. game: GameInfoData(),
  395. base: true,
  396. launch4GameCenter: true,
  397. detailGuide: true,
  398. ));
  399. },
  400. child: Container(
  401. margin: _padding,
  402. decoration: BoxDecoration(
  403. borderRadius: BorderRadius.circular(10.0),
  404. color: Colors.white,
  405. ),
  406. padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 15.0),
  407. child: Row(
  408. children: [
  409. Image.asset(
  410. "lib/assets/img/gamepop_icon_right_notes_black.png",
  411. height: 20.0,
  412. ),
  413. Expanded(
  414. child: Container(
  415. margin: const EdgeInsets.only(left: 12.0),
  416. child: Text(
  417. "游戏通用操作教程",
  418. style: Theme.of(context).textTheme.subtitle1,
  419. ),
  420. ),
  421. ),
  422. arrowRight()
  423. ],
  424. )),
  425. ),
  426. ),
  427. for (var e in games)
  428. SliverToBoxAdapter(
  429. child: Padding(
  430. padding: _padding,
  431. child: GestureDetector(
  432. onTap: () => NavigatorUtil.goPage(context, (context) => GameDetailsPage(e)),
  433. child: Container(
  434. decoration: BoxDecoration(
  435. borderRadius: BorderRadius.circular(10.0),
  436. color: Colors.white,
  437. ),
  438. padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
  439. child: Row(
  440. children: <Widget>[
  441. ClipRRect(
  442. borderRadius: BorderRadius.circular(6.0),
  443. child: CachedNetworkImage(
  444. width: 60.0,
  445. height: 60.0,
  446. fit: BoxFit.cover,
  447. imageUrl: "${e.cover}",
  448. ),
  449. ),
  450. const SizedBox(
  451. width: 12.0,
  452. ),
  453. Expanded(
  454. child: Column(
  455. crossAxisAlignment: CrossAxisAlignment.start,
  456. children: <Widget>[
  457. Text(
  458. e.name ?? "",
  459. style: Theme.of(context).textTheme.headline3,
  460. ),
  461. const SizedBox(
  462. height: 8,
  463. ),
  464. Row(
  465. children: [
  466. Text("${e.playMinute}分钟", style: Theme.of(context).textTheme.bodyText1),
  467. Container(
  468. decoration: BoxDecoration(shape: BoxShape.circle, color: const Color(0xff999999)),
  469. margin: const EdgeInsets.symmetric(horizontal: 4),
  470. width: 2,
  471. height: 2,
  472. ),
  473. Text("${difficulty[(e.difficulty ?? 0) ~/ 40.0]}", style: Theme.of(context).textTheme.bodyText1),
  474. ],
  475. ),
  476. ],
  477. ),
  478. ),
  479. Container(
  480. width: 110.0,
  481. child: StartButtonWidget(
  482. e,
  483. (v) {
  484. setState(() {
  485. gameIndex = games.indexOf(e);
  486. _ok();
  487. });
  488. },
  489. height: 35.0,
  490. textStyle: Theme.of(context).textTheme.subtitle1!.copyWith(color: Colors.white),
  491. ),
  492. )
  493. ],
  494. ),
  495. ),
  496. )),
  497. )
  498. ],
  499. ),
  500. );
  501. },
  502. );
  503. }
  504. }