远程遥控界面编写

This commit is contained in:
CC
2026-01-19 18:24:24 +08:00
parent 289e706337
commit 32b702fb6d
48 changed files with 1118 additions and 303 deletions

View File

@@ -3,3 +3,4 @@ library cc_ui_kit;
export 'src/cc_joystick.dart';
export 'src/cc_primary_button.dart';
export 'src/cc_primary_image_button.dart';
export 'src/cc_expand_slider.dart';

View File

@@ -0,0 +1,223 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
enum SliderAxis { horizontal, vertical }
class CCExpandSlider extends StatefulWidget {
final SliderAxis axis;
final bool isSpring;
final String label;
final String svgStart;
final String svgCenter;
final String svgEnd;
final double iconSize;
final double borderRadius;
final VoidCallback? onStart;
final VoidCallback? onCenter;
final VoidCallback? onEnd;
const CCExpandSlider({
super.key,
this.axis = SliderAxis.vertical,
this.isSpring = true,
required this.svgStart,
required this.svgCenter,
required this.svgEnd,
required this.label,
this.iconSize = 22.0,
this.borderRadius = 12.0,
this.onStart,
this.onCenter,
this.onEnd,
});
@override
State<CCExpandSlider> createState() => _CCExpandSliderState();
}
class _CCExpandSliderState extends State<CCExpandSlider> {
double _alignmentValue = 0.0;
bool _isExpanded = false;
Timer? _tickTimer;
@override
void dispose() {
_tickTimer?.cancel();
super.dispose();
}
void _startTicking() {
_tickTimer?.cancel();
_tickTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
if (!_isExpanded) return;
if (_alignmentValue < -0.5) {
widget.onStart?.call();
} else if (_alignmentValue > 0.5) {
widget.onEnd?.call();
} else {
widget.onCenter?.call();
}
});
}
void _safeSetExpanded(bool expanded) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isExpanded = expanded;
if (!expanded) {
_alignmentValue = 0.0;
_tickTimer?.cancel();
}
});
if (expanded) _startTicking();
}
});
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final isVertical = widget.axis == SliderAxis.vertical;
final double maxLength = isVertical ? constraints.maxHeight : constraints.maxWidth;
final double minLength = isVertical ? constraints.maxWidth : constraints.maxHeight;
final double currentLength = _isExpanded ? maxLength : minLength;
final double dragArea = (maxLength - minLength) / 2;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => _safeSetExpanded(true),
onPanUpdate: (details) {
if (!_isExpanded) return;
setState(() {
final delta = isVertical ? details.delta.dy : details.delta.dx;
_alignmentValue = (_alignmentValue + delta / (dragArea > 0 ? dragArea : 1)).clamp(-1.0, 1.0);
});
},
onPanEnd: (_) {
if (widget.isSpring) widget.onCenter?.call();
HapticFeedback.lightImpact();
_safeSetExpanded(false);
},
onPanCancel: () => _safeSetExpanded(false),
child: Stack(
alignment: Alignment.center,
clipBehavior: Clip.none,
children: [
// 1. 轨道背景:从中向外展开
AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
width: isVertical ? minLength : currentLength,
height: isVertical ? currentLength : minLength,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0),
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(color: Colors.white.withOpacity(0.3)),
),
child: _buildSegmentedContent(isVertical, maxLength, minLength),
),
// 2. 核心修改:文字位置固定逻辑
// 我们用一个和“缩起状态”一样大的 SizedBox 作为参考
if (!_isExpanded)
IgnorePointer( // 防止文字遮挡点击
child: SizedBox(
width: minLength,
height: minLength,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
Positioned(
bottom: -22, // 这里的 -22 是相对于“缩起时正方形”的底部
child: Text(
widget.label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 10,
fontWeight: FontWeight.w500
),
),
),
],
),
),
),
],
),
);
});
}
Widget _buildSegmentedContent(bool isVertical, double maxLength, double minLength) {
final double segmentHeight = maxLength / 3;
return Stack(
alignment: Alignment.center,
children: [
if (_isExpanded) ...[
Positioned(
top: 0,
left: 0,
right: 0,
height: segmentHeight,
child: Center(child: _svgItem(widget.svgStart, -1)),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
height: segmentHeight,
child: Center(child: _svgItem(widget.svgEnd, 1)),
),
],
Center(
child: SizedBox(
width: minLength,
height: minLength,
child: Center(child: _svgItem(widget.svgCenter, 0)),
),
),
if (_isExpanded)
Padding(
padding: const EdgeInsets.all(4.0),
child: Align(
alignment: isVertical ? Alignment(0, _alignmentValue) : Alignment(_alignmentValue, 0),
child: Container(
width: minLength * 0.82,
height: minLength * 0.82,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.28),
borderRadius: BorderRadius.circular(widget.borderRadius * 0.7),
),
),
),
),
],
);
}
Widget _svgItem(String assetPath, int type) {
Color color = Colors.white30;
if (_isExpanded) {
if (type == -1 && _alignmentValue < -0.5) color = const Color(0xFF0076F9);
else if (type == 1 && _alignmentValue > 0.5) color = const Color(0xFF0076F9);
else if (type == 0 && _alignmentValue.abs() <= 0.5) color = const Color(0xFF0076F9);
else color = Colors.white;
} else {
color = Colors.white;
}
return SvgPicture.asset(
assetPath,
width: widget.iconSize,
height: widget.iconSize,
fit: BoxFit.contain,
colorFilter: ColorFilter.mode(color, BlendMode.srcIn),
);
}
}

View File

@@ -132,16 +132,16 @@ class JoystickPainter extends CustomPainter {
paint
..color = const Color(0x6600BFFF)
..style = PaintingStyle.stroke
..strokeWidth = 2;
..strokeWidth = 1;
canvas.drawCircle(center, radius + 8, paint);
// 2. 绘制箭头 (对应 drawArrow 逻辑)
paint
..color = Colors.cyan.withOpacity(0.5)
..strokeCap = StrokeCap.round
..strokeWidth = 4;
..strokeWidth = 2;
const arrowSize = 20.0;
const arrowSize = 10.0;
if (axisHint == AxisHint.forwardBackward) {
_drawArrow(
canvas,

View File

@@ -11,6 +11,8 @@ dependencies:
flutter:
sdk: flutter
flutter_svg: ^2.2.3
dev_dependencies:
flutter_test:
sdk: flutter