ar_tourism_flutter_unity/lib/pages/home/homePage.dart
2025-05-14 18:24:12 +08:00

939 lines
30 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:developer';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../services/update_dialog_service.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
final UpdateDialogService _updateDialogService = UpdateDialogService();
StreamSubscription<Position>? _positionStreamSubscription;
bool _showLocation = true; // 控制图标和km数显示的布尔值
bool _showLocationPrompt = false; // 控制定位提示框显示的布尔值
final double quanchengSquareLat = 36.667763;
final double quanchengSquareLon = 117.028663;
final double damingLakeLat = 36.681025;
final double damingLakeLon = 117.030707;
final double heihuSpringLat = 36.66844;
final double heihuSpringLon = 117.040105;
final double wulongtanLat = 36.672278; // 五龙潭纬度
final double wulongtanLon = 117.021282; // 五龙潭经度
final double jiefangeLat = 36.669113; // 解放阁纬度
final double jiefangeLon = 117.040088; // 解放阁经度
final double baotugSpringLat = 36.667419; // 趵突泉纬度
final double baotugSpringLon = 117.022342; // 趵突泉经度
String qcgc = '0km';
String dmh = '0km';
String hhq = '0km';
String wlt = '0km'; // 五龙潭
String jfg = '0km'; // 解放阁
String btq = '0km'; // 趵突泉
Timer? _debounceTimer;
bool _isProcessingAR = false; // 防止重复启动AR
bool _isUnityInitialized = false; // 跟踪Unity是否已初始化
Position? _lastKnownPosition;
bool _isPageActive = false;
static const platform =
MethodChannel('com.example.ar_tourism_flutter_unity.unity');
@override
void initState() {
super.initState();
print('HomePage初始化');
WidgetsBinding.instance.addObserver(this);
// 初始化时设置页面激活状态
_isPageActive = true;
// 设置Unity通信通道
_setupUnityChannel();
// 延迟初始化
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isPageActive) {
_checkPermissionAndGetLocation();
_checkVersionUpdate(); // 检查更新但不重置状态
}
});
}
Future<String?> _getToken() async {
try {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('token');
} catch (e) {
print('获取token失败: $e');
return null;
}
}
Future<void> _checkVersionUpdate() async {
try {
// 判断是否需要检查更新
bool shouldCheck = await _updateDialogService.shouldCheckUpdate();
if (!shouldCheck) {
print('本次会话已检查过更新或今天已经检查过,跳过更新检查');
return;
}
// 获取当前版本号
PackageInfo packageInfo = await PackageInfo.fromPlatform();
String currentVersion = packageInfo.version;
final versionInfo = await _updateDialogService.checkForUpdate();
// 只有当版本信息存在且需要更新时才显示对话框
if (versionInfo != null &&
_updateDialogService.isNeedUpdate(currentVersion, versionInfo.version) &&
mounted) {
_updateDialogService.showUpdateDialog(context, versionInfo);
}
} catch (e) {
print('版本检查失败: $e');
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 使用Route观察页面是否是当前显示的页面
final route = ModalRoute.of(context);
if (route != null) {
route.addScopedWillPopCallback(() async {
_isPageActive = false;
return true;
});
}
}
void _onPageResume() {
if (!_isPageActive) return;
print('页面恢复焦点');
if (_lastKnownPosition != null) {
print('使用上次已知位置更新距离');
_updateDistances(_lastKnownPosition!);
}
// 重新检查权限并获取最新位置
_checkPermissionAndGetLocation();
}
void _setupUnityChannel() {
platform.setMethodCallHandler((call) async {
switch (call.method) {
case 'onUnityBackPressed':
print('收到Unity返回请求');
await _safeExitUnity();
return true;
case 'onUnityClosed':
print('Unity已关闭');
_isProcessingAR = false; // 重置处理标志
await _restartLocationService(); // 重启位置服务
return true;
case 'reinitializeLocation':
print('重新初始化位置服务');
await _restartLocationService();
return true;
default:
return false;
}
});
}
Future<void> _safeExitUnity() async {
try {
// 分步关闭流程
await Future.delayed(Duration(milliseconds: 300));
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
}
await Future.delayed(Duration(milliseconds: 200));
await SystemChannels.platform.invokeMethod('System.gc');
} catch (e) {
print('安全退出异常: $e');
}
}
Future<void> _initializeUnity() async {
try {
print('初始化Unity');
final result = await platform.invokeMethod('initializeUnity');
_isUnityInitialized = result ?? false;
print('Unity初始化结果: $_isUnityInitialized');
} catch (e) {
print('Unity初始化错误: $e');
_isUnityInitialized = false;
}
}
@override
void dispose() {
_isPageActive = false;
_cleanupLocationService();
_debounceTimer?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print('应用生命周期状态变化: $state');
if (state == AppLifecycleState.resumed && _isPageActive) {
// 应用从后台恢复,不主动检查更新
// 从Unity返回时需要重新启动Flutter的位置服务
_onPageResume();
} else if (state == AppLifecycleState.paused) {
// 切换到Unity时确保停止Flutter的位置服务
_cleanupLocationService();
} else if (state == AppLifecycleState.detached) {
// 应用可能被完全关闭,准备下次启动时的状态
_updateDialogService.resetAllCheckStatus();
}
}
// 计算两个经纬度之间的距离
double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) {
const double earthRadius = 6371000; // 地球半径,单位为米
// 将角度转换为弧度
final double dLat = _degreesToRadians(lat2 - lat1);
final double dLon = _degreesToRadians(lon2 - lon1);
// 使用 Haversine 公式计算距离
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(_degreesToRadians(lat1)) *
math.cos(_degreesToRadians(lat2)) *
math.sin(dLon / 2) *
math.sin(dLon / 2);
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return earthRadius * c; // 返回距离,单位为米
}
// 角度转弧度的辅助方法
double _degreesToRadians(double degrees) {
return degrees * math.pi / 180;
}
// 防抖动处理
void _debounce(VoidCallback callback,
{Duration duration = const Duration(milliseconds: 500)}) {
if (_debounceTimer?.isActive ?? false) {
_debounceTimer?.cancel();
}
_debounceTimer = Timer(duration, callback);
}
bool _isCheckingLocation = false; // 防止重复检查
int _locationCheckFailCount = 0; // 记录连续失败次数
DateTime? _lastLocationPromptTime; // 上次显示提示的时间
Future<void> _checkPermissionAndGetLocation() async {
if (!_isPageActive || _isCheckingLocation) {
print('页面非激活状态或已有检查正在进行,跳过');
return;
}
_isCheckingLocation = true;
try {
// 确保旧的订阅被完全取消
await _cleanupLocationService();
final hasPermission = await _getLocationPermission();
if (!hasPermission) {
print('没有位置权限');
_locationCheckFailCount++;
// 如果没有权限但有上次的位置数据,继续显示
if (_lastKnownPosition != null) {
if (mounted) {
_updateDistances(_lastKnownPosition!);
}
} else if (mounted) {
setState(() {
_showLocationPrompt = true;
_showLocation = false;
});
}
_isCheckingLocation = false;
return;
}
// 重置失败计数
_locationCheckFailCount = 0;
// 先显示界面,避免等待位置时的卡顿感
if (mounted) {
setState(() {
_showLocation = true;
_showLocationPrompt = false;
});
}
// 异步获取位置不阻塞UI
Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
timeLimit: const Duration(seconds: 5), // 添加超时限制
).then((position) {
print('获取到当前位置');
_lastKnownPosition = position; // 更新最后已知位置
_updateDistances(position);
_isCheckingLocation = false;
}).catchError((error) {
print('获取位置错误: $error');
// 获取位置失败,但权限已授予,可能是临时问题
// 如果有上次位置,继续使用
if (_lastKnownPosition != null && mounted) {
_updateDistances(_lastKnownPosition!);
}
_locationCheckFailCount++;
if (mounted && _locationCheckFailCount > 3) {
setState(() {
_showLocationPrompt = true;
_showLocation = false;
});
}
_isCheckingLocation = false;
});
// 使用低频率更新,减少计算负担
// 使用新的位置流配置
_positionStreamSubscription = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 20,
timeLimit: Duration(seconds: 30), // 增加超时时间
),
).listen(
(Position position) {
print('位置更新: ${position.latitude}, ${position.longitude}');
_lastKnownPosition = position; // 更新最后已知位置
_debounce(() {
_updateDistances(position);
_locationCheckFailCount = 0;
}, duration: const Duration(milliseconds: 300));
},
onError: (error) {
print('位置流错误: $error');
// 处理错误但不立即重启服务
if (_lastKnownPosition != null && mounted) {
_updateDistances(_lastKnownPosition!);
}
},
cancelOnError: false, // 防止因错误自动取消订阅
);
} catch (e) {
print('位置错误: $e');
} finally {
_isCheckingLocation = false;
}
}
final Map<String, double> _distanceCache = {};
String _getDistanceText(String key, double distance) {
// 缓存计算结果
if (!_distanceCache.containsKey(key) ||
(_distanceCache[key]! - distance).abs() > 10) {
// 只有距离变化超过10米才更新
_distanceCache[key] = distance;
}
return '${(_distanceCache[key]! / 1000).toStringAsFixed(2)} km';
}
// 更高效的距离计算方法
double _fastDistanceCalculation(
double lat1, double lon1, double lat2, double lon2) {
// 对于较短距离,可以使用更简单的计算方法
const double earthRadius = 6371000; // 地球半径,单位为米
// 将角度转换为弧度
final double lat1Rad = lat1 * math.pi / 180;
final double lat2Rad = lat2 * math.pi / 180;
final double dLat = (lat2 - lat1) * math.pi / 180;
final double dLon = (lon2 - lon1) * math.pi / 180;
// 使用简化的 Haversine 公式
final double a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(lat1Rad) *
math.cos(lat2Rad) *
math.sin(dLon / 2) *
math.sin(dLon / 2);
final double c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return earthRadius * c;
}
// 更新距离
void _updateDistances(Position position) {
if (!mounted) return;
// 保存最后一次位置
_lastKnownPosition = position;
// 使用更高效的计算方法
final distanceToQuancheng = _fastDistanceCalculation(
position.latitude,
position.longitude,
quanchengSquareLat,
quanchengSquareLon,
);
final distanceToDaming = _fastDistanceCalculation(
position.latitude,
position.longitude,
damingLakeLat,
damingLakeLon,
);
final distanceToHeihu = _fastDistanceCalculation(
position.latitude,
position.longitude,
heihuSpringLat,
heihuSpringLon,
);
// 添加新景点的距离计算
final distanceToWulongtan = _fastDistanceCalculation(
position.latitude,
position.longitude,
wulongtanLat,
wulongtanLon,
);
final distanceToJiefange = _fastDistanceCalculation(
position.latitude,
position.longitude,
jiefangeLat,
jiefangeLon,
);
final distanceToBaotug = _fastDistanceCalculation(
position.latitude,
position.longitude,
baotugSpringLat,
baotugSpringLon,
);
if (mounted) {
setState(() {
_showLocation = true;
_showLocationPrompt = false;
qcgc = _getDistanceText('qcgc', distanceToQuancheng);
dmh = _getDistanceText('dmh', distanceToDaming);
hhq = _getDistanceText('hhq', distanceToHeihu);
// 新增景点距离
wlt = _getDistanceText('wlt', distanceToWulongtan);
jfg = _getDistanceText('jfg', distanceToJiefange);
btq = _getDistanceText('btq', distanceToBaotug);
});
}
}
Future<bool> _getLocationPermission() async {
print('获取位置权限');
// 检查定位服务是否开启
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
print('定位服务未开启');
if (mounted) {
// 移除弹窗,直接显示顶部提示条
setState(() {
_showLocationPrompt = true;
_showLocation = false;
});
}
return false;
}
// 检查定位权限
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
print('位置权限被拒绝,请求权限');
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
print('位置权限请求被拒绝');
if (mounted) {
setState(() {
_showLocationPrompt = true;
_showLocation = false;
});
}
return false;
}
}
if (permission == LocationPermission.deniedForever) {
print('位置权限被永久拒绝');
if (mounted) {
setState(() {
_showLocationPrompt = true;
_showLocation = false;
});
}
return false;
}
print('位置权限已授予');
return true;
}
Future<bool> _initializeUnityIfNeeded() async {
if (_isUnityInitialized) return true;
try {
print('初始化Unity');
final result = await platform.invokeMethod('initializeUnity');
_isUnityInitialized = result ?? false;
print('Unity初始化结果: $_isUnityInitialized');
return _isUnityInitialized;
} catch (e) {
print('Unity初始化错误: $e');
_isUnityInitialized = false;
return false;
}
}
void _pauseLocationUpdates() {
print('暂停位置更新');
_positionStreamSubscription?.pause();
}
void _resumeLocationUpdates() {
print('恢复位置更新');
if (_positionStreamSubscription?.isPaused ?? false) {
_positionStreamSubscription?.resume();
} else {
_checkPermissionAndGetLocation();
}
}
void _launchUnityActivity() async {
if (_isProcessingAR) return;
setState(() => _isProcessingAR = true);
try {
final String? token = await _getToken();
// 停止位置服务
await _cleanupLocationService();
// 确保Unity初始化
if (!await platform.invokeMethod('initializeUnity')) {
throw Exception('Unity初始化失败');
}
// 检查权限
final bool hasPermissions = await platform.invokeMethod('checkAndRequestPermissions');
if (!hasPermissions) {
throw Exception('未获得必要权限');
}
// 启动Unity - 使用相同的方法名和参数在iOS和Android上都有相应实现
await platform.invokeMethod('start_unity', {
'returnToHome': true,
'token': token ?? '',
'sceneName': 'Main', // 默认场景iOS和Android通用
});
} catch (e) {
print('AR启动错误: $e');
// 显示错误提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('启动AR失败: ${e.toString()}'),
duration: const Duration(seconds: 3),
),
);
}
} finally {
if (mounted) {
setState(() => _isProcessingAR = false);
}
// 重启位置服务
await _restartLocationService();
}
}
Future<void> _cleanupLocationService() async {
print('清理位置服务');
try {
await _positionStreamSubscription?.cancel();
_positionStreamSubscription = null;
_lastKnownPosition = null; // 清除最后已知位置
} catch (e) {
print('清理位置服务时出错: $e');
}
}
// 新增:重启位置服务的方法
Future<void> _restartLocationService() async {
print('重启位置服务');
try {
// 确保完全清理
await _cleanupLocationService();
// 短暂延迟确保资源释放
await Future.delayed(const Duration(milliseconds: 300));
if (mounted && _isPageActive) {
await _checkPermissionAndGetLocation();
}
} catch (e) {
print('重启位置服务时出错: $e');
}
}
@override
Widget build(BuildContext context) {
print('构建HomePage界面');
return Stack(
children: [
// 允许水平滚动的背景图片容器
SingleChildScrollView(
scrollDirection: Axis.horizontal, // 允许水平滚动
child: Container(
width: MediaQuery.of(context).size.width * 1.6, // 宽度为屏幕宽度的两倍
height: MediaQuery.of(context).size.height, // 高度保持为屏幕高度
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('images/page_home.png'), // 背景图路径
fit: BoxFit.cover, // 背景图适应容器
),
),
child: Stack(
children: [
// 位置标记
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.38, // 约295/800
left: MediaQuery.of(context).size.width * 0.65, // 约365/960
title: '泉城广场',
location: qcgc,
isQuancheng: true,
),
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.14, // 约460/800
left: MediaQuery.of(context).size.width * 0.50, // 约285/960
title: '大明湖',
location: dmh,
),
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.615, // 约470/800
left: MediaQuery.of(context).size.width * 0.69, // 约435/960
title: '黑虎泉',
location: hhq,
),
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.30,
left: MediaQuery.of(context).size.width * 0.16,
title: '五龙潭',
location: wlt,
),
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.32,
left: MediaQuery.of(context).size.width * 1.1 ,
title: '解放阁',
location: jfg,
),
if (_showLocation)
_buildLocationMarker(
top: MediaQuery.of(context).size.height * 0.46,
left: MediaQuery.of(context).size.width * 0.26,
title: '趵突泉',
location: btq,
),
// 单独添加泉城广场GIF放在泉城广场标记下方
if (_showLocation)
Positioned(
top: MediaQuery.of(context).size.height * 0.40,
left: MediaQuery.of(context).size.width * 0.73,
child: GestureDetector(
onTap: () {
print('点击泉城广场GIF');
_debounce(() => _launchUnityActivity());
},
child: Image.asset(
'images/gif1.gif',
width: 160,
height: 120,
),
),
),
],
),
),
),
// 底部渐变阴影
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withOpacity(0.8),
Colors.black.withOpacity(0),
],
),
),
),
),
// 固定在屏幕右下角的 more 图标
Positioned(
bottom: 10, // 距离底部的距离
right: 10, // 距离右边的距离
child: GestureDetector(
onTap: () {
// 点击事件处理,跳转到 MorePage
print('点击更多按钮');
Navigator.pushNamed(context, '/more');
},
child: Image.asset(
'images/more.png',
width: 30, // 图标的宽度
height: 30, // 图标的高度
),
),
),
// 添加定位提示框
if (_showLocationPrompt)
Positioned(
top: 50,
right: 10,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF1F212E),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'开启定位服务进入精彩AR世界',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: () async {
print('点击开启定位服务');
if (_isCheckingLocation) return;
_isCheckingLocation = true;
try {
// 直接打开系统定位设置
await Geolocator.openLocationSettings();
// 延迟3秒后重新检查定位状态
if (mounted) {
Future.delayed(const Duration(seconds: 3), () {
_isCheckingLocation = false;
_checkPermissionAndGetLocation();
});
}
} catch (e) {
print('打开定位设置失败: $e');
_isCheckingLocation = false;
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFF96FBE9).withOpacity(0.2),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: const Color(0xFF96FBE9).withOpacity(0.8)),
),
child: const Text(
'开启',
style: TextStyle(
color: Colors.white,
fontSize: 12,
),
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
print('关闭定位提示');
setState(() {
_showLocationPrompt = false;
});
},
child: const Icon(
Icons.close,
color: Colors.white,
size: 20,
),
),
],
),
),
),
// 添加AR处理中的加载指示器
// if (_isProcessingAR)
// Container(
// color: Colors.black.withOpacity(0.5),
// child: const Center(
// child: CircularProgressIndicator(
// valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
// ),
// ),
// ),
],
);
}
// 抽取位置标记的构建逻辑,避免重复代码
Widget _buildLocationMarker({
required double top,
required double left,
required String title,
required String location,
bool isQuancheng = false,
}) {
return Positioned(
top: top,
left: left,
child: Column(
children: [
GestureDetector(
onTap: () {
if (isQuancheng) {
print('点击泉城广场标记');
_debounce(() => _launchUnityActivity());
}
},
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [
Color(0xFF3A9FAF),
Color(0xFF58C082),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(20),
),
child: Container(
margin: const EdgeInsets.all(1), // 创建1像素的边框效果
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(19), // 略小于外部圆角
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
// 非泉城广场景点添加 nouse.png 图标
if (!isQuancheng)
Padding(
padding: const EdgeInsets.only(right: 4),
child: Image.asset(
'images/nouse.png',
width: 16,
height: 16,
color: const Color(0xFFC1C1C1), // 设置灰色
),
),
Text(
title,
style: TextStyle(
fontSize: 12,
// 泉城广场黑色,其他景点灰色
color: isQuancheng
? Colors.black87
: const Color(0xFFC1C1C1),
fontWeight: FontWeight.w500,
),
),
Image.asset(
'images/location.png',
width: 12,
height: 12,
// 泉城广场黑色,其他景点灰色
color: isQuancheng ? Colors.black : const Color(0xFFC1C1C1),
),
Text(
location,
style: TextStyle(
fontSize: 12,
// 泉城广场黑色,其他景点灰色
color: isQuancheng
? Colors.black87
: const Color(0xFFC1C1C1),
fontWeight: FontWeight.w500,
),
),
],
),
if (isQuancheng)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'点击此处进入MR',
style: TextStyle(
color: Color(0xFF3A9FAF),
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
),
],
),
);
}
}