- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配 - 新增百度地图权限、AK 配置及定位权限说明 - 接入 MethodChannel 实现返回键退后台热启动优化 - 配置自定义签名并更新构建逻辑 - 完善 token 刷新与鉴权失效统一处理 - 新增地图页与截图页路由 - 修复返回键拦截导致的 PopScope 失效问题
226 lines
7.8 KiB
Dart
226 lines
7.8 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter_bmflocation/flutter_bmflocation.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:permission_handler/permission_handler.dart';
|
||
|
||
import '../../../app/constants/app_constants.dart';
|
||
|
||
/// 家属端手机本机定位结果。
|
||
///
|
||
/// 来自百度定位插件 `flutter_bmflocation`,坐标为百度 BD09LL
|
||
/// (与地图 SDK 坐标系一致,可直接用于 `BMFMapController.updateLocationData`)。
|
||
class DeviceCurrentLocation {
|
||
const DeviceCurrentLocation({
|
||
required this.latitude,
|
||
required this.longitude,
|
||
this.radius,
|
||
this.address,
|
||
this.altitude,
|
||
this.speed,
|
||
this.course,
|
||
});
|
||
|
||
final double latitude;
|
||
final double longitude;
|
||
|
||
/// 定位精度(米,Android 返回)。
|
||
final double? radius;
|
||
|
||
/// 反地理编码地址。
|
||
final String? address;
|
||
|
||
/// 海拔(米)。
|
||
final double? altitude;
|
||
|
||
/// 速度(m/s)。
|
||
final double? speed;
|
||
|
||
/// 方向角(度,正北为 0,顺时针)。
|
||
final double? course;
|
||
|
||
factory DeviceCurrentLocation.fromBaidu(BaiduLocation src) =>
|
||
DeviceCurrentLocation(
|
||
latitude: src.latitude ?? 0,
|
||
longitude: src.longitude ?? 0,
|
||
radius: src.radius,
|
||
address: src.address,
|
||
altitude: src.altitude,
|
||
speed: src.speed,
|
||
course: src.course,
|
||
);
|
||
}
|
||
|
||
/// 本机定位状态。
|
||
sealed class LocationState {
|
||
const LocationState();
|
||
}
|
||
|
||
/// 定位初始化中 / 尚未定位成功。
|
||
class LocationIdle extends LocationState {
|
||
const LocationIdle();
|
||
}
|
||
|
||
/// 定位成功。
|
||
class LocationSuccess extends LocationState {
|
||
const LocationSuccess(this.location);
|
||
final DeviceCurrentLocation location;
|
||
}
|
||
|
||
/// 定位失败。
|
||
class LocationFailed extends LocationState {
|
||
const LocationFailed(this.message);
|
||
final String message;
|
||
}
|
||
|
||
/// 家属端手机本机定位控制器。
|
||
///
|
||
/// 封装百度定位插件 `LocationFlutterPlugin`:
|
||
/// - 构建时先请求运行时定位权限(Android 6.0+ / iOS 动态授权),
|
||
/// 授权成功后才初始化并启动连续定位。
|
||
/// - 坐标类型设为 BD09LL(与地图 SDK 一致),无需二次坐标转换。
|
||
/// - 全流程输出 [debugPrint] 日志,便于排查定位失败原因。
|
||
/// - Provider 被 dispose(如地图页退出)时自动停止定位并释放插件。
|
||
class LocationController extends Notifier<LocationState> {
|
||
LocationFlutterPlugin? _plugin;
|
||
bool _started = false;
|
||
|
||
@override
|
||
LocationState build() {
|
||
_started = false;
|
||
_plugin = null;
|
||
// Provider 被首次 watch 时自动启动本机定位。
|
||
_init();
|
||
// Provider 被 dispose(如地图页退出)时自动停止定位并释放插件。
|
||
ref.onDispose(_teardown);
|
||
return const LocationIdle();
|
||
}
|
||
|
||
/// 初始化:请求定位权限 -> 初始化定位插件 -> 启动连续定位。
|
||
Future<void> _init() async {
|
||
if (_started) return;
|
||
_started = true;
|
||
|
||
// 1. 请求运行时定位权限。
|
||
final granted = await _ensureLocationPermission();
|
||
if (!granted) {
|
||
state = const LocationFailed('定位权限被拒绝,请到系统设置开启');
|
||
return;
|
||
}
|
||
|
||
// 2. 初始化定位插件。
|
||
final plugin = _plugin ??= LocationFlutterPlugin();
|
||
|
||
// 同意隐私(百度定位 SDK 要求)。
|
||
// ⚠️ 该插件 Android 原生端对 setAgreePrivacy 从不返回 MethodChannel 结果
|
||
// (onMethodCall 里仅调用 LocationClient.setAgreePrivacy,未调用 result.success,
|
||
// 且 dispatchMethodHandler 的 switch 对该 method 落入 default,永不回调)。
|
||
// 直接 await 会永久挂起,导致后续 prepareLoc/startLocation 不执行、定位无输出。
|
||
// 故加超时保护:超时视为已同意,继续走定位流程。
|
||
try {
|
||
await plugin.setAgreePrivacy(true).timeout(
|
||
const Duration(seconds: 2),
|
||
onTimeout: () => false,
|
||
);
|
||
} catch (_) {}
|
||
debugPrint('[Location] setAgreePrivacy done');
|
||
|
||
// authAK 仅对 iOS/鸿蒙生效,Android 复用 Manifest 中的 AK,不调用。
|
||
if (!kIsWeb && (defaultTargetPlatform == TargetPlatform.iOS ||
|
||
defaultTargetPlatform == TargetPlatform.macOS)) {
|
||
final authed = await plugin.authAK(AppConstants.kBaiduMapAk);
|
||
debugPrint('[Location] authAK(${AppConstants.kBaiduMapAk}) => $authed');
|
||
if (!_started) return;
|
||
} else {
|
||
debugPrint('[Location] 跳过 authAK(Android 使用 Manifest AK)');
|
||
}
|
||
|
||
// 3. 注册连续定位回调,把最新结果写入 state。
|
||
plugin.seriesLocationCallback(callback: (result) {
|
||
_handleLocationResult(result);
|
||
});
|
||
|
||
// 4. 设置 Android / iOS 定位参数:BD09LL、高精度、需要地址信息。
|
||
final androidMap = BaiduLocationAndroidOption(
|
||
coordType: BMFLocationCoordType.bd09ll,
|
||
locationMode: BMFLocationMode.hightAccuracy,
|
||
isNeedAddress: true,
|
||
isNeedLocationDescribe: true,
|
||
scanspan: 1000,
|
||
).getMap();
|
||
final iosMap = BaiduLocationIOSOption(
|
||
coordType: BMFLocationCoordType.bd09ll,
|
||
desiredAccuracy: BMFDesiredAccuracy.best,
|
||
isNeedNewVersionRgc: true,
|
||
).getMap();
|
||
final prepared = await plugin.prepareLoc(androidMap, iosMap, const <String, Object?>{});
|
||
debugPrint('[Location] prepareLoc => $prepared');
|
||
if (!_started) return;
|
||
|
||
final started = await plugin.startLocation();
|
||
debugPrint('[Location] startLocation => $started');
|
||
}
|
||
|
||
/// 请求运行时定位权限(WhenInUse 足够前台展示地图定位点)。
|
||
Future<bool> _ensureLocationPermission() async {
|
||
PermissionStatus status = await Permission.locationWhenInUse.status;
|
||
debugPrint('[Location] permission.status => ${status.name}');
|
||
if (status.isGranted) {
|
||
return true;
|
||
}
|
||
if (status.isPermanentlyDenied || status.isRestricted) {
|
||
debugPrint('[Location] permission permanentlyDenied/restricted,需到系统设置开启');
|
||
// 打开应用设置引导用户手动开启。
|
||
await openAppSettings();
|
||
return false;
|
||
}
|
||
status = await Permission.locationWhenInUse.request();
|
||
debugPrint('[Location] permission.request => ${status.name}');
|
||
return status.isGranted;
|
||
}
|
||
|
||
/// 处理定位回调结果。
|
||
void _handleLocationResult(BaiduLocation result) {
|
||
final lat = result.latitude;
|
||
final lon = result.longitude;
|
||
debugPrint('[Location] onResult latitude=$lat longitude=$lon '
|
||
'radius=${result.radius} address=${result.address} '
|
||
'detail=${result.locationDetail} errorInfo=${result.errorInfo} '
|
||
'errorCode=${result.errorCode} locType=${result.locType}');
|
||
if (lat == null || lon == null) {
|
||
state = LocationFailed(result.errorInfo ?? '定位失败');
|
||
return;
|
||
}
|
||
state = LocationSuccess(DeviceCurrentLocation.fromBaidu(result));
|
||
// 定位已有有效结果且非错误,主动停止连续定位,避免常驻耗电。
|
||
stop();
|
||
}
|
||
|
||
/// 停止连续定位(保留插件实例,可再次 [startLocation])。
|
||
///
|
||
/// 本机定位拿到首个有效结果后即停止,符合「有结果且非定位错误就停止定位」的约束。
|
||
void stop() {
|
||
if (!_started) return;
|
||
debugPrint('[Location] stop: stopLocation');
|
||
_started = false;
|
||
_plugin?.stopLocation();
|
||
}
|
||
|
||
/// 停止定位并释放插件。
|
||
void _teardown() {
|
||
if (!_started) return;
|
||
debugPrint('[Location] teardown: stopLocation');
|
||
_started = false;
|
||
_plugin?.stopLocation();
|
||
_plugin = null;
|
||
}
|
||
}
|
||
|
||
/// 家属端手机本机定位 Provider。
|
||
///
|
||
/// 页面级使用:地图页 watch 时自动启动定位,页面销毁时自动停止,
|
||
/// 避免常驻定位导致电量与资源浪费(符合项目 keepAlive 约束,不设 keepAlive)。
|
||
final locationControllerProvider =
|
||
NotifierProvider<LocationController, LocationState>(
|
||
LocationController.new,
|
||
);
|