# Flutter 绘制番外篇 - 数学中的角度知识

7,271 阅读9分钟

#### 一、两点间的角度

##### 1. 把线信息画出来

``````class Line {
Line({
this.start = Offset.zero,
this.end = Offset.zero,
});

Offset start;
Offset end;

final Paint pointPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;

void paint(Canvas canvas){
canvas.drawLine(Offset.zero, end, pointPaint);
drawAnchor(canvas,start);
drawAnchor(canvas,end);
}

void drawAnchor(Canvas canvas, Offset offset) {
canvas.drawCircle(offset, 4, pointPaint..style = PaintingStyle.stroke);
canvas.drawCircle(offset, 2, pointPaint..style = PaintingStyle.fill);
}
}
``````

p1(60，60)p1(60，-80)p1(-60,-80)p1(-60,80)
``````class AnglePainter extends CustomPainter {
// 绘制虚线
final DashPainter dashPainter = const DashPainter(span: 4, step: 4);

final Paint helpPaint = Paint()
..style = PaintingStyle.stroke..color = Colors.lightBlue..strokeWidth = 1;

final TextPainter textPainter = TextPainter(
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
);

Line line = Line(start: Offset.zero, end: const Offset(60, 60));

@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
drawHelp(canvas, size);
line.paint(canvas);
}

void drawHelp(Canvas canvas, Size size) {
Path helpPath = Path()
..moveTo(-size.width / 2, 0)
..relativeLineTo(size.width, 0);
dashPainter.paint(canvas, helpPath, helpPaint);
drawHelpText('0°', canvas, Offset(size.width / 2 - 20, 0));
drawHelpText('p0', canvas, line.start.translate(-20, 0));
drawHelpText('p1', canvas, line.end.translate(-20, 0));
}

void drawHelpText(  String text, Canvas canvas, Offset offset, {
Color color = Colors.lightBlue
}) {
textPainter.text = TextSpan(
text: text,
style: TextStyle(fontSize: 12, color: color),
);
textPainter.layout(maxWidth: 200);
textPainter.paint(canvas, offset);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
``````

##### 2.角度计算

`Flutter` 中的 `Offset` 对象有 `direction` 属性，它是通过 `atan2` 反正切函数进行计算的。下面来看一下通过 `direction` 属性获取的角度特点。

``````class Line {
// 略同...

}

---->[源码: Offset#direction]----
double get direction => math.atan2(dy, dx);
``````

p1(60，60)p1(-60,80)p1(-60,-60)p1(60，-80)
``````drawHelpText(
'角度: \${(line.rad * 180 / pi).toStringAsFixed(2)}°',
canvas,
Offset(
-size.width / 2 + 10,
-size.height / 2 + 10,
),
);
``````

``````---->[Line]----
double get rad => (end - start).direction;

``````

##### 3.角度的使用

``````class Line with ChangeNotifier {
// 略同...

double get length => (end - start).distance;

notifyListeners();
}
}
``````

``````---->[AnglePainter#drawHelp]----
canvas.drawArc(
Rect.fromCenter(center: Offset.zero, width: 20, height: 20),
0,
false,
helpPaint,
);
``````

##### 4. 点任意的绕点旋转

``````已知: p0(a,b)、p1(c,d)，求 p1 绕 p0 顺时针旋转 θ 弧度后得到 p1' 点。

``````

其实算起来很简单，如下，旋转了 `θ` 弧度后得到 `p1'` 。以 `p0` 为参考系原点的话，`p1'` 的坐标呼之欲出。

``````令两点间角度为 rad, 两点间距离为 length, 则:

``````

``````double detaRotate = 0;
void rotate(double rotate) {
detaRotate = rotate - detaRotate;
end = Offset(
) +
start;
detaRotate = rotate;
notifyListeners();
}
``````

#### 二、你的点又何须是点

##### 1. 绘制箭头

``````void paint(Canvas canvas) {
canvas.save();
canvas.translate(start.dx, start.dy);
Path arrowPath = Path();
arrowPath
..relativeLineTo(length - 10, 3)
..relativeLineTo(0, 2)
..lineTo(length, 0)
..relativeLineTo(-10, -5)
..relativeLineTo(0, 2)..close();
canvas.drawPath(arrowPath,pointPaint);
canvas.restore();
}
``````

##### 2. 绘制图片

``````import 'dart:ui';
import 'line.dart';

class ImageZone {
final Image image;
final Rect rect;

Line? _line;

ImageZone({required this.image, this.rect = Rect.zero});

Line get line {
if (_line != null) {
return _line!;
}
Offset start = Offset(
-(image.width / 2 - rect.right), -(image.height / 2 - rect.bottom));
Offset end = start.translate(-rect.width, -rect.height);
_line = Line(start: start, end: end);
return _line!;
}
}
``````

`ImageZone` 中定义一个 `paint` 方法，通过 `canvas``line` 进行图片的绘制。这样方便在 `Line `类中进行图片绘制，简化 `Line` 的绘制逻辑。

``````---->[ImageZone]----
void paint(Canvas canvas, Line line) {
canvas.save();
canvas.translate(line.start.dx, line.start.dy);
canvas.translate(-line.start.dx, -line.start.dy);
canvas.drawImageRect(
image,
rect,
rect.translate(-image.width / 2, -image.height / 2),
imagePaint,
);
canvas.restore();
}
``````

`Line` 类中，添加一个 `attachImage` 方法，将 `ImageZone` 对象关联到 `Line`对象上。在 `paint`中只需要通过 `_zone` 对象进行绘制即可。

``````---->[Line]----
class Line with ChangeNotifier {
// 略同...

ImageZone? _zone;

void attachImage(ImageZone zone) {
_zone = zone;
start = zone.line.start;
end = zone.line.end;
notifyListeners();
}

void paint(Canvas canvas) {
// 绘制箭头略....
_zone?.paint(canvas, this);
}
``````

``````void _loadImage() async {
List<int> bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
_image = await decodeImageFromList(Uint8List.fromList(bytes));
line.attachImage(ImageZone(
rect: const Rect.fromLTRB(0, 93, 104, 212),
image: _image!,
));
}
``````

``````void _updateLine() {
line.rotate(ctrl.value * 2* pi/50);
}
``````

#### 三、线绕任意点旋转

``````已知，p0、p1、p2点坐标，线段 p0、p1 绕 p2 顺时针旋转 θ 弧度后的到 p0'、p1'。

``````

##### 1.问题分析

``````void rotate(double rotate,{Offset? centre}) {
//TODO
}
``````

##### 2.解决方案和代码处理

``````求 p0’ 的坐标，可以构建 p2,p0 线段，让该线段执行旋转逻辑，其 end 坐标即是 p0’。

``````

``````---->[Line]----
void _rotateByStart(double rotate) {
end = Offset(
) +
start;
}
``````

``````double detaRotate = 0;

void rotate(double rotate, {Offset? centre}) {
detaRotate = rotate - detaRotate;
centre = centre ?? start;
Line p2p0 = Line(start: centre, end: start); // tag1
Line p2p1 = Line(start: centre, end: end); // tag2
p2p0._rotateByStart(detaRotate);
p2p1._rotateByStart(detaRotate);
start = p2p0.end;
end = p2p1.end;
detaRotate = rotate;
notifyListeners();
}
``````

##### 3.线段分度值出坐标

0.20.50.8

``````Offset percent(double percent){
return Offset(