diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f282b750..0f9a999b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
+
diff --git a/assets/svgs/remote_recognition_off.svg b/assets/svgs/remote_recognition_off.svg
new file mode 100644
index 00000000..f4ece88e
--- /dev/null
+++ b/assets/svgs/remote_recognition_off.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/svgs/remote_recognition_on.svg b/assets/svgs/remote_recognition_on.svg
new file mode 100644
index 00000000..30ccedf3
--- /dev/null
+++ b/assets/svgs/remote_recognition_on.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/lib/core/error/failure.dart b/lib/core/error/failure.dart
index 7d2e1cb4..96dfe2de 100644
--- a/lib/core/error/failure.dart
+++ b/lib/core/error/failure.dart
@@ -2,3 +2,7 @@ class Failure {
final String message;
Failure(this.message);
}
+
+class NetworkFailure extends Failure {
+ NetworkFailure(super.message);
+}
diff --git a/lib/core/network/net_message_dispatcher.dart b/lib/core/network/net_message_dispatcher.dart
index 6503620b..0eca1058 100644
--- a/lib/core/network/net_message_dispatcher.dart
+++ b/lib/core/network/net_message_dispatcher.dart
@@ -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) {
diff --git a/lib/features/devices/data/repositories/route_planning_repository_impl.dart b/lib/features/devices/data/repositories/route_planning_repository_impl.dart
index 926b571c..5c1e97ca 100644
--- a/lib/features/devices/data/repositories/route_planning_repository_impl.dart
+++ b/lib/features/devices/data/repositories/route_planning_repository_impl.dart
@@ -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,
diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart
index a519114c..6b9f1474 100644
--- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart
+++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart
@@ -18,6 +18,7 @@ class RemoteControlCubit extends Cubit {
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 {
}
+ // 🔥 新增:监听 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 {
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 {
@override
Future close() {
_timer?.cancel(); // 退出页面时务必销毁定时器
+ _deviceStatusSub?.cancel(); // 🔥 确保清理订阅
return super.close();
}
@@ -268,6 +325,14 @@ class RemoteControlCubit extends Cubit {
debugPrint(' [点火指令] ${i}');
updateFunction(ignition: i);
}
+ // 发送障碍物识别指令
+ void toggleObstacleRecognition() {
+ emit(state.copyWith(obstacleRecognitionFlag: !state.obstacleRecognitionFlag));
+ }
+
+ void toggleTopLeftExpand() {
+ emit(state.copyWith(topRightIsExpanded: !state.topRightIsExpanded));
+ }
diff --git a/lib/features/remote_control/presentation/bloc/remote_control_state.dart b/lib/features/remote_control/presentation/bloc/remote_control_state.dart
index 6226ee83..2942376d 100644
--- a/lib/features/remote_control/presentation/bloc/remote_control_state.dart
+++ b/lib/features/remote_control/presentation/bloc/remote_control_state.dart
@@ -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,
];
+
+
+
}
diff --git a/lib/features/remote_control/presentation/pages/remote_control_page.dart b/lib/features/remote_control/presentation/pages/remote_control_page.dart
index 52861c86..45a9a575 100644
--- a/lib/features/remote_control/presentation/pages/remote_control_page.dart
+++ b/lib/features/remote_control/presentation/pages/remote_control_page.dart
@@ -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 {
String _videoStreamUrl = "";
+ StreamSubscription? _deviceStatusSubscription;
@override
void initState() {
super.initState();
@@ -33,8 +36,17 @@ class _RemoteControlPageState extends State {
// 2. 隐藏状态栏和虚拟按键
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
+ // 🔥 关键:订阅 DeviceStatusBloc 的状态流
+
WidgetsBinding.instance.addPostFrameCallback((_) {
- context.read().startControlLoop();
+ final remoteCubit = context.read();
+ final devicesCubit = context.read();
+
+ remoteCubit.startControlLoop();
+ // 🔥 关键:订阅 DeviceStatusBloc 的状态流
+ remoteCubit.subscribeToDeviceStatus(devicesCubit.stream);
+
+ debugPrint('🚀 [遥控页面] 已启动控制循环和设备状态监听');
});
}
@@ -42,6 +54,8 @@ class _RemoteControlPageState extends State {
void dispose() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+ _deviceStatusSubscription?.cancel();
+ context.read().stopControlLoop();
super.dispose();
}
diff --git a/lib/features/remote_control/presentation/widgets/center_control_area.dart b/lib/features/remote_control/presentation/widgets/center_control_area.dart
index 71378143..d0bc6247 100644
--- a/lib/features/remote_control/presentation/widgets/center_control_area.dart
+++ b/lib/features/remote_control/presentation/widgets/center_control_area.dart
@@ -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();
}
// 🔥 底盘的业务逻辑(左滑块)
diff --git a/lib/features/remote_control/presentation/widgets/emergency_stop_button.dart b/lib/features/remote_control/presentation/widgets/emergency_stop_button.dart
index 4626ebee..c00ba6e9 100644
--- a/lib/features/remote_control/presentation/widgets/emergency_stop_button.dart
+++ b/lib/features/remote_control/presentation/widgets/emergency_stop_button.dart
@@ -17,7 +17,7 @@ class EmergencyStopButton extends StatefulWidget {
}
class _EmergencyStopButtonState extends State
- with TickerProviderStateMixin {
+ with SingleTickerProviderStateMixin {
late AnimationController _progressController;
bool _isPressing = false;
@@ -28,6 +28,13 @@ class _EmergencyStopButtonState extends State
vsync: this,
duration: const Duration(seconds: 3),
);
+
+ // 添加监听器,确保动画值变化时重建 UI
+ _progressController.addListener(() {
+ if (mounted) {
+ setState(() {});
+ }
+ });
}
@override
@@ -36,94 +43,118 @@ class _EmergencyStopButtonState extends State
super.dispose();
}
+ void _resetProgress() {
+ setState(() => _isPressing = false);
+ _progressController.stop();
+ _progressController.value = 0;
+ }
+
@override
Widget build(BuildContext context) {
- // 监听 Cubit 中的急停状态
final isEmergencyActive = context
.watch()
.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().updateEmergency(true);
- HapticFeedback.heavyImpact(); // 震动反馈
- }
- },
- // 2. 处理长按开始:仅在已急停时触发解除逻辑
- onLongPressStart: (_) {
- if (isEmergencyActive) {
- setState(() => _isPressing = true);
- _progressController.forward(from: 0).then((_) {
- if (_isPressing) {
- // 进度走完且仍在按压
- context.read().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().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().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,
+ ),
+ ),
+ ],
+ ),
),
),
],
diff --git a/lib/features/remote_control/presentation/widgets/left_joystick_area.dart b/lib/features/remote_control/presentation/widgets/left_joystick_area.dart
index 04653f8e..241f7e7d 100644
--- a/lib/features/remote_control/presentation/widgets/left_joystick_area.dart
+++ b/lib/features/remote_control/presentation/widgets/left_joystick_area.dart
@@ -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().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');
+ });
+ }
}
diff --git a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart
index 04fb2921..e111c221 100644
--- a/lib/features/remote_control/presentation/widgets/right_joystick_area.dart
+++ b/lib/features/remote_control/presentation/widgets/right_joystick_area.dart
@@ -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().updateOriginY(value.x);
context.read().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');
+ });
+ }
}
diff --git a/lib/features/remote_control/presentation/widgets/top_status_bar.dart b/lib/features/remote_control/presentation/widgets/top_status_bar.dart
index 3643e4df..9e786e95 100644
--- a/lib/features/remote_control/presentation/widgets/top_status_bar.dart
+++ b/lib/features/remote_control/presentation/widgets/top_status_bar.dart
@@ -17,6 +17,7 @@ class TopStatusBar extends StatelessWidget {
// 监听全局设备状态
final deviceState = context.watch().state;
final device = deviceState.selectedDevice;
+ var _remoteControlCubit = context.read();
ControlMode _parseControlMode(String modeString) {
switch (modeString.toUpperCase()) {
case 'BLE':
@@ -38,12 +39,12 @@ class TopStatusBar extends StatelessWidget {
final remoteState = context.watch().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; // 图标尺寸放大,避免过小拥挤
diff --git a/packages/cc_ui_kit/lib/src/cc_expand_slider.dart b/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
index 23a231a6..356e6278 100644
--- a/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
+++ b/packages/cc_ui_kit/lib/src/cc_expand_slider.dart
@@ -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 {
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 _checkVibrator() async {
+ _hasVibrator = await Vibration.hasVibrator();
+ setState(() {
+ _hasCheckedVibrator = true;
+ });
+ debugPrint('🔍 震动器检测:${_hasVibrator == true ? "有" : "无"}');
+ }
@override
void dispose() {
_tickTimer?.cancel();
+ _continuousVibrateTimer?.cancel();
super.dispose();
}
+ Future _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 {
if (!expanded) {
_alignmentValue = 0.0;
_tickTimer?.cancel();
+ _continuousVibrateTimer?.cancel();
+ _lastVibrationState = null;
}
});
if (expanded) _startTicking();
@@ -88,7 +157,13 @@ class _CCExpandSliderState extends State {
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 {
});
},
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 {
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 {
alignment: Alignment.center,
children: [
Positioned(
- bottom: -22, // 这里的 -22 是相对于“缩起时正方形”的底部
+ bottom: -22,
child: Text(
widget.label,
style: TextStyle(
@@ -220,4 +300,4 @@ class _CCExpandSliderState extends State {
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
);
}
-}
\ No newline at end of file
+}
diff --git a/pubspec.lock b/pubspec.lock
index f4999771..67f6c7d6 100644
--- a/pubspec.lock
+++ b/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:
diff --git a/pubspec.yaml b/pubspec.yaml
index 5428cdb3..a92c796d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -125,7 +125,7 @@ dependencies:
flutter_markdown: ^0.7.1
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
-
+ vibration: ^3.1.8
dev_dependencies:
flutter_test: