完成远程遥控页面的优化,包括 折叠功能栏 点击振动反馈 ,急停的读秒进度条 ,左右的振动反馈 ,锁屏的功能按钮的开发(禁用上下左右的滑轮)
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
<!-- 如果还需要扫描闪光灯控制,可选添加 -->
|
<!-- 如果还需要扫描闪光灯控制,可选添加 -->
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<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;
|
final String message;
|
||||||
Failure(this.message);
|
Failure(this.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NetworkFailure extends Failure {
|
||||||
|
NetworkFailure(super.message);
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,6 +223,9 @@ class NetMessageDispatcher {
|
|||||||
fullData[1] == 0xAA &&
|
fullData[1] == 0xAA &&
|
||||||
fullData[2] == 0x01) {
|
fullData[2] == 0x01) {
|
||||||
|
|
||||||
|
if(fullData[5] == 0x02){
|
||||||
|
debugPrint('下位机回复收到指令 - 完整数据包:${fullData.join(" ")}');
|
||||||
|
}
|
||||||
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
|
debugPrint('[Dispatcher] 下位机回复成功 - 完整数据包:${fullData.join(" ")}');
|
||||||
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
|
// 检查状态位 (索引 5 对应 payload 的第 2 个字节)
|
||||||
if (fullData.length > 5 && fullData[5] == 0x01) {
|
if (fullData.length > 5 && fullData[5] == 0x01) {
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ class PathPlanner {
|
|||||||
|
|
||||||
final routePlanSendEntity = RoutePlanSendEntity(
|
final routePlanSendEntity = RoutePlanSendEntity(
|
||||||
commandType: 0x01,
|
commandType: 0x01,
|
||||||
pointCounts: pointIndex, // 🔥 使用点编号,而不是固定的 1
|
pointCounts: 1, // 🔥 使用点编号,而不是固定的 1
|
||||||
targetLatitude: entity.latitude,
|
targetLatitude: entity.latitude,
|
||||||
targetLongitude: entity.longitude,
|
targetLongitude: entity.longitude,
|
||||||
speed: 1000,
|
speed: 1000,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
|||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||||
final NetMessageDispatcher dispatcher;
|
final NetMessageDispatcher dispatcher;
|
||||||
|
StreamSubscription? _deviceStatusSub;//🔥 新增:用于监听 DeviceStat
|
||||||
|
|
||||||
RemoteControlCubit(this._repository, this._requestControlPermissionUseCase, this.dispatcher)
|
RemoteControlCubit(this._repository, this._requestControlPermissionUseCase, this.dispatcher)
|
||||||
: super(
|
: 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 权限)
|
// 1. 初始化回包监听 (如 0x12 权限)
|
||||||
// void _initPacketListener() {
|
// void _initPacketListener() {
|
||||||
// _repository.responseStream.listen((packet) {
|
// _repository.responseStream.listen((packet) {
|
||||||
@@ -170,7 +224,9 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
|||||||
emit(state.copyWith(status: RemoteControlStatus.initial));
|
emit(state.copyWith(status: RemoteControlStatus.initial));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleLock() {}
|
void toggleLock() {
|
||||||
|
emit(state.copyWith(isLocked: !state.isLocked));
|
||||||
|
}
|
||||||
|
|
||||||
void togglePermissionDialog(bool show) {
|
void togglePermissionDialog(bool show) {
|
||||||
emit(state.copyWith(showPermissionRequestDialog: show));
|
emit(state.copyWith(showPermissionRequestDialog: show));
|
||||||
@@ -188,6 +244,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_timer?.cancel(); // 退出页面时务必销毁定时器
|
_timer?.cancel(); // 退出页面时务必销毁定时器
|
||||||
|
_deviceStatusSub?.cancel(); // 🔥 确保清理订阅
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +325,14 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
|||||||
debugPrint(' [点火指令] ${i}');
|
debugPrint(' [点火指令] ${i}');
|
||||||
updateFunction(ignition: 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 showLeftPip;
|
||||||
final bool showRightPip;
|
final bool showRightPip;
|
||||||
|
|
||||||
|
final bool topRightIsExpanded; // 顶部右侧按钮展开状态
|
||||||
|
final bool obstacleRecognitionFlag; // 障碍物识别标志位(这是UI显示的)
|
||||||
|
|
||||||
final String obstacleFlag; //障碍物标志位
|
final String obstacleFlag; //障碍物标志位
|
||||||
|
|
||||||
const RemoteControlState({
|
const RemoteControlState({
|
||||||
@@ -44,6 +47,8 @@ class RemoteControlState extends Equatable {
|
|||||||
this.showRightPip = true,
|
this.showRightPip = true,
|
||||||
this.obstacleFlag = '',
|
this.obstacleFlag = '',
|
||||||
required this.runningStatusModel,
|
required this.runningStatusModel,
|
||||||
|
this.topRightIsExpanded = false,
|
||||||
|
this.obstacleRecognitionFlag = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 方便 UI 更新部分属性
|
// 方便 UI 更新部分属性
|
||||||
@@ -64,6 +69,9 @@ class RemoteControlState extends Equatable {
|
|||||||
bool? showRightPip,
|
bool? showRightPip,
|
||||||
RunningStatusModel? runningStatusModel,
|
RunningStatusModel? runningStatusModel,
|
||||||
String? obstacleFlag,
|
String? obstacleFlag,
|
||||||
|
bool? topRightIsExpanded,
|
||||||
|
bool? obstacleRecognitionFlag,
|
||||||
|
|
||||||
}) {
|
}) {
|
||||||
return RemoteControlState(
|
return RemoteControlState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -83,6 +91,9 @@ class RemoteControlState extends Equatable {
|
|||||||
showRightPip: showRightPip ?? this.showRightPip,
|
showRightPip: showRightPip ?? this.showRightPip,
|
||||||
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
|
runningStatusModel: runningStatusModel ?? this.runningStatusModel,
|
||||||
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
obstacleFlag: obstacleFlag ?? this.obstacleFlag,
|
||||||
|
topRightIsExpanded: topRightIsExpanded ?? this.topRightIsExpanded,
|
||||||
|
obstacleRecognitionFlag: obstacleRecognitionFlag ?? this.obstacleRecognitionFlag,
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,5 +115,10 @@ class RemoteControlState extends Equatable {
|
|||||||
showRightPip,
|
showRightPip,
|
||||||
runningStatusModel,
|
runningStatusModel,
|
||||||
obstacleFlag,
|
obstacleFlag,
|
||||||
|
topRightIsExpanded,
|
||||||
|
obstacleRecognitionFlag,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@@ -25,6 +27,7 @@ class RemoteControlPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _RemoteControlPageState extends State<RemoteControlPage> {
|
class _RemoteControlPageState extends State<RemoteControlPage> {
|
||||||
String _videoStreamUrl = "";
|
String _videoStreamUrl = "";
|
||||||
|
StreamSubscription? _deviceStatusSubscription;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -33,8 +36,17 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
|
|||||||
// 2. 隐藏状态栏和虚拟按键
|
// 2. 隐藏状态栏和虚拟按键
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
|
|
||||||
|
// 🔥 关键:订阅 DeviceStatusBloc 的状态流
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
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() {
|
void dispose() {
|
||||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
context.read<RemoteControlCubit>().stopControlLoop();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../bloc/remote_control_cubit.dart';
|
import '../bloc/remote_control_cubit.dart';
|
||||||
@@ -121,6 +122,8 @@ class CenterControlArea extends StatelessWidget {
|
|||||||
default:
|
default:
|
||||||
debugPrint('⚠️ 未知的 label: $label');
|
debugPrint('⚠️ 未知的 label: $label');
|
||||||
}
|
}
|
||||||
|
// 🔥 在指令执行后触发震动反馈(增强手感)
|
||||||
|
HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔥 底盘的业务逻辑(左滑块)
|
// 🔥 底盘的业务逻辑(左滑块)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class EmergencyStopButton extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
||||||
with TickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late AnimationController _progressController;
|
late AnimationController _progressController;
|
||||||
bool _isPressing = false;
|
bool _isPressing = false;
|
||||||
|
|
||||||
@@ -28,6 +28,13 @@ class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
|||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 添加监听器,确保动画值变化时重建 UI
|
||||||
|
_progressController.addListener(() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -36,94 +43,118 @@ class _EmergencyStopButtonState extends State<EmergencyStopButton>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetProgress() {
|
||||||
|
setState(() => _isPressing = false);
|
||||||
|
_progressController.stop();
|
||||||
|
_progressController.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// 监听 Cubit 中的急停状态
|
|
||||||
final isEmergencyActive = context
|
final isEmergencyActive = context
|
||||||
.watch<RemoteControlCubit>()
|
.watch<RemoteControlCubit>()
|
||||||
.state
|
.state
|
||||||
.isEmergency;
|
.isEmergency;
|
||||||
|
|
||||||
final width = widget.width * 0.65;
|
final width = widget.width * 0.65;
|
||||||
final double indicatorSize = width + 10;
|
final double indicatorSize = width + 20;
|
||||||
|
|
||||||
return GestureDetector(
|
return SizedBox(
|
||||||
// 1. 处理点击:仅在未急停时触发
|
width: indicatorSize,
|
||||||
onTap: () {
|
height: indicatorSize,
|
||||||
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(() {});
|
|
||||||
},
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
// 对应 Compose 的 Canvas 绘制进度环
|
// 进度环 - 只在按压时显示
|
||||||
if (_isPressing)
|
AnimatedOpacity(
|
||||||
SizedBox(
|
opacity: _isPressing ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: SizedBox(
|
||||||
width: indicatorSize,
|
width: indicatorSize,
|
||||||
height: indicatorSize,
|
height: indicatorSize,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
value: _progressController.value,
|
value: _progressController.value,
|
||||||
strokeWidth: 10,
|
strokeWidth: 6,
|
||||||
color: Colors.red.withOpacity(0.8),
|
color: Colors.red.withOpacity(0.9),
|
||||||
backgroundColor: Colors.red.withValues(alpha: 0.2),
|
backgroundColor: Colors.red.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
Container(
|
// 按钮主体
|
||||||
width: width,
|
GestureDetector(
|
||||||
height: width,
|
onTap: () {
|
||||||
decoration: BoxDecoration(
|
debugPrint('🔴 [点击] 触发急停');
|
||||||
shape: BoxShape.circle,
|
if (!isEmergencyActive) {
|
||||||
// 根据状态切换颜色,对应 Color(0x80C53030)
|
context.read<RemoteControlCubit>().updateEmergency(true);
|
||||||
color: isEmergencyActive
|
HapticFeedback.heavyImpact();
|
||||||
? Colors.red.withOpacity(0.5)
|
}
|
||||||
: const Color(0x80C53030),
|
},
|
||||||
boxShadow: isEmergencyActive
|
onLongPressStart: (_) {
|
||||||
? [
|
debugPrint('🔴 [长按开始] isEmergency=$isEmergencyActive');
|
||||||
BoxShadow(
|
if (isEmergencyActive && !_isPressing) {
|
||||||
color: Colors.red.withValues(alpha: 0.5),
|
setState(() => _isPressing = true);
|
||||||
blurRadius: 20,
|
debugPrint('🔴 [开始动画] 从 0 开始');
|
||||||
),
|
_progressController.reset();
|
||||||
]
|
_progressController.forward().then((_) {
|
||||||
: [],
|
debugPrint('🔴 [动画完成] _isPressing=$_isPressing');
|
||||||
),
|
if (_isPressing && mounted) {
|
||||||
child: Column(
|
debugPrint('✅ [解除急停] 执行');
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
context.read<RemoteControlCubit>().updateEmergency(false);
|
||||||
children: [
|
HapticFeedback.vibrate();
|
||||||
SvgPicture.asset('assets/svgs/remote_alert.svg',width: width * 0.4,color: Colors.white.withValues(alpha: 0.5),),
|
setState(() => _isPressing = false);
|
||||||
const SizedBox(height: 6),
|
}
|
||||||
Text(
|
});
|
||||||
isEmergencyActive ? "急停中" : "急停",
|
}
|
||||||
style: TextStyle(
|
},
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
onLongPressEnd: (_) {
|
||||||
fontSize: 14,
|
debugPrint('🔴 [长按结束]');
|
||||||
fontWeight: FontWeight.w900,
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
import '../bloc/remote_control_cubit.dart';
|
import '../bloc/remote_control_cubit.dart';
|
||||||
|
|
||||||
@@ -32,10 +33,12 @@ class LeftJoystickArea extends StatelessWidget {
|
|||||||
onValueChanged: (value) {
|
onValueChanged: (value) {
|
||||||
// 对应 viewModel.updateOriginY(y)
|
// 对应 viewModel.updateOriginY(y)
|
||||||
context.read<RemoteControlCubit>().updateOriginY(value.y);
|
context.read<RemoteControlCubit>().updateOriginY(value.y);
|
||||||
|
_triggerVibration();
|
||||||
},
|
},
|
||||||
onPress: () {
|
onPress: () {
|
||||||
// 对应 VibrateOnce(current, 100)
|
// 对应 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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
import '../bloc/remote_control_cubit.dart';
|
import '../bloc/remote_control_cubit.dart';
|
||||||
|
|
||||||
@@ -33,11 +34,12 @@ class RightJoystickArea extends StatelessWidget {
|
|||||||
// 对应 viewModel.updateOriginX(x)
|
// 对应 viewModel.updateOriginX(x)
|
||||||
/// context.read<RemoteControlCubit>().updateOriginY(value.x);
|
/// context.read<RemoteControlCubit>().updateOriginY(value.x);
|
||||||
context.read<RemoteControlCubit>().updateOriginX(value.x);
|
context.read<RemoteControlCubit>().updateOriginX(value.x);
|
||||||
|
_triggerVibration();
|
||||||
},
|
},
|
||||||
onPress: () {
|
onPress: () {
|
||||||
// 对应 VibrateOnce(current, 100)
|
_triggerVibration();
|
||||||
HapticFeedback.mediumImpact();
|
}
|
||||||
},
|
,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -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 deviceState = context.watch<DevicesCubit>().state;
|
||||||
final device = deviceState.selectedDevice;
|
final device = deviceState.selectedDevice;
|
||||||
|
var _remoteControlCubit = context.read<RemoteControlCubit>();
|
||||||
ControlMode _parseControlMode(String modeString) {
|
ControlMode _parseControlMode(String modeString) {
|
||||||
switch (modeString.toUpperCase()) {
|
switch (modeString.toUpperCase()) {
|
||||||
case 'BLE':
|
case 'BLE':
|
||||||
@@ -38,12 +39,12 @@ class TopStatusBar extends StatelessWidget {
|
|||||||
final remoteState = context.watch<RemoteControlCubit>().state;
|
final remoteState = context.watch<RemoteControlCubit>().state;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.fromLTRB(1, 12, 1, 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
|
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
|
||||||
_buildIconButton("assets/svgs/remote_back.svg", () => context.pop()),
|
_buildIconButton("assets/svgs/remote_back.svg", () => context.pop()),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
// 2. 控制状态 (对应 StatusChipLeft)
|
// 2. 控制状态 (对应 StatusChipLeft)
|
||||||
StatusChip(
|
StatusChip(
|
||||||
@@ -74,28 +75,81 @@ class TopStatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
// 4. 刷新按钮
|
// 4. 刷新按钮
|
||||||
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
|
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
|
||||||
const SizedBox(width: 25),
|
// const SizedBox(width: 8),
|
||||||
// 5. 火技能按钮 (对应 SmallFunctionButton)
|
// 5. 火技能按钮 (对应 SmallFunctionButton)
|
||||||
//_buildIconButton("assets/svgs/fire.svg", () {}),
|
//_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
|
// 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)
|
// 5. 信号延迟 (对应 pingStatusChip)
|
||||||
_buildPingChip(remoteState.ping),
|
//_buildPingChip(remoteState.ping),
|
||||||
const SizedBox(width: 8),
|
// const SizedBox(width: 8),
|
||||||
|
|
||||||
// 6. 电量 (对应 StatusChipRight)
|
// 6. 电量 (对应 StatusChipRight)
|
||||||
_buildBatteryChip(remoteState.battery),
|
_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) {
|
Widget _buildPingChip(int ping) {
|
||||||
Color color = ping < 100
|
Color color = ping < 100
|
||||||
? Colors.green
|
? Colors.green
|
||||||
@@ -141,27 +242,32 @@ class TopStatusBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildControlModeChip(ControlMode mode) {
|
Widget _buildControlModeChip(String mode) {
|
||||||
String displayText;
|
String displayText;
|
||||||
switch (mode) {
|
if(mode == ''){
|
||||||
case ControlMode.BLE:
|
displayText = '无模式';
|
||||||
displayText = '蓝牙模式';
|
}else{
|
||||||
break;
|
displayText = mode;
|
||||||
case ControlMode.LOCAL:
|
|
||||||
displayText = '本地模式';
|
|
||||||
break;
|
|
||||||
case ControlMode.TCP:
|
|
||||||
displayText = 'TCP模式';
|
|
||||||
break;
|
|
||||||
case ControlMode.NONE:
|
|
||||||
displayText = '无模式';
|
|
||||||
break;
|
|
||||||
case ControlMode.OTHER:
|
|
||||||
displayText = '其他模式';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
displayText = '未知模式';
|
|
||||||
}
|
}
|
||||||
|
// switch (mode) {
|
||||||
|
// case "蓝牙模式":
|
||||||
|
// displayText = '蓝牙模式';
|
||||||
|
// break;
|
||||||
|
// case "本地模式":
|
||||||
|
// displayText = '本地模式';
|
||||||
|
// break;
|
||||||
|
// case "TCP模式":
|
||||||
|
// displayText = 'TCP模式';
|
||||||
|
// break;
|
||||||
|
// case "":
|
||||||
|
// displayText = '无模式';
|
||||||
|
// break;
|
||||||
|
// case "其他模式":
|
||||||
|
// displayText = '其他模式';
|
||||||
|
// break;
|
||||||
|
// default:
|
||||||
|
// displayText = '未知模式';
|
||||||
|
// }
|
||||||
|
|
||||||
return StatusChip(
|
return StatusChip(
|
||||||
text: displayText,
|
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(
|
return StatusChip(
|
||||||
text: "$voltage V",
|
text: "$voltage V",
|
||||||
color: Color(0xFFFACC15).withValues(alpha: 0.6),
|
color: Color(0xFFFACC15).withValues(alpha: 0.6),
|
||||||
@@ -205,7 +347,7 @@ class TopStatusBar extends StatelessWidget {
|
|||||||
// 2. 使用 StatusChip 包装,但 icon 传入 SvgPicture
|
// 2. 使用 StatusChip 包装,但 icon 传入 SvgPicture
|
||||||
return StatusChip(
|
return StatusChip(
|
||||||
text: "$level%",
|
text: "$level%",
|
||||||
color: color.withValues(alpha: 0.6),
|
color: color.withValues(alpha: 0.5),
|
||||||
svgColor: color,
|
svgColor: color,
|
||||||
// 如果你的 StatusChip 已经改为接受 Widget,则直接传 SvgPicture
|
// 如果你的 StatusChip 已经改为接受 Widget,则直接传 SvgPicture
|
||||||
// 如果 StatusChip 目前只接受 IconData,你需要按我之前的建议给它加个 leading 参数
|
// 如果 StatusChip 目前只接受 IconData,你需要按我之前的建议给它加个 leading 参数
|
||||||
@@ -215,7 +357,7 @@ class TopStatusBar extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildSliderBox(String label, bool isLeft, BuildContext context) {
|
Widget _buildSliderBox(String label, bool isLeft, BuildContext context) {
|
||||||
// 1. 核心尺寸调整:足够宽的容器解决拥挤,高度匹配状态栏
|
// 1. 核心尺寸调整:足够宽的容器解决拥挤,高度匹配状态栏
|
||||||
const double boxWidth = 96; // 水平宽度放大,容纳左右图标
|
const double boxWidth = 76; // 水平宽度放大,容纳左右图标
|
||||||
const double boxHeight = 32; // 高度和其他按钮保持一致
|
const double boxHeight = 32; // 高度和其他按钮保持一致
|
||||||
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
|
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:vibration/vibration.dart';
|
||||||
|
|
||||||
enum SliderAxis { horizontal, vertical }
|
enum SliderAxis { horizontal, vertical }
|
||||||
|
|
||||||
@@ -41,25 +42,91 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
double _alignmentValue = 0.0;
|
double _alignmentValue = 0.0;
|
||||||
bool _isExpanded = false;
|
bool _isExpanded = false;
|
||||||
Timer? _tickTimer;
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tickTimer?.cancel();
|
_tickTimer?.cancel();
|
||||||
|
_continuousVibrateTimer?.cancel();
|
||||||
super.dispose();
|
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() {
|
void _startTicking() {
|
||||||
_tickTimer?.cancel();
|
_tickTimer?.cancel();
|
||||||
_tickTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
_tickTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
if (!_isExpanded) return;
|
if (!_isExpanded) return;
|
||||||
|
|
||||||
|
int currentState;
|
||||||
if (_alignmentValue < -0.5) {
|
if (_alignmentValue < -0.5) {
|
||||||
|
currentState = -1;
|
||||||
widget.onStart?.call();
|
widget.onStart?.call();
|
||||||
} else if (_alignmentValue > 0.5) {
|
} else if (_alignmentValue > 0.5) {
|
||||||
|
currentState = 1;
|
||||||
widget.onEnd?.call();
|
widget.onEnd?.call();
|
||||||
} else {
|
} else {
|
||||||
|
currentState = 0;
|
||||||
widget.onCenter?.call();
|
widget.onCenter?.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_vibrate(currentState);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_startContinuousVibration();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _safeSetExpanded(bool expanded) {
|
void _safeSetExpanded(bool expanded) {
|
||||||
@@ -70,6 +137,8 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
_alignmentValue = 0.0;
|
_alignmentValue = 0.0;
|
||||||
_tickTimer?.cancel();
|
_tickTimer?.cancel();
|
||||||
|
_continuousVibrateTimer?.cancel();
|
||||||
|
_lastVibrationState = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (expanded) _startTicking();
|
if (expanded) _startTicking();
|
||||||
@@ -88,7 +157,13 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onPanDown: (_) => _safeSetExpanded(true),
|
onPanDown: (_) {
|
||||||
|
debugPrint('👆 按下');
|
||||||
|
if (_hasCheckedVibrator) {
|
||||||
|
_triggerVibration(40, '按下');
|
||||||
|
}
|
||||||
|
_safeSetExpanded(true);
|
||||||
|
},
|
||||||
onPanUpdate: (details) {
|
onPanUpdate: (details) {
|
||||||
if (!_isExpanded) return;
|
if (!_isExpanded) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -97,16 +172,23 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onPanEnd: (_) {
|
onPanEnd: (_) {
|
||||||
|
debugPrint('✋ 松开');
|
||||||
if (widget.isSpring) widget.onCenter?.call();
|
if (widget.isSpring) widget.onCenter?.call();
|
||||||
HapticFeedback.lightImpact();
|
if (_hasCheckedVibrator) {
|
||||||
|
_triggerVibration(40, '松开');
|
||||||
|
}
|
||||||
|
_lastVibrationState = null;
|
||||||
|
_stopContinuousVibration();
|
||||||
|
_safeSetExpanded(false);
|
||||||
|
},
|
||||||
|
onPanCancel: () {
|
||||||
|
debugPrint('❌ 取消');
|
||||||
_safeSetExpanded(false);
|
_safeSetExpanded(false);
|
||||||
},
|
},
|
||||||
onPanCancel: () => _safeSetExpanded(false),
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
// 1. 轨道背景:从中向外展开
|
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
@@ -120,10 +202,8 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
child: _buildSegmentedContent(isVertical, maxLength, minLength),
|
child: _buildSegmentedContent(isVertical, maxLength, minLength),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 2. 核心修改:文字位置固定逻辑
|
|
||||||
// 我们用一个和“缩起状态”一样大的 SizedBox 作为参考
|
|
||||||
if (!_isExpanded)
|
if (!_isExpanded)
|
||||||
IgnorePointer( // 防止文字遮挡点击
|
IgnorePointer(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: minLength,
|
width: minLength,
|
||||||
height: minLength,
|
height: minLength,
|
||||||
@@ -132,7 +212,7 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: -22, // 这里的 -22 是相对于“缩起时正方形”的底部
|
bottom: -22,
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.label,
|
widget.label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -220,4 +300,4 @@ class _CCExpandSliderState extends State<CCExpandSlider> {
|
|||||||
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -1420,6 +1420,22 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
visibility_detector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ dependencies:
|
|||||||
flutter_markdown: ^0.7.1
|
flutter_markdown: ^0.7.1
|
||||||
|
|
||||||
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
|
#qr_code_scanner: ^1.0.1 # 用于扫描二维码
|
||||||
|
vibration: ^3.1.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user