远程遥控界面编写
This commit is contained in:
@@ -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';
|
||||
|
||||
223
packages/cc_ui_kit/lib/src/cc_expand_slider.dart
Normal file
223
packages/cc_ui_kit/lib/src/cc_expand_slider.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,8 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_svg: ^2.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user