import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:amap_flutter_base/amap_flutter_base.dart'; import 'package:amap_flutter_map/amap_flutter_map.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:get_it/get_it.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sport/pages/run/run_start.dart'; import 'package:sport/pages/run/setting_healthkit_page.dart'; import 'package:sport/pages/run/setting_permission_page.dart'; import 'package:sport/provider/user_model.dart'; import 'package:sport/router/navigator_util.dart'; import 'package:sport/services/api/rest_client.dart'; import 'package:sport/services/app_lifecycle_state.dart'; import 'package:sport/widgets/appbar.dart'; import 'package:sport/widgets/button_cancel.dart'; import 'package:sport/widgets/button_primary.dart'; import 'package:sport/widgets/dialog/alert_dialog.dart'; import 'package:sport/widgets/image.dart'; const MAP_LIST = [ {"type": "normal", "name": "标准", "custom": ""}, {"type": "night", "name": "夜间"}, {"type": "satellite", "name": "卫星"}, // {"type": "normal_blue", "name": "碧空", "custom": "blue_"}, // {"type": "normal_white", "name": "纯白", "custom": "white_"}, // {"type": "normal_earth", "name": "大地", "custom": "earth_"}, {"type": "normal_cyan", "name": "青草", "custom": "cyan_"}, {"type": "normal_dark", "name": "深黑", "custom": "dark_"}, {"type": "navi", "name": "不显示"} ]; int selectMapIndex(String type) => MAP_LIST.indexWhere((element) => element['type'] == type); MapType selectMapType(int index) { if (index == 1) { return MapType.night; } else if (index == 2) { return MapType.satellite; } else if (index == MAP_LIST.length - 1) { return MapType.navi; } return MapType.normal; } mixin RunSetting on State { static const String KEY_START = "run_start"; static const String KEY_LATITUDE = "run_latitude"; static const String KEY_LONGITUDE = "run_longitude"; static const String KEY_BROADCAST = "run_broadcast"; static const String KEY_BROADCAST_KM = "run_broadcast_KM"; static const String KEY_MAP_TYPE = "run_map_type"; static const String KEY_MAP_KM = "run_map_km"; static const String KEY_STEP_RATE = "run_step_rate"; static const String KEY_TARGET_DURATION = "run_target_duration"; static const String KEY_TARGET_KM = "run_target_km"; int runBroadcastKm = 1; int runMapType = 0; bool runMapKm = true; bool runBroadcast = true; double runStepRate = 1.02; double runTargetDuration = 0; double runTargetKm = 0; Future loadSetting() async { var preferences = await SharedPreferences.getInstance(); runBroadcast = preferences.getBool(KEY_BROADCAST) ?? true; runBroadcastKm = preferences.getInt(KEY_BROADCAST_KM) ?? 1; runMapType = max(0, preferences.getInt(KEY_MAP_TYPE) ?? 0); runMapKm = preferences.getBool(KEY_MAP_KM) ?? true; // runStepRate = max(0.6, preferences.getDouble(KEY_STEP_RATE) ?? 1.02); runStepRate = 1.02; runTargetDuration = preferences.getDouble(KEY_TARGET_DURATION) ?? 0; runTargetKm = preferences.getDouble(KEY_TARGET_KM) ?? 0; } bool autoLoadSetting() => true; Future requestBackground() async { var preferences = await SharedPreferences.getInstance(); bool _background = preferences.getBool("RUN_BACKGROUND") ?? false; if (!_background) { var result = await showDialog( context: context, builder: (context) => CustomAlertDialog( title: "提示", child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), child: Text( "由于您当前的操作系统版本会管控后台设置,可能会在运动记录过程中误杀趣动进程,请您开启后台权限以保证正常运动。", style: TextStyle(fontSize: 16.0, color: Color(0xff333333)), ), ), textCancel: "稍后", textOk: "立即设置", ok: () { Navigator.pop(context, true); }, )); if (result == true) { preferences.setBool("RUN_BACKGROUND", true); await NavigatorUtil.goPage(context, (context) => SettingPermissionPage()); _background = true; } } return _background; // bool checkPermissions = await SystemAlertWindow.checkPermissions; // if (!checkPermissions) { // var result = await showDialog( // context: context, // builder: (context) => CustomAlertDialog( // title: "提示", // child: Padding( // padding: const EdgeInsets.symmetric(horizontal: 20.0), // child: Text( // "由于您当前的操作系统版本会管控后台设置,可能会在运动记录过程中误杀趣动进程,请您开启悬浮窗权限以保证正常运动。", // style: TextStyle(fontSize: 16.0, color: Color(0xff333333)), // ), // ), // textCancel: "稍后", // textOk: "立即设置", // ok: () { // Navigator.pop(context, true); // }, // )); // if (result == true) { // await SystemAlertWindow.requestPermissions; // } // return false; // } // return true; } @override void initState() { super.initState(); if (autoLoadSetting()) refreshSetting(); } void refreshSetting() { loadSetting().then((value) => loadCustomData()); } updateSetting() async { var preferences = await SharedPreferences.getInstance(); preferences.setBool(KEY_BROADCAST, runBroadcast); preferences.setBool(KEY_MAP_KM, runMapKm); preferences.setInt(KEY_BROADCAST_KM, runBroadcastKm); preferences.setInt(KEY_MAP_TYPE, runMapType); preferences.setDouble(KEY_TARGET_DURATION, runTargetDuration); preferences.setDouble(KEY_TARGET_KM, runTargetKm); var result = await GetIt.I().jogSetting( setting: json.encode({ KEY_BROADCAST: runBroadcast, KEY_BROADCAST_KM: runBroadcastKm, KEY_MAP_TYPE: runMapType, KEY_MAP_KM: runMapKm, KEY_STEP_RATE: runStepRate, KEY_TARGET_DURATION: runTargetDuration, KEY_TARGET_KM: runTargetKm, })); } CustomStyleOptions _customStyleOptions = CustomStyleOptions(false); CustomStyleOptions get customStyleOptions => _customStyleOptions; //加载自定义地图样式 void loadCustomData() async { Map map = MAP_LIST[this.runMapType]; if (map.containsKey("custom")) { ByteData styleByteData = await rootBundle.load('assets/mapstyle/${map["custom"]}style.data'); _customStyleOptions.styleData = styleByteData.buffer.asUint8List(); ByteData styleExtraByteData = await rootBundle.load('assets/mapstyle/${map["custom"]}style_extra.data'); _customStyleOptions.styleExtraData = styleExtraByteData.buffer.asUint8List(); } //如果需要加载完成后直接展示自定义地图,可以通过setState修改CustomStyleOptions的enable为true setState(() { _customStyleOptions.enabled = map.containsKey("custom"); }); } } class SettingPage extends StatefulWidget { final bool run; final bool map; const SettingPage({Key? key, this.run = false, this.map = true}) : super(key: key); @override State createState() => _PageState(); } class _PageState extends LifecycleState with RunSetting { @override void initState() { super.initState(); } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { var list = [ {"type": "normal", "name": "标准", "custom": ""}, {"type": "night", "name": "夜间"}, {"type": "satellite", "name": "卫星"}, // {"type": "normal_blue", "name": "碧空", "custom": "blue_"}, // {"type": "normal_white", "name": "纯白", "custom": "white_"}, // {"type": "normal_earth", "name": "大地", "custom": "earth_"}, {"type": "normal_cyan", "name": "青草", "custom": "cyan_"}, {"type": "normal_dark", "name": "深黑", "custom": "dark_"}, {"type": "navi", "name": "不显示"} ]; var _labelStyle = TextStyle(fontSize: 16.0, color: Color(0xff333333)); return WillPopScope( onWillPop: () async { updateSetting(); return true; }, child: Scaffold( backgroundColor: Colors.white, appBar: buildAppBar(context, title: "跑步设置"), body: Padding( padding: const EdgeInsets.all(16.0), child: CustomScrollView( slivers: [ if (widget.map == true) SliverGrid.count( crossAxisCount: 3, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: .7, children: list .map((e) => GestureDetector( onTap: () async { int type = selectMapIndex(e["type"].toString()); var result = await showDialog( context: context, builder: (context) { GlobalKey<_MapViewState> childKey = GlobalKey(); return CustomAlertDialog( title: '是否切换地图样式', child: _MapView( type: type, key: childKey, ), textOk: '确定', ok: () { _MapViewState? state = childKey.currentState; if (state != null) { Navigator.of(context).pop(state.runMapType); } else { Navigator.of(context).pop(0); } }); }, ); if (this.runMapType != result) { this.runMapType = result; SharedPreferences.getInstance().then((prefs) { prefs.setInt(RunSetting.KEY_MAP_TYPE, this.runMapType); setState(() {}); }); } }, child: Column( children: [ AspectRatio( aspectRatio: 1.0, child: Container( foregroundDecoration: this.runMapType == selectMapIndex(e["type"].toString()) ? BoxDecoration( border: Border.all( color: Theme.of(context).accentColor, width: 2, ), borderRadius: BorderRadius.circular(10), ) : null, decoration: BoxDecoration( color: Colors.white, // 底色 borderRadius: new BorderRadius.all(Radius.circular(10.0)), // 也可控件一边圆角大小 boxShadow: [BoxShadow(offset: Offset(0.0, 3), blurRadius: 6, spreadRadius: 0, color: Color.fromRGBO(0, 0, 0, 0.07))]), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.asset( "lib/assets/img/map_img_${e["type"]}.png", fit: BoxFit.cover, ), )), ), SizedBox( height: 8.0, ), Text( e["name"] as String, style: this.runMapType == selectMapIndex(e["type"].toString()) ? Theme.of(context).textTheme.subtitle1!.copyWith(color: Theme.of(context).accentColor) : Theme.of(context).textTheme.subtitle1!, ) ], ))) .toList(), ), SliverToBoxAdapter( child: Column( children: [ SizedBox( height: 20.0, ), Divider(), ], ), ), SliverToBoxAdapter( child: Column( children: [ ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("公里标签开关", style: _labelStyle), Switch( value: runMapKm, onChanged: (bool value) { runMapKm = !runMapKm; SharedPreferences.getInstance().then((prefs) { prefs.setBool(RunSetting.KEY_MAP_KM, runMapKm); setState(() {}); }); }, ) ], ), contentPadding: const EdgeInsets.symmetric(horizontal: 0.0), ), Divider(), ], ), ), if (widget.run == true) SliverToBoxAdapter( child: Column( children: [ ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("语音播报开关", style: _labelStyle), Switch( value: runBroadcast, onChanged: (bool value) { runBroadcast = !runBroadcast; SharedPreferences.getInstance().then((prefs) { prefs.setBool(RunSetting.KEY_BROADCAST, runBroadcast); setState(() {}); }); }, ) ], ), contentPadding: const EdgeInsets.symmetric(horizontal: 0.0), ), if (runBroadcast == true) ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Text('距离播报', style: _labelStyle)], ), onTap: () async { var result = await showModalBottomSheet(context: context, builder: (context) => _KmWidget(), backgroundColor: Colors.transparent); if (result != null) { runBroadcastKm = result; SharedPreferences.getInstance().then((prefs) { prefs.setInt(RunSetting.KEY_BROADCAST_KM, runBroadcastKm); setState(() {}); }); } }, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text( "每$runBroadcastKm公里", style: Theme.of(context).textTheme.bodyText2!, ), SizedBox( width: 5, ), arrowRight5(), ], ), contentPadding: const EdgeInsets.symmetric(horizontal: 0.0), ), Divider(), ], ), ), if (Platform.isAndroid) SliverToBoxAdapter( child: Column( children: [ ListTile( title: Text("跑步权限", style: _labelStyle), contentPadding: const EdgeInsets.symmetric(horizontal: 0.0), trailing: arrowRight5(), onTap: () { NavigatorUtil.goPage(context, (context) => SettingPermissionPage()); }, ), Divider(), ], ), ), if (Platform.isIOS) SliverToBoxAdapter( child: Column( children: [ ListTile( title: Text("连接苹果健康", style: _labelStyle), contentPadding: const EdgeInsets.symmetric(horizontal: 0.0), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ FutureBuilder( builder: (context, snapshot) { return Text("${snapshot.data?.containsKey("HEALTH_KIT_CONNECTED") == true ? "已连接" : "未连接"}"); }, future: SharedPreferences.getInstance(), ), SizedBox( width: 5, ), arrowRight5() ], ), onTap: () { NavigatorUtil.goPage(context, (context) => SettingHealthKitPage()); }, ), Divider(), ], ), ), ], ), ), ), ); } } class _KmWidget extends StatefulWidget { @override State createState() => _KmWidgetState(); } class _KmWidgetState extends State<_KmWidget> { int _index = 0; final List items = [1, 2, 5]; final List labels = ["1公里", "2公里", "5公里"]; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: EdgeInsets.all(20.0), decoration: BoxDecoration( borderRadius: BorderRadius.only(topLeft: Radius.circular(10), topRight: Radius.circular(10)), color: Colors.white, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ AlignedGridView.count( padding: EdgeInsets.zero, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), crossAxisCount: 3, itemCount: items.length, itemBuilder: (BuildContext context, int index) => GestureDetector( onTap: () { setState(() { _index = index; }); }, child: Center( child: Container( height: 44.0, decoration: BoxDecoration( image: _index == index ? DecorationImage(image: AssetImage("lib/assets/img/control_img_selected.png"), alignment: Alignment.bottomRight) : null, borderRadius: BorderRadius.circular(10), border: Border.all( color: _index == index ? Theme.of(context).accentColor : const Color(0xffCECECE), width: .5, ), ), child: Center(child: Text("${labels[index]}", style: _index == index ? Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 16.0, color: Theme.of(context).accentColor) : Theme.of(context).textTheme.subtitle1!.copyWith(fontSize: 16.0))), )), ), crossAxisSpacing: 12.0, mainAxisSpacing: 12.0, ), SizedBox( height: 16, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: CancelButton( height: 35, callback: () { Navigator.of(context).pop(false); }, content: "取消"), ), SizedBox( width: 16, ), Expanded( child: PrimaryButton( height: 35, callback: () { Navigator.pop(context, items[_index]); }, content: "确定")) ], ) ], ), ); } } class _MapView extends StatefulWidget { final int type; const _MapView({Key? key, this.type = 0}) : super(key: key); @override State createState() => _MapViewState(); } class _MapViewState extends State<_MapView> { CustomStyleOptions _customStyleOptions = CustomStyleOptions(false); CustomStyleOptions get customStyleOptions => _customStyleOptions; int runMapType = 0; //加载自定义地图样式 void loadCustomData() async { Map map = MAP_LIST[this.runMapType]; if (map.containsKey("custom")) { ByteData styleByteData = await rootBundle.load('assets/mapstyle/${map["custom"]}style.data'); _customStyleOptions.styleData = styleByteData.buffer.asUint8List(); ByteData styleExtraByteData = await rootBundle.load('assets/mapstyle/${map["custom"]}style_extra.data'); _customStyleOptions.styleExtraData = styleExtraByteData.buffer.asUint8List(); } //如果需要加载完成后直接展示自定义地图,可以通过setState修改CustomStyleOptions的enable为true setState(() { _customStyleOptions.enabled = map.containsKey("custom"); }); } LatLng? _initLatLng; @override initState() { super.initState(); runMapType = widget.type; UserModel userModel = Provider.of(context, listen: false); if (userModel.latitude != 0 && userModel.longitude != 0) { _initLatLng = LatLng(userModel.latitude, userModel.longitude); } loadCustomData(); } @override Widget build(BuildContext context) { return Container( height: 150.0, child: Row( children: [ GestureDetector( onTap: () { runMapType = --runMapType % MAP_LIST.length; loadCustomData(); }, behavior: HitTestBehavior.opaque, child: Container( width: 50, height: 20, child: Image.asset( "lib/assets/img/btn_arrow_left.png", fit: BoxFit.fitHeight, color: Color(0xff999999), ), ), ), Expanded( child: ClipRRect( borderRadius: BorderRadius.circular(10.0), child: Stack( children: [ Positioned( top: 0, right: 0, left: 0, bottom: -50, child: AMapWidget( privacyStatement: amapPrivacyStatement, apiKey: AMapApiKey(androidKey: KEY_ANDROID, iosKey: KEY_IOS), mapType: selectMapType(runMapType), customStyleOptions: customStyleOptions, initialCameraPosition: _initLatLng != null ? CameraPosition(target: _initLatLng!, zoom: 15) : const CameraPosition(target: LatLng(39.909187, 116.397451), zoom: 15), buildingsEnabled: true, tiltGesturesEnabled: false, rotateGesturesEnabled: false, touchPoiEnabled: false, labelsEnabled: false, scrollGesturesEnabled: false, scaleEnabled: false, ), ), Align( alignment: Alignment.bottomCenter, child: Container( height: 26, color: Colors.black45, child: Center( child: Text( "${MAP_LIST[runMapType]['name']}", style: TextStyle(color: Colors.white, fontSize: 14.0), )), )) ], ), )), GestureDetector( onTap: () { runMapType = ++runMapType % MAP_LIST.length; loadCustomData(); }, behavior: HitTestBehavior.opaque, child: Container( width: 50, height: 20, child: Image.asset( "lib/assets/img/btn_arrow_right.png", fit: BoxFit.fitHeight, color: Color(0xff999999), ), ), ), ], ), ); } }