第二次提交

This commit is contained in:
cc
2026-01-18 20:21:14 +08:00
parent 4346601481
commit 289e706337
129 changed files with 4615 additions and 135 deletions

View File

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

View File

@@ -0,0 +1,213 @@
import 'dart:math';
import 'package:flutter/material.dart';
enum AxisHint { forwardBackward, leftRight }
class JoystickValue {
final int x; // -100 ~ 100
final int y; // -100 ~ 100
JoystickValue(this.x, this.y);
}
class CCJoystick extends StatefulWidget {
final AxisHint axisHint;
final Function(JoystickValue) onValueChanged;
final VoidCallback? onPress;
final double radius;
const CCJoystick({
super.key,
required this.axisHint,
required this.onValueChanged,
this.onPress,
this.radius = 80,
});
@override
State<CCJoystick> createState() => _CustomJoystickState();
}
class _CustomJoystickState extends State<CCJoystick>
with SingleTickerProviderStateMixin {
Offset _offset = Offset.zero;
late AnimationController _floatController;
@override
void initState() {
super.initState();
// 漂浮动画:对应 Compose 的 floatAnim
_floatController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
)..repeat(reverse: true);
}
@override
void dispose() {
_floatController.dispose();
super.dispose();
}
// 计算输出值并应用死区
void _updateValue() {
int x = 0;
int y = 0;
if (widget.axisHint == AxisHint.leftRight) {
x = ((_offset.dx / widget.radius) * 100).clamp(-100, 100).toInt();
} else {
y = ((-_offset.dy / widget.radius) * 100).clamp(-100, 100).toInt();
}
widget.onValueChanged(JoystickValue(_applyDeadZone(x), _applyDeadZone(y)));
}
int _applyDeadZone(int value, {int deadZone = 5}) {
return value.abs() < deadZone ? 0 : value;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: (_) => widget.onPress?.call(),
onPanUpdate: (details) {
setState(() {
Offset newOffset = _offset + details.delta;
double distance = newOffset.distance;
// 限制在圆圈内
if (distance <= widget.radius) {
_offset = newOffset;
} else {
double angle = atan2(newOffset.dy, newOffset.dx);
_offset = Offset(
cos(angle) * widget.radius,
sin(angle) * widget.radius,
);
}
});
_updateValue();
},
onPanEnd: (_) {
setState(() => _offset = Offset.zero); // 归位
_updateValue();
},
child: AnimatedBuilder(
animation: _floatController,
builder: (context, child) {
return CustomPaint(
size: Size(widget.radius * 2, widget.radius * 2),
painter: JoystickPainter(
offset: _offset,
radius: widget.radius,
axisHint: widget.axisHint,
floatValue: _floatController.value * 10 - 5, // -5f 到 5f
),
);
},
),
);
}
}
class JoystickPainter extends CustomPainter {
final Offset offset;
final double radius;
final AxisHint axisHint;
final double floatValue;
JoystickPainter({
required this.offset,
required this.radius,
required this.axisHint,
required this.floatValue,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final paint = Paint();
// 1. 绘制底盘 (对应 Color(0x6600BFFF))
paint
..color = const Color(0x6600BFFF)
..style = PaintingStyle.stroke
..strokeWidth = 2;
canvas.drawCircle(center, radius + 8, paint);
// 2. 绘制箭头 (对应 drawArrow 逻辑)
paint
..color = Colors.cyan.withOpacity(0.5)
..strokeCap = StrokeCap.round
..strokeWidth = 4;
const arrowSize = 20.0;
if (axisHint == AxisHint.forwardBackward) {
_drawArrow(
canvas,
center + Offset(0, -radius - arrowSize + floatValue),
"UP",
arrowSize,
paint,
);
_drawArrow(
canvas,
center + Offset(0, radius + arrowSize - floatValue),
"DOWN",
arrowSize,
paint,
);
} else {
_drawArrow(
canvas,
center + Offset(-radius - arrowSize + floatValue, 0),
"LEFT",
arrowSize,
paint,
);
_drawArrow(
canvas,
center + Offset(radius + arrowSize - floatValue, 0),
"RIGHT",
arrowSize,
paint,
);
}
// 3. 绘制旋钮 (对应 Color(0x8000D4FF))
paint
..style = PaintingStyle.fill
..color = const Color(0x8000D4FF);
canvas.drawCircle(center + offset, 35, paint);
}
void _drawArrow(
Canvas canvas,
Offset pos,
String direction,
double size,
Paint paint,
) {
switch (direction) {
case "UP":
canvas.drawLine(pos + Offset(-size / 2, size / 2), pos, paint);
canvas.drawLine(pos + Offset(size / 2, size / 2), pos, paint);
break;
case "DOWN":
canvas.drawLine(pos + Offset(-size / 2, -size / 2), pos, paint);
canvas.drawLine(pos + Offset(size / 2, -size / 2), pos, paint);
break;
case "LEFT":
canvas.drawLine(pos + Offset(size / 2, -size / 2), pos, paint);
canvas.drawLine(pos + Offset(size / 2, size / 2), pos, paint);
break;
case "RIGHT":
canvas.drawLine(pos + Offset(-size / 2, -size / 2), pos, paint);
canvas.drawLine(pos + Offset(-size / 2, size / 2), pos, paint); // 修正坐标
break;
}
}
@override
bool shouldRepaint(covariant JoystickPainter oldDelegate) => true;
}

View File

@@ -2,16 +2,52 @@ import 'package:flutter/material.dart';
class CCPrimaryButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final VoidCallback? onPressed;
final double? width;
final double height;
final double fontSize;
final Color backgroundColor; // 新增:背景颜色
final Color textColor; // 新增:字体颜色
const CCPrimaryButton({
super.key,
required this.text,
required this.onPressed,
this.onPressed,
this.width = double.infinity,
this.height = 54.0,
this.fontSize = 16.0,
this.backgroundColor = Colors.black, // 默认黑色背景
this.textColor = Colors.white, // 默认白色文字
});
@override
Widget build(BuildContext context) {
return ElevatedButton(onPressed: onPressed, child: Text(text));
// 💡 核心改动:如果父组件是 Row 且没有指定 width,
// 直接用 double.infinity 会崩溃。
// 使用这种方式可以更加安全。
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor, // 使用参数
foregroundColor: textColor, // 使用参数
disabledBackgroundColor: Colors.grey.shade300,
disabledForegroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
child: Text(text),
),
);
}
}

View File

@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
class CCPrimaryImageButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final double? width;
final double height;
final double fontSize;
final Color backgroundColor;
final Color textColor;
final String? imagePath; // 新增:图片路径 (例如 'assets/images/car.png')
final double imageSize; // 新增:控制图片大小
const CCPrimaryImageButton({
super.key,
required this.text,
this.onPressed,
this.width = double.infinity,
this.height = 54.0,
this.fontSize = 16.0,
this.backgroundColor = Colors.black,
this.textColor = Colors.white,
this.imagePath, // 可以为空,为空时不显示图片
this.imageSize = 24.0, // 默认图片尺寸
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor,
foregroundColor: textColor,
disabledBackgroundColor: Colors.grey.shade300,
disabledForegroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
textStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
// 使用 Row 来组合图片和文字
child: Row(
mainAxisSize: MainAxisSize.min, // 确保内容居中紧凑
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (imagePath != null) ...[
Image.asset(
imagePath!,
width: imageSize,
height: imageSize,
fit: BoxFit.contain,
),
const SizedBox(width: 8), // 图片和文字之间的间距
],
Text(text),
],
),
),
);
}
}