Dart
import 'package:flutter/material.dart';
import 'dart:math';
const double spacingAngle = 45.0; // 每两个文字之间的角度
// 自定义绘制器,ArcTextPainter 用于在圆弧上绘制文字
class ArcTextPainter extends CustomPainter {
final double rotationAngle; // 动画旋转角度
final double strokeWidth; // 圆环的宽度
final List<String> text; // 文字列表
final double curIndex; // 当前旋转进度
ArcTextPainter({
required this.rotationAngle,
required this.strokeWidth,
required this.text,
required this.curIndex,
});
@override
void paint(Canvas canvas, Size size) {
final radius = size.width / 2; // 圆的半径
final center = Offset(size.width / 2, size.height / 2); // 圆心的坐标
// 创建用于绘制圆弧的画笔
final paint = Paint()
..color = Colors.grey.shade300 // 圆弧的颜色
..strokeWidth = strokeWidth // 圆弧的宽度
..style = PaintingStyle.stroke; // 画笔样式为描边
// 计算圆弧的矩形区域
final arcRect = Rect.fromCircle(center: center, radius: radius - strokeWidth / 2);
canvas.drawArc(arcRect, pi, pi, false, paint); // 绘制圆弧
// 创建用于绘制箭头的画笔
final arrowPaint = Paint()
..color = Colors.purple // 箭头的颜色
..style = PaintingStyle.fill; // 画笔样式为填充
// 定义箭头的路径
final arrowPath = Path();
arrowPath.moveTo(center.dx, center.dy - radius + strokeWidth / 2); // 箭头起点
arrowPath.lineTo(center.dx - 10, center.dy - radius + strokeWidth / 2 + 20); // 箭头的左边
arrowPath.lineTo(center.dx + 10, center.dy - radius + strokeWidth / 2 + 20); // 箭头的右边
arrowPath.close(); // 结束路径
canvas.drawPath(arrowPath, arrowPaint); // 绘制箭头
// 绘制圆弧上的文字
_drawTextAlongArc(canvas, center, radius - strokeWidth / 2);
}
// 在圆弧上绘制文字
void _drawTextAlongArc(Canvas canvas, Offset center, double radius) {
final textPainter = TextPainter(
textAlign: TextAlign.center, // 文字对齐方式为居中
textDirection: TextDirection.ltr, // 文字方向为从左到右
);
// 遍历所有文字并绘制
for (int i = 0; i < text.length; i++) {
// 计算当前文字的角度
double angle = (i - curIndex) * spacingAngle * (pi / 180) - pi/2;
// print("angle:${i} ${angle*180/pi}");
// 检查文字是否在可视范围内
if (angle >= -pi && angle <= 0) {
// 计算文字的位置
final x = center.dx + radius * cos(angle); // x 坐标
final y = center.dy + radius * sin(angle); // y 坐标
canvas.save(); // 保存当前画布状态
canvas.translate(x, y); // 移动画布到文字的位置
// 设置文字的样式和内容
textPainter.text = TextSpan(
text: text[i],
style: TextStyle(fontSize: 14, color: Colors.black), // 文字的样式
);
textPainter.layout(); // 计算文字的大小
// 计算文字的实际可见区域
double visibleFraction = _calculateVisibleFraction(angle);
if (visibleFraction < 1.0) {
// 如果文字不完全可见,则应用裁剪遮罩
canvas.clipRect(Rect.fromLTWH(
-textPainter.width / 2, // 左上角 x 坐标
-textPainter.height / 2, // 左上角 y 坐标
textPainter.width, // 文字的宽度
textPainter.height, // 文字的高度
));
}
textPainter.paint(canvas, Offset(-textPainter.width / 2, -textPainter.height / 2)); // 绘制文字
canvas.restore(); // 恢复画布状态
}
}
}
// 计算文字的可见比例
double _calculateVisibleFraction(double angle) {
// 文字显示的比例,确保在 [-pi, 0] 范围内显示完全
if (angle < -pi / 2) {
return max(0, (angle + pi) / (pi / 2)); // 文字被遮挡的部分
} else if (angle > 0) {
return max(0, (-angle) / (pi / 2)); // 文字被遮挡的部分
}
return 1.0; // 文字完全可见
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true; // 是否需要重新绘制
}
// ArcTextExample 是一个示例 widget,用于展示自定义绘制的效果
class ArcTextExample extends StatefulWidget {
final double strokeWidth; // 圆环的宽度
final List<String> text; // 文字列表
final int initialIndex; // 初始索引
final double animationDuration; // 动画持续时间
const ArcTextExample({
Key? key,
required this.strokeWidth,
required this.text,
required this.initialIndex,
required this.animationDuration,
}) : super(key: key);
@override
_ArcTextExampleState createState() => _ArcTextExampleState();
}
class _ArcTextExampleState extends State<ArcTextExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller; // 动画控制器
late Animation<double> _animation; // 动画
double curIndex = 0.0; // 保存当前旋转的进度
bool isAnimating = false; // 标记动画是否正在进行
final TextEditingController indexController = TextEditingController(); // 目标索引的文本控制器
final TextEditingController durationController = TextEditingController(); // 动画持续时间的文本控制器
@override
void initState() {
super.initState();
// 初始化文本控制器的值
indexController.text = widget.initialIndex.toString();
durationController.text = widget.animationDuration.toString();
// 计算初始旋转角度
double initialAngle = ( - widget.initialIndex ) * spacingAngle * (pi / 180) - pi / 2;
curIndex = widget.initialIndex.toDouble(); // 初始化时 curIndex 是初始索引
// 创建动画控制器
_controller = AnimationController(
duration: Duration(seconds: widget.animationDuration.toInt()), // 设置动画的持续时间
vsync: this, // 与当前的 TickerProvider 绑定
);
// print("initialAngle: ${initialAngle*180/pi}");
// 创建动画
_animation = Tween<double>(
begin: initialAngle, // 动画开始的角度
end: initialAngle + 2 * pi, // 动画结束的角度
).animate(_controller)
..addListener(() {
setState(() {
print("_animation.value: ${_animation.value * 180 / pi}");
// 更新当前角度对应的索引范
curIndex = (-(_animation.value + pi / 2) * (180 / pi)) / spacingAngle;
print("Current Index: ${curIndex.toStringAsFixed(2)}"); // 打印当前索引
});
});
}
@override
void dispose() {
_controller.dispose(); // 释放动画控制器资源
indexController.dispose(); // 释放目标索引的文本控制器资源
durationController.dispose(); // 释放动画持续时间的文本控制器资源
super.dispose();
}
// 旋转到目标索引
void rotateToIndex(int targetIndex, double duration) {
if(targetIndex != curIndex){
setState(() {
if (isAnimating) {
// 如果正在进行动画,则停止并重置
_controller.stop();
isAnimating = false;
}
_controller.duration = Duration(seconds: duration.toInt()); // 设置动画的持续时间
double startAngle = (-curIndex) * spacingAngle * (pi / 180) - pi / 2; // 使用当前索引角度作为起始角度
double targetAngle = (-targetIndex) * spacingAngle * (pi / 180) - pi / 2; // 计算目标角度
print("开始度数: ${startAngle * 180/pi} 结束度数:${targetAngle * 180/pi}");
double endAngle;
// 确定旋转方1
if (targetAngle < 0) {
// 顺时针旋转
endAngle = startAngle + targetAngle;
} else {
// 逆时针旋转
endAngle = startAngle - targetAngle;
}
_animation = Tween<double>(
begin: startAngle, // 动画开始的角度
end: targetAngle, // 动画结束的角度
).animate(_controller);
isAnimating = true; // 标记动画为进行中
_controller.reset(); // 重置动画控制
_controller.forward(); // 开始动画
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SizedBox(height: 140), // 上部间距
Center(
child: CustomPaint(
size: Size(300, 200), // 设置圆弧的大小
painter: ArcTextPainter(
rotationAngle: _animation.value, // 当前旋转角度
strokeWidth: widget.strokeWidth, // 圆环的宽度
text: widget.text, // 文字列表
curIndex: curIndex, // 当前旋转进度
),
),
),
SizedBox(height: 20), // 下部间距
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0), // 水平内边距
child: Column(
children: [
// 目标索引的输入框
TextField(
controller: indexController,
decoration: InputDecoration(
labelText: 'Target Index', // 输入框标签
),
keyboardType: TextInputType.number, // 键盘类型为数字
),
// 动画持续时间的输入框
TextField(
controller: durationController,
decoration: InputDecoration(
labelText: 'Animation Duration (seconds)', // 输入框标签
),
keyboardType: TextInputType.number, // 键盘类型为数字
),
SizedBox(height: 20), // 输入框和按钮之间的间距
// 旋转按钮
ElevatedButton(
onPressed: () {
// 获取目标索引和动画持续时间
int targetIndex = int.tryParse(indexController.text) ?? 0;
double duration = double.tryParse(durationController.text) ?? 10.0;
// if (isAnimating) {
// // 如果动画正在进行,停止并保存当前进度
// _controller.stop();
// curIndex = (-(_animation.value + pi / 2) * (180 / pi)) / spacingAngle; // 保存当前进度为 curIndex
// _controller.reset(); // 重置动画控制器
// }
// 旋转到目标索引
rotateToIndex(targetIndex, duration);
},
child: Text('Rotate to Index'), // 按钮文本
),
],
),
),
],
),
);
}
}
void main() {
runApp(MaterialApp(
home: ArcTextExample(
strokeWidth: 100.0, // 圆环的宽度
text: List.generate(11, (i) => '第$i层'), // 文字列表
initialIndex: 3, // 初
animationDuration: 10.0, // 默认动画时间为10秒
),
));
}