完成远程遥控页面的优化,包括 折叠功能栏 点击振动反馈 ,急停的读秒进度条 ,左右的振动反馈 ,锁屏的功能按钮的开发(禁用上下左右的滑轮)

This commit is contained in:
2026-03-24 17:09:12 +08:00
parent f7523a943d
commit 8772b1cb43
17 changed files with 525 additions and 119 deletions

View File

@@ -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" />

View 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

View 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

View File

@@ -2,3 +2,7 @@ class Failure {
final String message;
Failure(this.message);
}
class NetworkFailure extends Failure {
NetworkFailure(super.message);
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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,
];
}

View File

@@ -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();
}

View File

@@ -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();
}
// 🔥 底盘的业务逻辑(左滑块)

View File

@@ -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,
),
),
],
),
),
),
],

View File

@@ -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');
});
}
}

View File

@@ -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');
});
}
}

View File

@@ -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; // 图标尺寸放大,避免过小拥挤

View File

@@ -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),
);
}
}
}

View File

@@ -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:

View File

@@ -125,7 +125,7 @@ dependencies:
flutter_markdown: ^0.7.1
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
vibration: ^3.1.8
dev_dependencies:
flutter_test: