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: