第二次提交
This commit is contained in:
@@ -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';
|
||||
|
||||
213
packages/cc_ui_kit/lib/src/cc_joystick.dart
Normal file
213
packages/cc_ui_kit/lib/src/cc_joystick.dart
Normal 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;
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
70
packages/cc_ui_kit/lib/src/cc_primary_image_button.dart
Normal file
70
packages/cc_ui_kit/lib/src/cc_primary_image_button.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user