feat(android): 支持沉浸式状态栏、地图定位与自定义签名

- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配
- 新增百度地图权限、AK 配置及定位权限说明
- 接入 MethodChannel 实现返回键退后台热启动优化
- 配置自定义签名并更新构建逻辑
- 完善 token 刷新与鉴权失效统一处理
- 新增地图页与截图页路由
- 修复返回键拦截导致的 PopScope 失效问题
This commit is contained in:
TongTongStudio
2026-08-19 03:43:12 +08:00
parent 41d815336a
commit 06cdc77172
51 changed files with 3988 additions and 634 deletions

View File

@@ -0,0 +1,50 @@
import 'dart:math';
import 'package:flutter_baidu_mapapi_base/flutter_baidu_mapapi_base.dart';
/// WGS84(GPS) 经纬度转百度 BD09LL 坐标。
///
/// 分两步WGS84 -> GCJ02国测局火星坐标再 GCJ02 -> BD09。
/// 设备上报的定位通常为 GPS(WGS84),而百度地图使用 BD09LL 坐标系,
/// 展示前必须做该转换,否则地图上位置会有偏移。
BMFCoordinate wgs84ToBd09(double lat, double lon) {
const double a = 6378245.0;
const double ee = 0.00669342162296594323;
double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y +
0.2 * sqrt(x.abs());
ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * sin(y * pi) + 40.0 * sin(y / 3.0 * pi)) * 2.0 / 3.0;
ret += (160.0 * sin(y / 12.0 * pi) + 320 * sin(y * pi / 30.0)) * 2.0 / 3.0;
return ret;
}
double transformLon(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y +
0.1 * sqrt(x.abs());
ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0;
ret += (20.0 * sin(x * pi) + 40.0 * sin(x / 3.0 * pi)) * 2.0 / 3.0;
ret += (150.0 * sin(x / 12.0 * pi) + 300.0 * sin(x / 30.0 * pi)) * 2.0 / 3.0;
return ret;
}
// WGS84 -> GCJ02
final double dLat = transformLat(lon - 105.0, lat - 35.0);
final double dLon = transformLon(lon - 105.0, lat - 35.0);
final double radLat = lat / 180.0 * pi;
final double magic = sin(radLat);
final double sqrtMagic = sqrt(1 - ee * magic * magic);
final double mgLat =
lat + (dLat * 180.0) / ((a * (1 - ee)) / (sqrtMagic * sqrtMagic) * pi);
final double mgLon =
lon + (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * pi);
// GCJ02 -> BD09
final double z = sqrt(mgLon * mgLon + mgLat * mgLat) + 0.00002 * sin(mgLat * pi);
final double theta = atan2(mgLat, mgLon) + 0.000003 * cos(mgLon * pi);
final double bdLon = z * cos(theta) + 0.0065;
final double bdLat = z * sin(theta) + 0.006;
return BMFCoordinate(bdLat, bdLon);
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
/// 系统 UI状态栏 / 导航栏)适配工具。
///
/// 集中处理沉浸式状态栏、edge-to-edge 布局与刘海屏Display Cutout适配
/// 避免在业务页面中散落系统 UI 相关代码。
class SystemUiUtil {
const SystemUiUtil._();
/// 应用启动时统一配置系统 UI。
///
/// - 使用 edge-to-edge 布局:内容延伸到状态栏与导航栏区域。
/// - 状态栏 / 导航栏采用半透明叠加,图标使用深色(适配浅色背景 UI
static Future<void> configure() async {
// edge-to-edgeApp 内容铺满全屏,安全区由 Flutter 的 SafeArea / MediaQuery
// 在布局层兜底,系统栏以半透明覆盖层呈现。
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// 状态栏 / 导航栏图标亮度:本工程为浅色背景,使用深色图标。
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
// 状态栏图标(时间、电量等)为深色。
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
// 导航栏图标为深色,导航栏背景半透明以配合 edge-to-edge。
systemNavigationBarIconBrightness: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
// 允许刘海屏内容延伸Android 端会同步在 Manifest 配置 cutout 模式)。
statusBarColor: Colors.transparent,
),
);
}
/// 将应用移动到后台(实现 Android 热启动优化)。
///
/// 调用原生端的 moveTaskToBack(false),避免 Activity 销毁。
static Future<void> moveToBack() async {
const channel = MethodChannel('com.ttstd.familycare/app');
try {
await channel.invokeMethod('moveTaskToBack');
} on PlatformException catch (e) {
debugPrint('Failed to move task to back: ${e.message}');
// 兜底方案:退回到系统默认行为
await SystemNavigator.pop();
}
}
}

View File

@@ -0,0 +1,57 @@
/// 统一的时间格式化工具。
///
/// 后端各时间字段(如截图 `uploadTime`、设备 `lastUpdateTime` 等)常以
/// 原始字符串返回格式不一ISO8601 / 空格分隔 / 纯数字时间戳等)。
/// 通过 [formatTimeString] 统一解析并格式化为友好的 `MM-dd HH:mm`。
library;
/// 将后端返回的时间字符串解析为友好显示(`MM-dd HH:mm`)。
///
/// 支持以下常见格式:
/// - ISO8601`2026-08-18T14:30:00`、`2026-08-18T14:30:00+08:00`
/// - 空格分隔:`2026-08-18 14:30:00`
/// - 纯数字时间戳(秒或毫秒):`1784500200000`
///
/// 解析失败时返回 [fallback](默认空串);[raw] 为 null/空 时同样返回
/// [fallback]。若无法识别但仍非空,会原样返回原始字符串。
String formatTimeString(String? raw, {String fallback = ''}) {
if (raw == null || raw.isEmpty) return fallback;
final dt = tryParseTimeString(raw);
if (dt == null) return raw;
return formatDateTime(dt);
}
/// 将 [DateTime] 格式化为友好的 `MM-dd HH:mm`。
String formatDateTime(DateTime time) {
final local = time.toLocal();
final m = _two(local.month);
final d = _two(local.day);
final h = _two(local.hour);
final min = _two(local.minute);
return '$m-$d $h:$min';
}
/// 尝试把后端常见的时间字符串解析为 [DateTime],失败返回 null。
DateTime? tryParseTimeString(String raw) {
final s = raw.trim();
if (s.isEmpty) return null;
// 1. 纯数字:可能是秒或毫秒时间戳。
if (RegExp(r'^\d+$').hasMatch(s)) {
final n = int.tryParse(s);
if (n != null) {
// 13 位以上按毫秒,否则按秒。
return n >= 1000000000000
? DateTime.fromMillisecondsSinceEpoch(n)
: DateTime.fromMillisecondsSinceEpoch(n * 1000);
}
}
// 2. 空格分隔:`2026-08-18 14:30:00` → 补 `T` 变 ISO8601。
final iso = s.contains(' ') ? s.replaceFirst(' ', 'T') : s;
return DateTime.tryParse(iso);
}
String _two(int v) => v.toString().padLeft(2, '0');