完成远程遥控页面的优化,包括 折叠功能栏 点击振动反馈 ,急停的读秒进度条 ,左右的振动反馈 ,锁屏的功能按钮的开发(禁用上下左右的滑轮)
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
<!-- 如果还需要扫描闪光灯控制,可选添加 -->
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
1
assets/svgs/remote_recognition_off.svg
Normal file
1
assets/svgs/remote_recognition_off.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye-off-icon lucide-eye-off"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>
|
||||
|
After Width: | Height: | Size: 517 B |
1
assets/svgs/remote_recognition_on.svg
Normal file
1
assets/svgs/remote_recognition_on.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-eye-icon lucide-scan-eye"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>
|
||||
|
After Width: | Height: | Size: 515 B |
@@ -2,3 +2,7 @@ class Failure {
|
||||
final String message;
|
||||
Failure(this.message);
|
||||
}
|
||||
|
||||
class NetworkFailure extends Failure {
|
||||
NetworkFailure(super.message);
|
||||
}
|
||||
|
||||
@@ -223,6 +223,9 @@ class NetMessageDispatcher {
|
||||
fullData[1] == 0xAA &&
|
||||
fullData[2] == 0x01) {
|
||||
|
||||
if(fullData[5] == 0x02){
|
||||
debugPrint('下位机回复收到指令 - 完整数据包:${fullData.join(" ")}');
|
||||
}
|
||||
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
|
||||
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
|
||||
if (fullData.length > 5 && fullData[5] == 0x01) {
|
||||
|
||||
@@ -265,7 +265,7 @@ class PathPlanner {
|
||||
|
||||
final routePlanSendEntity = RoutePlanSendEntity(
|
||||
commandType: 0x01,
|
||||
pointCounts: pointIndex, // 🔥 使用点编号,而不是固定的 1
|
||||
pointCounts: 1, // 🔥 使用点编号,而不是固定的 1
|
||||
targetLatitude: entity.latitude,
|
||||
targetLongitude: entity.longitude,
|
||||
speed: 1000,
|
||||
|
||||
@@ -18,6 +18,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
Timer? _timer;
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
final NetMessageDispatcher dispatcher;
|
||||
StreamSubscription? _deviceStatusSub;//🔥 新增:用于监听 DeviceStat
|
||||
|
||||
RemoteControlCubit(this._repository, this._requestControlPermissionUseCase, this.dispatcher)
|
||||
: super(
|
||||
@@ -30,6 +31,59 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
|
||||
}
|
||||
|
||||
// 🔥 新增:监听 DeviceStatusBloc 的状态更新
|
||||
void subscribeToDeviceStatus(Stream deviceStatusStream) {
|
||||
_deviceStatusSub?.cancel();
|
||||
|
||||
_deviceStatusSub = deviceStatusStream.listen((deviceState) {
|
||||
try {
|
||||
// 🔥 关键:从 DeviceStatusBloc 的 state 中提取数据
|
||||
// 这里假设 deviceState 是 DevicesCubit 的 state
|
||||
// 你需要根据实际的 DevicesCubit state 类型来调整
|
||||
|
||||
debugPrint('📡 [收到设备状态更新] deviceState: $deviceState');
|
||||
|
||||
// 如果 DevicesCubit 的 state 中有 runningStatusModel 或类似字段
|
||||
// 方式 1:直接同步整个 RunningStatusModel
|
||||
if (deviceState is Map && deviceState.containsKey('runningStatusModel')) {
|
||||
final newStatus = deviceState['runningStatusModel'] as RunningStatusModel;
|
||||
_updateStatusFromDevice(newStatus);
|
||||
}
|
||||
// 方式 2:如果 DevicesCubit 直接暴露 voltage, battery 等字段
|
||||
else if (deviceState is Map) {
|
||||
final voltage = deviceState['voltage'] as int? ?? 0;
|
||||
final battery = deviceState['battery'] as int? ?? 0;
|
||||
|
||||
emit(state.copyWith(
|
||||
voltage: voltage,
|
||||
battery: battery,
|
||||
));
|
||||
|
||||
debugPrint('📡 [同步设备状态] 电压:${voltage}V, 电量:${battery}%');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [同步设备状态失败] $e');
|
||||
}
|
||||
});
|
||||
|
||||
debugPrint('✅ [DeviceStatus] 已建立监听');
|
||||
}
|
||||
|
||||
|
||||
// 🔥 辅助方法:更新运行状态
|
||||
void _updateStatusFromDevice(RunningStatusModel newStatus) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(
|
||||
runningStatusModel: newStatus,
|
||||
voltage:int.tryParse(newStatus.voltage) ?? 0,
|
||||
battery: int.tryParse(newStatus.voltage) ?? 0,
|
||||
// 如果需要同步其他字段,在这里添加
|
||||
// controlMode: newStatus.controlMode,
|
||||
));
|
||||
|
||||
debugPrint('✅ [更新运行状态] 电压:${newStatus.voltage}V, 电量:${newStatus.battery}%');
|
||||
}
|
||||
}
|
||||
// 1. 初始化回包监听 (如 0x12 权限)
|
||||
// void _initPacketListener() {
|
||||
// _repository.responseStream.listen((packet) {
|
||||
@@ -170,7 +224,9 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
emit(state.copyWith(status: RemoteControlStatus.initial));
|
||||
}
|
||||
|
||||
void toggleLock() {}
|
||||
void toggleLock() {
|
||||
emit(state.copyWith(isLocked: !state.isLocked));
|
||||
}
|
||||
|
||||
void togglePermissionDialog(bool show) {
|
||||
emit(state.copyWith(showPermissionRequestDialog: show));
|
||||
@@ -188,6 +244,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
@override
|
||||
Future<void> close() {
|
||||
_timer?.cancel(); // 退出页面时务必销毁定时器
|
||||
_deviceStatusSub?.cancel(); // 🔥 确保清理订阅
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -268,6 +325,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
debugPrint(' [点火指令] ${i}');
|
||||
updateFunction(ignition: i);
|
||||
}
|
||||
// 发送障碍物识别指令
|
||||
void toggleObstacleRecognition() {
|
||||
emit(state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag));
|
||||
}
|
||||
|
||||
void toggleTopLeftExpand() {
|
||||
emit(state.copyWith(topRightIsExpanded: !state.topRightIsExpanded));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ class RemoteControlState extends Equatable {
|
||||
final bool showLeftPip;
|
||||
final bool showRightPip;
|
||||
|
||||
final bool topRightIsExpanded; // 顶部右侧按钮展开状态
|
||||
final bool obstacleRecognitionFlag; // 障碍物识别标志位(这是UI显示的)
|
||||
|
||||
final String obstacleFlag; //障碍物标志位
|
||||
|
||||
const RemoteControlState({
|
||||
@@ -44,6 +47,8 @@ class RemoteControlState extends Equatable {
|
||||
this.showRightPip = true,
|
||||
this.obstacleFlag = '',
|
||||
required this.runningStatusModel,
|
||||
this.topRightIsExpanded = false,
|
||||
this.obstacleRecognitionFlag = true,
|
||||
});
|
||||
|
||||
// 方便 UI 更新部分属性
|
||||
@@ -64,6 +69,9 @@ class RemoteControlState extends Equatable {
|
||||
bool? showRightPip,
|
||||
RunningStatusModel? runningStatusModel,
|
||||
String? obstacleFlag,
|
||||
bool? topRightIsExpanded,
|
||||
bool? obstacleRecognitionFlag,
|
||||
|
||||
}) {
|
||||
return RemoteControlState(
|
||||
status: status ?? this.status,
|
||||
@@ -83,6 +91,9 @@ class RemoteControlState extends Equatable {
|
||||
showRightPip: showRightPip ?? this.showRightPip,
|
||||
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
|
||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||
topRightIsExpanded: topRightIsExpanded ?? this.topRightIsExpanded,
|
||||
obstacleRecognitionFlag: obstacleRecognitionFlag ?? this.obstacleRecognitionFlag,
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,5 +115,10 @@ class RemoteControlState extends Equatable {
|
||||
showRightPip,
|
||||
runningStatusModel,
|
||||
obstacleFlag,
|
||||
topRightIsExpanded,
|
||||
obstacleRecognitionFlag,
|
||||
];
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -25,6 +27,7 @@ class RemoteControlPage extends StatefulWidget {
|
||||
|
||||
class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
String _videoStreamUrl = "";
|
||||
StreamSubscription? _deviceStatusSubscription;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -33,8 +36,17 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
// 2. 隐藏状态栏和虚拟按键
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||
|
||||
// 🔥 关键:订阅 DeviceStatusBloc 的状态流
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<RemoteControlCubit>().startControlLoop();
|
||||
final remoteCubit = context.read<RemoteControlCubit>();
|
||||
final devicesCubit = context.read<DevicesCubit>();
|
||||
|
||||
remoteCubit.startControlLoop();
|
||||
// 🔥 关键:订阅 DeviceStatusBloc 的状态流
|
||||
remoteCubit.subscribeToDeviceStatus(devicesCubit.stream);
|
||||
|
||||
debugPrint('🚀 [遥控页面] 已启动控制循环和设备状态监听');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +54,8 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||
void dispose() {
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
_deviceStatusSubscription?.cancel();
|
||||
context.read<RemoteControlCubit>().stopControlLoop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
@@ -121,6 +122,8 @@ class CenterControlArea extends StatelessWidget {
|
||||
default:
|
||||
debugPrint('⚠️ 未知的 label: $label');
|
||||
}
|
||||
// 🔥 在指令执行后触发震动反馈(增强手感)
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
|
||||
// 🔥 底盘的业务逻辑(左滑块)
|
||||
|
||||
@@ -17,7 +17,7 @@ class EmergencyStopButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
||||
with TickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _progressController;
|
||||
bool _isPressing = false;
|
||||
|
||||
@@ -28,6 +28,13 @@ class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
// 添加监听器,确保动画值变化时重建 UI
|
||||
_progressController.addListener(() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,94 +43,118 @@ class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _resetProgress() {
|
||||
setState(() => _isPressing = false);
|
||||
_progressController.stop();
|
||||
_progressController.value = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 监听 Cubit 中的急停状态
|
||||
final isEmergencyActive = context
|
||||
.watch<RemoteControlCubit>()
|
||||
.state
|
||||
.isEmergency;
|
||||
|
||||
final width = widget.width * 0.65;
|
||||
final double indicatorSize = width + 10;
|
||||
final double indicatorSize = width + 20;
|
||||
|
||||
return GestureDetector(
|
||||
// 1. 处理点击:仅在未急停时触发
|
||||
onTap: () {
|
||||
if (!isEmergencyActive) {
|
||||
context.read<RemoteControlCubit>().updateEmergency(true);
|
||||
HapticFeedback.heavyImpact(); // 震动反馈
|
||||
}
|
||||
},
|
||||
// 2. 处理长按开始:仅在已急停时触发解除逻辑
|
||||
onLongPressStart: (_) {
|
||||
if (isEmergencyActive) {
|
||||
setState(() => _isPressing = true);
|
||||
_progressController.forward(from: 0).then((_) {
|
||||
if (_isPressing) {
|
||||
// 进度走完且仍在按压
|
||||
context.read<RemoteControlCubit>().updateEmergency(false);
|
||||
HapticFeedback.vibrate();
|
||||
setState(() => _isPressing = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
// 3. 处理松手:重置进度
|
||||
onLongPressEnd: (_) {
|
||||
_isPressing = false;
|
||||
_progressController.stop();
|
||||
_progressController.value = 0;
|
||||
setState(() {});
|
||||
},
|
||||
return SizedBox(
|
||||
width: indicatorSize,
|
||||
height: indicatorSize,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 对应 Compose 的 Canvas 绘制进度环
|
||||
if (_isPressing)
|
||||
SizedBox(
|
||||
// 进度环 - 只在按压时显示
|
||||
AnimatedOpacity(
|
||||
opacity: _isPressing ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
width: indicatorSize,
|
||||
height: indicatorSize,
|
||||
child: CircularProgressIndicator(
|
||||
value: _progressController.value,
|
||||
strokeWidth: 10,
|
||||
color: Colors.red.withOpacity(0.8),
|
||||
strokeWidth: 6,
|
||||
color: Colors.red.withOpacity(0.9),
|
||||
backgroundColor: Colors.red.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Container(
|
||||
width: width,
|
||||
height: width,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// 根据状态切换颜色,对应 Color(0x80C53030)
|
||||
color: isEmergencyActive
|
||||
? Colors.red.withOpacity(0.5)
|
||||
: const Color(0x80C53030),
|
||||
boxShadow: isEmergencyActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.red.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset('assets/svgs/remote_alert.svg',width: width * 0.4,color: Colors.white.withValues(alpha: 0.5),),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
isEmergencyActive ? "急停中" : "急停",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
// 按钮主体
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('🔴 [点击] 触发急停');
|
||||
if (!isEmergencyActive) {
|
||||
context.read<RemoteControlCubit>().updateEmergency(true);
|
||||
HapticFeedback.heavyImpact();
|
||||
}
|
||||
},
|
||||
onLongPressStart: (_) {
|
||||
debugPrint('🔴 [长按开始] isEmergency=$isEmergencyActive');
|
||||
if (isEmergencyActive && !_isPressing) {
|
||||
setState(() => _isPressing = true);
|
||||
debugPrint('🔴 [开始动画] 从 0 开始');
|
||||
_progressController.reset();
|
||||
_progressController.forward().then((_) {
|
||||
debugPrint('🔴 [动画完成] _isPressing=$_isPressing');
|
||||
if (_isPressing && mounted) {
|
||||
debugPrint('✅ [解除急停] 执行');
|
||||
context.read<RemoteControlCubit>().updateEmergency(false);
|
||||
HapticFeedback.vibrate();
|
||||
setState(() => _isPressing = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (_) {
|
||||
debugPrint('🔴 [长按结束]');
|
||||
if (_isPressing) {
|
||||
_resetProgress();
|
||||
}
|
||||
},
|
||||
onLongPressCancel: () {
|
||||
debugPrint('🔴 [长按取消]');
|
||||
if (_isPressing) {
|
||||
_resetProgress();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: width,
|
||||
height: width,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isEmergencyActive
|
||||
? Colors.red.withOpacity(0.5)
|
||||
: const Color(0x80C53030),
|
||||
boxShadow: isEmergencyActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.red.withValues(alpha: 0.5),
|
||||
blurRadius: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/svgs/remote_alert.svg',
|
||||
width: width * 0.4,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
isEmergencyActive ? "急停中" : "急停",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
@@ -32,10 +33,12 @@ class LeftJoystickArea extends StatelessWidget {
|
||||
onValueChanged: (value) {
|
||||
// 对应 viewModel.updateOriginY(y)
|
||||
context.read<RemoteControlCubit>().updateOriginY(value.y);
|
||||
_triggerVibration();
|
||||
},
|
||||
onPress: () {
|
||||
// 对应 VibrateOnce(current, 100)
|
||||
HapticFeedback.mediumImpact();
|
||||
//HapticFeedback.mediumImpact();
|
||||
_triggerVibration();
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -53,4 +56,16 @@ class LeftJoystickArea extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
void _triggerVibration() {
|
||||
Vibration.hasVibrator().then((hasVibrator) {
|
||||
if (hasVibrator ?? false) {
|
||||
Vibration.vibrate(duration: 100);
|
||||
debugPrint('📳 [右摇杆] 震动执行 - 100ms');
|
||||
} else {
|
||||
debugPrint('⚠️ [右摇杆] 设备不支持震动');
|
||||
}
|
||||
}).catchError((e) {
|
||||
debugPrint('❌ [右摇杆] 震动失败:$e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
import '../bloc/remote_control_cubit.dart';
|
||||
|
||||
@@ -33,11 +34,12 @@ class RightJoystickArea extends StatelessWidget {
|
||||
// 对应 viewModel.updateOriginX(x)
|
||||
/// context.read<RemoteControlCubit>().updateOriginY(value.x);
|
||||
context.read<RemoteControlCubit>().updateOriginX(value.x);
|
||||
_triggerVibration();
|
||||
},
|
||||
onPress: () {
|
||||
// 对应 VibrateOnce(current, 100)
|
||||
HapticFeedback.mediumImpact();
|
||||
},
|
||||
onPress: () {
|
||||
_triggerVibration();
|
||||
}
|
||||
,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -55,4 +57,16 @@ class RightJoystickArea extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
void _triggerVibration() {
|
||||
Vibration.hasVibrator().then((hasVibrator) {
|
||||
if (hasVibrator ?? false) {
|
||||
Vibration.vibrate(duration: 100);
|
||||
debugPrint('📳 [右摇杆] 震动执行 - 100ms');
|
||||
} else {
|
||||
debugPrint('⚠️ [右摇杆] 设备不支持震动');
|
||||
}
|
||||
}).catchError((e) {
|
||||
debugPrint('❌ [右摇杆] 震动失败:$e');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
// 监听全局设备状态
|
||||
final deviceState = context.watch<DevicesCubit>().state;
|
||||
final device = deviceState.selectedDevice;
|
||||
var _remoteControlCubit = context.read<RemoteControlCubit>();
|
||||
ControlMode _parseControlMode(String modeString) {
|
||||
switch (modeString.toUpperCase()) {
|
||||
case 'BLE':
|
||||
@@ -38,12 +39,12 @@ class TopStatusBar extends StatelessWidget {
|
||||
final remoteState = context.watch<RemoteControlCubit>().state;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.fromLTRB(1, 12, 1, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
|
||||
_buildIconButton("assets/svgs/remote_back.svg", () => context.pop()),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// 2. 控制状态 (对应 StatusChipLeft)
|
||||
StatusChip(
|
||||
@@ -74,28 +75,81 @@ class TopStatusBar extends StatelessWidget {
|
||||
|
||||
// 4. 刷新按钮
|
||||
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
|
||||
const SizedBox(width: 25),
|
||||
// const SizedBox(width: 8),
|
||||
// 5. 火技能按钮 (对应 SmallFunctionButton)
|
||||
//_buildIconButton("assets/svgs/fire.svg", () {}),
|
||||
_buildSliderBox("割刀", false,context),
|
||||
_buildSliderBox("火技能按钮", false,context),
|
||||
|
||||
const Spacer(),
|
||||
const Spacer(),
|
||||
_buildExpandIconButton(
|
||||
iconPath: "assets/svgs/remote_expand.svg",
|
||||
selectedIconPath: "assets/svgs/remote_unexpand.svg",
|
||||
isSelected: remoteState.topRightIsExpanded,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleTopLeftExpand();
|
||||
},
|
||||
),
|
||||
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
child: Row(
|
||||
children: !remoteState.topRightIsExpanded
|
||||
? [
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_recognition_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_recognition_on.svg",
|
||||
isSelected: remoteState.obstacleRecognitionFlag,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleObstacleRecognition();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_video_left_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_video_left.svg",
|
||||
isSelected: remoteState.showLeftPip,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleLeftPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSwitchIconButton(
|
||||
iconPath: "assets/svgs/remote_video_right_off.svg",
|
||||
selectedIconPath: "assets/svgs/remote_video_right.svg",
|
||||
isSelected: remoteState.showRightPip,
|
||||
onTap: (){
|
||||
_remoteControlCubit.toggleRightPip();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildControlModeChip(remoteState.runningStatusModel.controlMode),
|
||||
const SizedBox(width: 8),
|
||||
_buildVoltageChip(remoteState.runningStatusModel.voltage),
|
||||
const SizedBox(width: 8),
|
||||
_buildPingChip(remoteState.ping),
|
||||
const SizedBox(width: 8),
|
||||
]
|
||||
: [], // 折叠时数组为空
|
||||
),
|
||||
),
|
||||
// TODO
|
||||
_buildControlModeChip( _parseControlMode(remoteState.runningStatusModel.controlMode)),
|
||||
// _buildControlModeChip( _parseControlMode(remoteState.runningStatusModel.controlMode)),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
_buildVoltageChip(remoteState.voltage),
|
||||
//_buildVoltageChip(remoteState.voltage),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 5. 信号延迟 (对应 pingStatusChip)
|
||||
_buildPingChip(remoteState.ping),
|
||||
const SizedBox(width: 8),
|
||||
//_buildPingChip(remoteState.ping),
|
||||
// const SizedBox(width: 8),
|
||||
|
||||
// 6. 电量 (对应 StatusChipRight)
|
||||
_buildBatteryChip(remoteState.battery),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -129,6 +183,53 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchIconButton({
|
||||
required String iconPath, // 默认图标路径
|
||||
required String selectedIconPath, // 选中后的图标路径
|
||||
required VoidCallback onTap, // 点击事件
|
||||
bool isSelected = false, // 是否选中状态
|
||||
double size = 32, // 按钮尺寸
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
// 增加颜色切换动画
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
// 选中时为深蓝色,未选中时为半透明灰色
|
||||
color: isSelected
|
||||
? const Color(0xFF0078D4)
|
||||
: Colors.grey.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.transparent
|
||||
: Colors.white.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0078D4).withOpacity(0.4),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
padding: const EdgeInsets.all(7),
|
||||
child: SvgPicture.asset(
|
||||
// 根据状态切换图片
|
||||
isSelected ? selectedIconPath : iconPath,
|
||||
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPingChip(int ping) {
|
||||
Color color = ping < 100
|
||||
? Colors.green
|
||||
@@ -141,27 +242,32 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlModeChip(ControlMode mode) {
|
||||
Widget _buildControlModeChip(String mode) {
|
||||
String displayText;
|
||||
switch (mode) {
|
||||
case ControlMode.BLE:
|
||||
displayText = '蓝牙模式';
|
||||
break;
|
||||
case ControlMode.LOCAL:
|
||||
displayText = '本地模式';
|
||||
break;
|
||||
case ControlMode.TCP:
|
||||
displayText = 'TCP模式';
|
||||
break;
|
||||
case ControlMode.NONE:
|
||||
displayText = '无模式';
|
||||
break;
|
||||
case ControlMode.OTHER:
|
||||
displayText = '其他模式';
|
||||
break;
|
||||
default:
|
||||
displayText = '未知模式';
|
||||
if(mode == ''){
|
||||
displayText = '无模式';
|
||||
}else{
|
||||
displayText = mode;
|
||||
}
|
||||
// switch (mode) {
|
||||
// case "蓝牙模式":
|
||||
// displayText = '蓝牙模式';
|
||||
// break;
|
||||
// case "本地模式":
|
||||
// displayText = '本地模式';
|
||||
// break;
|
||||
// case "TCP模式":
|
||||
// displayText = 'TCP模式';
|
||||
// break;
|
||||
// case "":
|
||||
// displayText = '无模式';
|
||||
// break;
|
||||
// case "其他模式":
|
||||
// displayText = '其他模式';
|
||||
// break;
|
||||
// default:
|
||||
// displayText = '未知模式';
|
||||
// }
|
||||
|
||||
return StatusChip(
|
||||
text: displayText,
|
||||
@@ -171,7 +277,43 @@ class TopStatusBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVoltageChip(int voltage) {
|
||||
Widget _buildExpandIconButton({
|
||||
required String iconPath, // 默认图标路径
|
||||
required String selectedIconPath, // 选中后的图标路径
|
||||
required VoidCallback onTap, // 点击事件
|
||||
bool isSelected = false, // 是否选中状态
|
||||
double size = 28, // 按钮尺寸
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
// 增加颜色切换动画
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
// 选中时为深蓝色,未选中时为半透明灰色
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 0.5,
|
||||
),
|
||||
boxShadow: null,
|
||||
),
|
||||
padding: const EdgeInsets.all(7),
|
||||
child: SvgPicture.asset(
|
||||
// 根据状态切换图片
|
||||
isSelected ? selectedIconPath : iconPath,
|
||||
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildVoltageChip(String voltage) {
|
||||
return StatusChip(
|
||||
text: "$voltage V",
|
||||
color: Color(0xFFFACC15).withValues(alpha: 0.6),
|
||||
@@ -205,7 +347,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
// 2. 使用 StatusChip 包装,但 icon 传入 SvgPicture
|
||||
return StatusChip(
|
||||
text: "$level%",
|
||||
color: color.withValues(alpha: 0.6),
|
||||
color: color.withValues(alpha: 0.5),
|
||||
svgColor: color,
|
||||
// 如果你的 StatusChip 已经改为接受 Widget,则直接传 SvgPicture
|
||||
// 如果 StatusChip 目前只接受 IconData,你需要按我之前的建议给它加个 leading 参数
|
||||
@@ -215,7 +357,7 @@ class TopStatusBar extends StatelessWidget {
|
||||
|
||||
Widget _buildSliderBox(String label, bool isLeft, BuildContext context) {
|
||||
// 1. 核心尺寸调整:足够宽的容器解决拥挤,高度匹配状态栏
|
||||
const double boxWidth = 96; // 水平宽度放大,容纳左右图标
|
||||
const double boxWidth = 76; // 水平宽度放大,容纳左右图标
|
||||
const double boxHeight = 32; // 高度和其他按钮保持一致
|
||||
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
|
||||
enum SliderAxis { horizontal, vertical }
|
||||
|
||||
@@ -41,25 +42,91 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
double _alignmentValue = 0.0;
|
||||
bool _isExpanded = false;
|
||||
Timer? _tickTimer;
|
||||
int? _lastVibrationState;
|
||||
Timer? _continuousVibrateTimer;
|
||||
bool _hasCheckedVibrator = false;
|
||||
bool? _hasVibrator;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkVibrator();
|
||||
}
|
||||
|
||||
Future<void> _checkVibrator() async {
|
||||
_hasVibrator = await Vibration.hasVibrator();
|
||||
setState(() {
|
||||
_hasCheckedVibrator = true;
|
||||
});
|
||||
debugPrint('🔍 震动器检测:${_hasVibrator == true ? "有" : "无"}');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tickTimer?.cancel();
|
||||
_continuousVibrateTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _triggerVibration(int duration, String reason) async {
|
||||
if (!_hasCheckedVibrator) return;
|
||||
|
||||
try {
|
||||
if (_hasVibrator == true) {
|
||||
await Vibration.vibrate(duration: duration);
|
||||
debugPrint('📳 $reason - 震动 ${duration}ms');
|
||||
} else {
|
||||
debugPrint('⚠️ $reason - 设备不支持震动');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ $reason - 震动失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
void _vibrate(int state) {
|
||||
if (state != _lastVibrationState) {
|
||||
_triggerVibration(50, '状态变化');
|
||||
_lastVibrationState = state;
|
||||
}
|
||||
}
|
||||
|
||||
void _startContinuousVibration() {
|
||||
_continuousVibrateTimer?.cancel();
|
||||
_continuousVibrateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) {
|
||||
if (!_isExpanded) return;
|
||||
|
||||
if (_alignmentValue < -0.5 || _alignmentValue > 0.5) {
|
||||
_triggerVibration(30, '连续震动');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _stopContinuousVibration() {
|
||||
_continuousVibrateTimer?.cancel();
|
||||
_continuousVibrateTimer = null;
|
||||
}
|
||||
|
||||
void _startTicking() {
|
||||
_tickTimer?.cancel();
|
||||
_tickTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||
if (!_isExpanded) return;
|
||||
|
||||
int currentState;
|
||||
if (_alignmentValue < -0.5) {
|
||||
currentState = -1;
|
||||
widget.onStart?.call();
|
||||
} else if (_alignmentValue > 0.5) {
|
||||
currentState = 1;
|
||||
widget.onEnd?.call();
|
||||
} else {
|
||||
currentState = 0;
|
||||
widget.onCenter?.call();
|
||||
}
|
||||
|
||||
_vibrate(currentState);
|
||||
});
|
||||
|
||||
_startContinuousVibration();
|
||||
}
|
||||
|
||||
void _safeSetExpanded(bool expanded) {
|
||||
@@ -70,6 +137,8 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
if (!expanded) {
|
||||
_alignmentValue = 0.0;
|
||||
_tickTimer?.cancel();
|
||||
_continuousVibrateTimer?.cancel();
|
||||
_lastVibrationState = null;
|
||||
}
|
||||
});
|
||||
if (expanded) _startTicking();
|
||||
@@ -88,7 +157,13 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanDown: (_) => _safeSetExpanded(true),
|
||||
onPanDown: (_) {
|
||||
debugPrint('👆 按下');
|
||||
if (_hasCheckedVibrator) {
|
||||
_triggerVibration(40, '按下');
|
||||
}
|
||||
_safeSetExpanded(true);
|
||||
},
|
||||
onPanUpdate: (details) {
|
||||
if (!_isExpanded) return;
|
||||
setState(() {
|
||||
@@ -97,16 +172,23 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
});
|
||||
},
|
||||
onPanEnd: (_) {
|
||||
debugPrint('✋ 松开');
|
||||
if (widget.isSpring) widget.onCenter?.call();
|
||||
HapticFeedback.lightImpact();
|
||||
if (_hasCheckedVibrator) {
|
||||
_triggerVibration(40, '松开');
|
||||
}
|
||||
_lastVibrationState = null;
|
||||
_stopContinuousVibration();
|
||||
_safeSetExpanded(false);
|
||||
},
|
||||
onPanCancel: () {
|
||||
debugPrint('❌ 取消');
|
||||
_safeSetExpanded(false);
|
||||
},
|
||||
onPanCancel: () => _safeSetExpanded(false),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
// 1. 轨道背景:从中向外展开
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
@@ -120,10 +202,8 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
child: _buildSegmentedContent(isVertical, maxLength, minLength),
|
||||
),
|
||||
|
||||
// 2. 核心修改:文字位置固定逻辑
|
||||
// 我们用一个和“缩起状态”一样大的 SizedBox 作为参考
|
||||
if (!_isExpanded)
|
||||
IgnorePointer( // 防止文字遮挡点击
|
||||
IgnorePointer(
|
||||
child: SizedBox(
|
||||
width: minLength,
|
||||
height: minLength,
|
||||
@@ -132,7 +212,7 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
bottom: -22, // 这里的 -22 是相对于“缩起时正方形”的底部
|
||||
bottom: -22,
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: TextStyle(
|
||||
@@ -220,4 +300,4 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -1420,6 +1420,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
vibration:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: vibration
|
||||
sha256: "9bb06614c69260f8bd11c80fe01ed7988905cf00e3417d656c2647e41f261d87"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.8"
|
||||
vibration_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vibration_platform_interface
|
||||
sha256: "4134fbfcd427b59a7a91f8733292e4e9b29a7f1e8224ff0d80f5745fbf0743c6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
visibility_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -125,7 +125,7 @@ dependencies:
|
||||
flutter_markdown: ^0.7.1
|
||||
|
||||
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
|
||||
|
||||
vibration: ^3.1.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user