基本思路
1、创建圆角矩形,高光模型(小球),使用Stack来定义高光与矩形的相对位置关系;
2、利用AnimatedBuilder创建动画,通过_controller.addListener来更新高光位置坐标,从而实现动画效果。
3、拆分运动行程,四条直线和四个弧度,dart中坐标系原点为左上角(0,0),顺时针方向从除圆角外直线向右开始行程。在案例中,行程分别对应line1、arc1(右上角)P、line2、arc2(右下角)、line3、arc3(左下角)、line4、arc4(左上角)。
实现方法
1、定义变量:
late AnimationController _controller;
late Animation<double> _animation;
double w = 400; #矩形宽
double h = 300; #矩形高
double r = 30; #圆角半径
late double totalLength; #圆角矩形周长
double ballR = 2; #高光小球半径
double x = 0; #高光小球x坐标
double y = 0; #高光小球y坐标
2、初始化:
@override
void initState() {
super.initState();
totalLength = (w + h) * 2 - 8 * r + 4 * r * pi / 2;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000),
)..repeat();
_animation = Tween<double>(begin: 0, end: totalLength).animate(_controller);
// 初始化 x 和 y 的值 运动起点(r,0)
x = r;
y = 0;
_controller.addListener(() {
// 更新 x 和 y 的值
updatePosition();
});
}
3、路径坐标算法:(圆角弧度的theta角计算:使用运动距离的增量除以半径r):
delta _animation = _animation.value - 已走过的行程;
theta t = delta _animation / r;
x = r * cos(t);
y = r * sin(t);
void updatePosition() {
// 定义行程
double line1 = w - 2 * r;
double line2 = h - 2 * r;
double line3 = w - 2 * r;
double line4 = h - 2 * r;
double arc1 = r * pi / 2;
double arc2 = r * pi / 2;
double arc3 = r * pi / 2;
double arc4 = r * pi / 2;
setState(() {
// 在这里更新 x 和 y 的值
if (_animation.value <= line1) {
// 第1段,向右直线 line1 从坐标(r,0)开始,向右走直线,距离为 w-2*r
x = r + _animation.value;
y = 0;
} else if (_animation.value > line1 && _animation.value <= (line1 + arc1)) {
// 第2段,右上圆角 arc1
double t = (_animation.value - line1) / r - pi / 2;
x = (w - r) + r * cos(t);
y = r + r * sin(t);
} else if (_animation.value > (line1 + arc1) && (_animation.value <= (line1 + arc1 + line2))) {
// 第3段,下行直线 line2
x = w;
y = _animation.value - (line1 + arc1) + r;
} else if (_animation.value > (line1 + arc1 + line2) && _animation.value <= (line1 + arc1 + line2 + arc2)) {
// 第4段,右下圆角 arc2
double t = (_animation.value - (line1 + arc1 + line2)) / r;
x = (w - r) + r * cos(t);
y = (h - r) + r * sin(t);
} else if (_animation.value > (line1 + arc1 + line2 + arc2) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3)) {
// 第5段,向左直线 line3
y = h;
x = (w - r) - (_animation.value - (line1 + arc1 + line2 + arc2));
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3)) {
// 第6段,左下圆角 arc3
double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3)) / r - 3 * pi / 2;
x = r + r * cos(t);
y = (h - r) + r * sin(t);
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) {
// 第7段,上行直线 line4
x = 0;
y = (h - r) - (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3));
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4) && _animation.value <= totalLength) {
// 第8段,左上圆角 arc4
double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) / r - pi;
x = r + r * cos(t);
y = r + r * sin(t);
}
});
}
4、相关数学公式:
# 在圆的极坐标系中,弧度R、半径 radius 和角度 theta 之间的关系可以通过以下公式表示:
R = theta × radius
# 这个公式表明,在圆上,给定半径和角度,弧度可以通过将角度除以半径来计算。
# 从弧度转换为角度,可以使用以下公式:
theta = R / radius
# 使用 theta 计算 x 和 y 坐标:
x = r * cos(theta);
y = r * sin(theta);
# 最后注意要相对dart直角坐标原点做坐标平移转换:
x = (平移量)+ r * cos(theta);
y = (平移量)+ r * sin(theta);
完整代码
import 'dart:math';
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
const TestPage({Key? key}) : super(key: key);
@override
_TestPageState createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Animation Example',
style: TextStyle(fontSize: 16, color: Colors.red),
),
),
body: const Center(
child: MyCustomRectangle(),
),
);
}
}
class MyCustomRectangle extends StatefulWidget {
const MyCustomRectangle({super.key});
@override
_MyCustomRectangleState createState() => _MyCustomRectangleState();
}
class _MyCustomRectangleState extends State<MyCustomRectangle> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double w = 400;
double h = 300;
double r = 30;
late double totalLength;
double ballR = 2;
double x = 0;
double y = 0;
@override
void initState() {
super.initState();
totalLength = (w + h) * 2 - 8 * r + 4 * r * pi / 2;
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 10000),
)..repeat();
_animation = Tween<double>(begin: 0, end: totalLength).animate(_controller);
// 初始化 x 和 y 的值 运动起点(r,0)
x = r;
y = 0;
_controller.addListener(() {
// 更新 x 和 y 的值
updatePosition();
});
}
void updatePosition() {
setState(() {
// 在这里更新 x 和 y 的值
// 定义行程
double line1 = w - 2 * r;
double line2 = h - 2 * r;
double line3 = w - 2 * r;
double line4 = h - 2 * r;
double arc1 = r * pi / 2;
double arc2 = r * pi / 2;
double arc3 = r * pi / 2;
double arc4 = r * pi / 2;
if (_animation.value <= line1) {
// 第1段,向右直线 line1 从坐标(r,0)开始,向右走直线,距离为 w-2*r
x = r + _animation.value % line1;
y = 0;
} else if (_animation.value > line1 && _animation.value <= (line1 + arc1)) {
// 第2段,右上圆角 arc1
double t = (_animation.value - line1) / r - pi / 2;
x = (w - r) + r * cos(t);
y = r + r * sin(t);
} else if (_animation.value > (line1 + arc1) && (_animation.value <= (line1 + arc1 + line2))) {
// 第3段,下行直线 line2
x = w;
y = _animation.value - (line1 + arc1) + r;
} else if (_animation.value > (line1 + arc1 + line2) && _animation.value <= (line1 + arc1 + line2 + arc2)) {
// 第4段,右下圆角 arc2
double t = (_animation.value - (line1 + arc1 + line2)) / r;
x = (w - r) + r * cos(t);
y = (h - r) + r * sin(t);
} else if (_animation.value > (line1 + arc1 + line2 + arc2) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3)) {
// 第5段,向左直线 line3
y = h;
x = (w - r) - (_animation.value - (line1 + arc1 + line2 + arc2));
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3)) {
// 第6段,左下圆角 arc3
double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3)) / r - 3 * pi / 2;
x = r + r * cos(t);
y = (h - r) + r * sin(t);
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3) && _animation.value <= (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) {
// 第7段,上行直线 line4
x = 0;
y = (h - r) - (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3));
} else if (_animation.value > (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4) && _animation.value <= totalLength) {
// 第8段,左上圆角 arc4
double t = (_animation.value - (line1 + arc1 + line2 + arc2 + line3 + arc3 + line4)) / r - pi;
x = r + r * cos(t);
y = r + r * sin(t);
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: w + 2 * ballR,
height: h + 2 * ballR,
child: Stack(
children: [
Positioned(
left: ballR,
top: ballR,
child: Container(
width: w,
height: h,
decoration: BoxDecoration(
color: Colors.yellow,
border: Border.all(
color: Colors.blue,
width: 2,
),
borderRadius: BorderRadius.circular(r)),
),
),
// Moving dot along the border
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Positioned(
left: x,
top: y,
child: Container(
width: ballR * 2,
height: ballR * 2,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.purple,
),
),
);
},
),
],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
网友评论