最终效果:
完成需要三个要素:
1.画一条贝塞尔曲线。
2.根据贝塞尔计算小车的移动轨迹。
3.计算小车的角度。
还是挺简单的吧。
开搞。。
首先我们来画一个简单的布局和一些基础的变量
画线
这里用到了CustomPainter。线不是必须的,只是为了好观察
计算小车的位置
这里主要靠一个公式,来求出贝塞尔曲线的路径点
var t = animation.value;
x偏移量=(pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) .toDouble()
y偏移量 =(pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) .toDouble();
计算出新的位置之后就可以了。这里可以根据动画的进度,计算出相应的贝塞尔曲线路径点。我们知道,动画的默认值是0-1 ,所以这就意味着,如果你想手动控制车辆在路径上的位置,而不是通过动画,那么你知道设置这个值就可以了,比如0.5,就是移动到路径的中间位置,0是开始位置,1是结束位置。
效果如下(车没了,用这个凑活一下 。)
按轨迹移动已经做完了,那我们完活收工!
开玩笑的。这东西是不是和学了三年动画的兄弟做出来的有一拼了。
现在还有两个问题
车辆的位置和车辆的角度。
可以发现偏移量的基准点是车组件的左上角。那我们计算的时候是不是可以把车辆的宽度计算进去是不是就可以了呢? 是的,就是这么简单 。
现在来调整一下在计算车辆偏移量的地方
已知车辆组件的宽度是50,那我们把它减去一半,就可以得到它的中心点
x偏移量=(pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) -25.toDouble()
y偏移量 =(pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) -25.toDouble();
来看效果
我们成功了,让小车的中间移动到了我们想要的位置。
接下来解决转向的问题,我的解题思路是这样的:
线是由点构成的,所以我们上面计算出了贝塞尔曲线的路径点,并将其运用到了移动当中。
那么我们是否只需要计算两个移动点之间相差的是否就可以了呢
例如我们有A、B、C、D4个点,a、b两条线,A点在a线上,B点在b线上(点画的有点大,示意图,大家凑合一下),我们的初始位置是A,然后我们把AB两点连线,然后计算AB线与a线的夹角,这样我们就知道了,B点在A点多少度的位置。然后B的角度,就是我们需要转向的角度。
思路大概就是这样。下面开始实操。其实数学基础好的同学,到这里,就已经知道该怎么做了,自己就可以动工了。我得出这个结论之后,又找了半天才知道原来有这样的一个函数,可以帮助计算。
这个函数是atan2。下边是百度的解释。
atan2是一个函数,在C语言里返回的是指方位角,C 语言中atan2的函数原型为 double atan2(double y, double x) ,返回以弧度表示的 y/x 的反正切。y 和 x 的值的符号决定了正确的象限。也可以理解为计算复数 x+yi 的辐角,计算时atan2 比 atan 稳定。
嗯~懂得都懂。
不懂的咱就先说结果 ,这个函数能帮助我们计算出我们所需要的角度,即:AB线与a线的夹角
atan2的结果范围是-3.14-3.14 ,也就是正负圆周率,它是一个弧度。
还得用一张图,借用一下百度的:
图中的y和x可以对应到atan2(y,x)中,这样可以计算出图中x轴和r半径的夹角,是不是和我们的需求一摸一样。
我们需要做的就是计算出这个y和x,然后利用函数就可以了。
现在来更新一下整理一下我们在刷新的时候需要做的事情,1.计算偏移量,2.计算角度。
整合一下,代码如下
运行一下
新的风暴出现了,偏移量发生了问题,不知道偏移到哪去了。
这个问题的原因是因为我们用的Transform组件,这个组件旋转的时候会有一个默认轴线(组件的左上角),围绕这个进行旋转,所以我们只需要设置一下他的中心点就可以了 有两个设置方法,一个是自动一个是手动
以下是完整代码,运行环境是 flutter2.2.3
引入了 flutter_svg 插件,用于使用svg,可以把内容换成其他的。
import 'dart:math';
import 'package:byb/page/test/xian.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class ActiveScreen extends StatefulWidget {
static String routeName = 'activeName';
@override
_ActiveScreenState createState() => _ActiveScreenState();
}
class _ActiveScreenState extends State
with TickerProviderStateMixin {
var p0;
var p1;
var p2;
late DataModle dataModle;
late AnimationController animationController;
late Animation animation;
@override
void initState() {
// 开始点
p0 = Offset(100, 100);
// 控制点
p1 = Offset(50, 150);
//结束点
p2 = Offset(300, 300);
dataModle = DataModle();
// 定义动画控制器
animationController =
AnimationController(vsync: this, duration: Duration(seconds: 3));
// 定义动画
animation =
// Tween(begin: Offset(100, 100), end: Offset(100, 201))
Tween(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.linear))
.animate(animationController);
animation.addListener(() {
updateOffset();
});
updateOffset();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.play_arrow),
onPressed: () {
animationController.reset();
animationController.forward();
})
],
),
body: Stack(
children: [
Transform(
origin: Offset(25, 25), // 手动调整,适合内容不是正方形的场景
// alignment: Alignment.center, // 自动居中,内容是正方形的时候可以用这个
transform: Matrix4.identity()
..translate(dataModle.left, dataModle.top, 0.0)
..rotateZ(dataModle.rotate),
child: Container(
height: 50,
width: 50,
color: Colors.red,
child: SvgPicture.asset("assets/icons/move_right.svg"),
),
),
CustomPaint(
painter: Xian(p0: p0, p1: p1, p2: p2),
),
],
),
);
}
updateOffset() {
// 获取动画的当前值
var t = animation.value;
// 得到组件当前的位置
Offset currentOffset = Offset(dataModle.left, dataModle.top);
// 计算并重新赋值
dataModle.left =
(pow(1 - t, 2) * p0.dx + 2 * t * (1 - t) * p1.dx + pow(t, 2) * p2.dx) -
25.toDouble();
dataModle.top =
(pow(1 - t, 2) * p0.dy + 2 * t * (1 - t) * p1.dy + pow(t, 2) * p2.dy) -
25.toDouble();
// 求出旋转弧度 下一个点对于现在这个点的弧度是多少?
double rorate = atan2(
dataModle.top - currentOffset.dy, dataModle.left - currentOffset.dx);
// double huDu = atan2(
// currentOffset.dy - dataModle.top, currentOffset.dx - dataModle.left);
double angle = rorate / pi * 180;
print("角度====$angle 弧度====$rorate");
if (rorate != 0.0) {
dataModle.rotate = rorate;
}
setState(() {});
}
}
class DataModle {
double left;
double top;
double rotate;
DataModle({this.left = 0.0, this.top = 0.0, this.rotate = 0.0});
}
import 'package:flutter/material.dart';
class Xian extends CustomPainter {
final p0;
final p1;
final p2;
Xian({this.p0, this.p1, this.p2});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..isAntiAlias = true
..strokeWidth = .3
..style = PaintingStyle.stroke
..color = Colors.black
..invertColors = false;
Path path = Path();
// 移动到起点
path.moveTo(p0.dx, p0.dy);
// 以p1为控制点,画一个到p2的曲线
path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
// 绘画
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(Xian oldDelegate) {
return this != oldDelegate;
}
}