当前位置:网站首页>Exploration of flutter drawing skills: draw arrows together (skill development)
Exploration of flutter drawing skills: draw arrows together (skill development)
2022-07-29 06:01:00 【Code and thinking】
0. Preface
Some people may think , There is nothing to say about drawing arrows , Don't you just add two heads to one line ? In fact, the drawing of arrows is relatively complex , It also contains many painting tips . The arrow itself has a strong Schematic function , Usually used to indicate 、 mark 、 Connect . Various arrow ends , Plus the difference of linetype , It can be combined into some fixed connection syntax , such as UML The class diagram .

An arrow , Its core data is the coordinates of two points , from Left and right ends and Linetype constitute . This article will explore , How to draw a support for various styles , And easy to expand arrow .

1. Division of arrow position
First of all, let's say something , What I hope to get is the arrow route , Instead of simply drawing arrows . Because there is a path , Can do more , For example, cutting according to the path 、 Motion along path 、 Merge operations between multiple paths . Of course , After the path is formed , Drawing nature is very simple . So in drawing skills , Path is a very important topic .
As shown below , Let's first generate a three part path , And draw , Both ends are temporarily circular paths :
[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-evTuUncE-1657777368533)(https://upload-images.jianshu.io/upload_images/27762813-4c11341ebbedc0a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
The code implementation is as follows , The starting points for testing are (40,40) and (200,40), The circular path is centered on the starting point , Width height is 10. It can be seen that although the requirements are realized , But it's all written in one piece , The code looks messy . When it comes to generating arrows of various styles , Modifying the code here is also very troublesome , The next thing to do is to abstract the path formation process of the arrow .
final Paint arrowPainter = Paint();
Offset p0 = Offset(40, 40);
Offset p1 = Offset(200, 40);
double width = 10;
double height = 10;
Rect startZone = Rect.fromCenter(center: p0, width: width, height: height);
Path startPath = Path()..addOval(startZone);
Rect endZone = Rect.fromCenter(center: p1, width: width, height: height);
Path endPath = Path()..addOval(endZone);
Path linePath = Path()..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy);
arrowPainter
..style = PaintingStyle.stroke..strokeWidth = 1
..color = Colors.red;
canvas.drawPath(startPath, arrowPainter);
canvas.drawPath(endPath, arrowPainter);
canvas.drawPath(linePath, arrowPainter);
as follows , Defining abstract classes AbstractPath hold formPath Abstract it out , To be implemented by subclasses . The path of the endpoint is derived PortPath To implement , This can encapsulate some repetitive logic , It is also conducive to maintenance and expansion . The overall path is generated by ArrowPath Class responsible :
abstract class AbstractPath{
Path formPath();
}
class PortPath extends AbstractPath{
final Offset position;
final Size size;
PortPath(this.position, this.size);
@override
Path formPath() {
Path path = Path();
Rect zone = Rect.fromCenter(center: position, width: size.width, height: size.height);
path.addOval(zone);
return path;
}
}
class ArrowPath extends AbstractPath{
final PortPath head;
final PortPath tail;
ArrowPath({required this.head,required this.tail});
@override
Path formPath() {
Offset line = (tail.position - head.position);
Offset center = head.position+line/2;
double length = line.distance;
Rect lineZone = Rect.fromCenter(center:center,width:length,height:2);
Path linePath = Path()..addRect(lineZone);
Path temp = Path.combine(PathOperation.union, linePath, head.formPath());
return Path.combine(PathOperation.union, temp, tail.formPath());
}
}
such , Determination of rectangular field and generation of path , It is implemented by specific classes , It will be much more convenient in use :
double width =10;
double height =10;
Size portSize = Size(width, height);
ArrowPath arrow = ArrowPath(
head: PortPath(p0, portSize),
tail: PortPath(p1, portSize),
);
canvas.drawPath(arrow.formPath(), arrowPainter);

2. About the transformation of path
Our straight line above is actually a rectangular path , There will be some problems , For example, when the arrow is not a horizontal line , There will be the following problems :

The solution is simple , Just let the path of the rectangular straight line rotate along the center of two points , The angle of rotation is the angle between two points and the horizontal line . This involves very important skills in rendering : Matrix transformation . The following code adds four lines Matrix4 The operation of , Through matrix transformation , Give Way linePath With center Rotate the angle between two points for the center . Notice here ,tag1 The translation at is to change the transformation center to center、 and tag2 The reverse translation at is to offset tag1 The effect of translation . So the transformation between the two , That is to say center Centered transformation :
class ArrowPath extends AbstractPath{
final PortPath head;
final PortPath tail;
ArrowPath({required this.head,required this.tail});
@override
Path formPath() {
Offset line = (tail.position - head.position);
Offset center = head.position+line/2;
double length = line.distance;
Rect lineZone = Rect.fromCenter(center:center,width:length,height:2);
Path linePath = Path()..addRect(lineZone);
// By matrix transformation , Give Way linePath With center Rotate around the center The angle between two points
Matrix4 lineM4 = Matrix4.translationValues(center.dx, center.dy, 0); // tag1
lineM4.multiply(Matrix4.rotationZ(line.direction));
lineM4.multiply(Matrix4.translationValues(-center.dx, -center.dy, 0)); // tag2
linePath = linePath.transform(lineM4.storage);
Path temp = Path.combine(PathOperation.union, linePath, head.formPath());
return Path.combine(PathOperation.union, temp, tail.formPath());
}
}
So it's all right , Some might wonder , Why not use two points directly to form a path ? So there is no need to rotate :

I said before. , What we hope to get here is a Arrow path , Using linear mode, you can see the beauty of using rectangle . If we only deal with the movement of the path , It is necessary to calculate the point , More complicated . And use rectangle plus rotation , A lot of convenience :

3. Dimensional correction
It can be seen that , At present, it is a rectangular area with the starting and ending points as the center , But in fact, we need to make the vertices at both ends of the arrow on two points . There are two solutions : firstly , stay PortPath When generating paths , Correct the center of the rectangular area ; second , The first breakpoint is corrected by offset before the synthesis path .
[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-v4L8kZ2m-1657777368535)(https://upload-images.jianshu.io/upload_images/27762813-70f57bf7142182bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
I prefer the latter , Because I hope PortPath Only responsible for the generation of breakpoint path , Don't worry about anything else . in addition PortPath I don't know whether the end point is the starting point or the end point , Because the starting point needs to be offset along the direction , The end point needs to be offset in the opposite direction . The effect after treatment is as follows :

---->[ArrowPath#formPath]----
Path headPath = head.formPath();
Matrix4 headM4 = Matrix4.translationValues(head.size.width/2, 0, 0);
headPath = headPath.transform(headM4.storage);
Path tailPath = tail.formPath();
Matrix4 tailM4 = Matrix4.translationValues(-head.size.width/2, 0, 0);
tailPath = tailPath.transform(tailM4.storage);
Although it seems to be aligned with the vertex on the surface , But if you change the non horizontal line, you will see the clue . We need to Direction along the line Translate , in other words , Ensure that the straight line passes the center of the rectangular area :

As shown below , When we translate the breakpoint , You need to calculate the offset according to the angle of the line :

Path headPath = head.formPath();
double fixDx = head.size.width/2*cos(line.direction);
double fixDy = head.size.height/2*sin(line.direction);
Matrix4 headM4 = Matrix4.translationValues(fixDx, fixDy, 0);
headPath = headPath.transform(headM4.storage);
Path tailPath = tail.formPath();
Matrix4 tailM4 = Matrix4.translationValues(-fixDx, -fixDy, 0);
tailPath = tailPath.transform(tailM4.storage);
4. Drawing of arrows
Every PortPath There is a rectangular area , Next, just focus on drawing arrows in this area . Like the following p0 、p1 、p2 Can form a triangle :

The corresponding code is as follows :
class PortPath extends AbstractPath{
final Offset position;
final Size size;
PortPath(this.position, this.size);
@override
Path formPath() {
Path path = Path();
Rect zone = Rect.fromCenter(center: position, width: size.width, height: size.height);
Offset p0 = zone.centerLeft;
Offset p1 = zone.bottomRight;
Offset p2 = zone.topRight;
path..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..close();
return path;
}
}
Because in PortPath It is impossible to perceive whether the child is the head or the tail , So we can see that both arrows are left . It's also very simple , Just turn around 180° That's it .

in addition , Although it looks good , But there are also problems similar to the above , When changing coordinates , There will be disharmonious scenes . The solution is the same as before , Add rotation transformation for the arrow of the breakpoint according to the inclination of the line .

Rotate as follows , You can get the desired arrow ,tag3 You can rotate by the way 180° Adjust the tail point . In this way, the coordinates of two points are arbitrarily specified , You can get an arrow .

Matrix4 headM4 = Matrix4.translationValues(fixDx, fixDy, 0);
center = head.position;
headM4.multiply(Matrix4.translationValues(center.dx, center.dy, 0));
headM4.multiply(Matrix4.rotationZ(line.direction));
headM4.multiply(Matrix4.translationValues(-center.dx, -center.dy, 0));
headPath = headPath.transform(headM4.storage);
Matrix4 tailM4 = Matrix4.translationValues(-fixDx, -fixDy, 0);
center = tail.position;
tailM4.multiply(Matrix4.translationValues(center.dx, center.dy, 0));
tailM4.multiply(Matrix4.rotationZ(line.direction-pi)); // tag3
tailM4.multiply(Matrix4.translationValues(-center.dx, -center.dy, 0));
tailPath = tailPath.transform(tailM4.storage);
5. The expansion of the arrow
As can be seen from the above , This arrow breakpoint has strong expansion ability , As long as the corresponding path is formed in the rectangular area . For example, the arrow form with two sharp corners below , The path generation code is as follows :

class PortPath extends AbstractPath{
final Offset position;
final Size size;
PortPath(this.position, this.size);
@override
Path formPath() {
Rect zone = Rect.fromCenter(center: position, width: size.width, height: size.height);
return pathBuilder(zone);
}
Path pathBuilder(Rect zone){
Path path = Path();
Rect zone = Rect.fromCenter(center: position, width: size.width, height: size.height);
final double rate = 0.8;
Offset p0 = zone.centerLeft;
Offset p1 = zone.bottomRight;
Offset p2 = zone.topRight;
Offset p3 = p0.translate(rate*zone.width, 0);
path..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)
..lineTo(p3.dx, p3.dy)..lineTo(p2.dx, p2.dy)..close();
return path;
}
}
This is as follows , As long as change pathBuilder Path building logic in , You can get different arrow styles . And you just need to create a positive path in the rectangular area , The rotation of the arrow following the line has been encapsulated in ArrowPath in . This is it. Shielding the details , Simplify the use process . Otherwise, the angle deflection calculation is also performed when creating the path , Don't you bother to die .

Come here , The multi style arrow setting scheme should be ready . like Flutter Various in animation Curve equally , Derive through abstraction , Realize different types of numerical transformation . Here we can also abstract the behavior of path construction , To derive various path classes . The advantage of this is : In the implementation class , Additional parameters can be defined , Control the details of the drawing .
as follows , Abstract out PortPathBuilder , adopt fromPathByRect Method , Generate the path according to the rectangular area . stay PortPath You can rely on abstract To get the job done :
abstract class PortPathBuilder{
const PortPathBuilder();
Path fromPathByRect(Rect zone);
}
class PortPath extends AbstractPath {
final Offset position;
final Size size;
PortPathBuilder portPath;
PortPath(
this.position,
this.size, {
this.portPath = const CustomPortPath(),
});
@override
Path formPath() {
Rect zone = Rect.fromCenter(
center: position, width: size.width, height: size.height);
return portPath.fromPathByRect(zone);
}
}
When use , You can specify PortPathBuilder Implementation class of , To configure different endpoint styles , For example, to realize the routine at the beginning CustomPortPath :
class CustomPortPath extends PortPathBuilder{
const CustomPortPath();
@override
Path fromPathByRect(Rect zone) {
Path path = Path();
Offset p0 = zone.centerLeft;
Offset p1 = zone.bottomRight;
Offset p2 = zone.topRight;
path..moveTo(p0.dx, p0.dy)..lineTo(p1.dx, p1.dy)..lineTo(p2.dx, p2.dy)..close();
return path;
}
}
And three arrows ThreeAnglePortPath , We can rate extracted , As a construction input , In this way, the arrow can have more features , For example, here is 0.5 and 0.8 Comparison of :

class ThreeAnglePortPath extends PortPathBuilder{
final double rate;
ThreeAnglePortPath({this.rate = 0.8});
@override
Path fromPathByRect(Rect zone) {
Path path = Path();
Offset p0 = zone.centerLeft;
Offset p1 = zone.bottomRight;
Offset p2 = zone.topRight;
Offset p3 = p0.translate(rate * zone.width, 0);
path
..moveTo(p0.dx, p0.dy)
..lineTo(p1.dx, p1.dy)
..lineTo(p3.dx, p3.dy)
..lineTo(p2.dx, p2.dy)
..close();
return path;
}
}
Want to implement different endpoint types of arrows , Only in construction PortPath when , Specify corresponding portPath that will do . The two ends of the following red arrows are used respectively ThreeAnglePortPath and CirclePortPath .

ArrowPath arrow = ArrowPath(
head: PortPath(
p0.translate(40, 0),
const Size(10, 10),
portPath: const ThreeAnglePortPath(rate: 0.8),
),
tail: PortPath(
p1.translate(40, 0),
const Size(8, 8),
portPath: const CirclePortPath(),
),
);
Such a small arrow drawing system that users can expand freely has been able to work perfectly . You can experience one based on this abstract The meaning of , as well as polymorphic The embodiment of . There are many rendering tips of rotation transformation in this article , Next , Let's draw all kinds of PortPathBuilder Implementation class , To enrich the arrow drawing , Create a small but powerful arrow drawing library .
author : Zhang fengjietelie
link :https://juejin.cn/post/7120010916602576926
边栏推荐
- 识变!应变!求变!
- Spring, summer, autumn and winter with Miss Zhang (2)
- Interesting talk about performance optimization thread pool: is the more threads open, the better?
- day02作业之进程管理
- 【数据库】数据库课程设计一一疫苗接种数据库
- Laravel swagger add access password
- 【比赛网站】收集机器学习/深度学习比赛网站(持续更新)
- Detailed explanation of tool classes countdownlatch and cyclicbarrier of concurrent programming learning notes
- C# 判断用户是手机访问还是电脑访问
- Synchronous development with open source projects & codereview & pull request & Fork how to pull the original warehouse
猜你喜欢

"Shandong University mobile Internet development technology teaching website construction" project training log V

anaconda中移除旧环境、增加新环境、查看环境、安装库、清理缓存等操作命令

Operation commands in anaconda, such as removing old environment, adding new environment, viewing environment, installing library, cleaning cache, etc

【综述】图像分类网络

How to PR an open source composer project

day02作业之进程管理

『全闪实测』数据库加速解决方案

Ribbon学习笔记一

并发编程学习笔记 之 Lock锁及其实现类ReentrantLock、ReentrantReadWriteLock和StampedLock的基本用法

Flutter 绘制技巧探索:一起画箭头(技巧拓展)
随机推荐
Flink connector Oracle CDC synchronizes data to MySQL in real time (oracle19c)
主流实时流处理计算框架Flink初体验。
xtrabackup 的使用
ReportingService WebService Form身份验证
【数据库】数据库课程设计一一疫苗接种数据库
Huawei 2020 school recruitment written test programming questions read this article is enough (Part 2)
【bug】XLRDError: Excel xlsx file; not supported
rsync+inotyfy实现数据单项监控实时同步
并发编程学习笔记 之 原子操作类AtomicReference、AtomicStampedReference详解
[DL] introduction and understanding of tensor
浅谈分布式全闪存储自动化测试平台设计
【Clustrmaps】访客统计
Ribbon learning notes II
Super simple integration HMS ml kit face detection to achieve cute stickers
DataX installation
Operation commands in anaconda, such as removing old environment, adding new environment, viewing environment, installing library, cleaning cache, etc
并发编程学习笔记 之 工具类Semaphore(信号量)
Training log 6 of the project "construction of Shandong University mobile Internet development technology teaching website"
Super simple integration of HMS ml kit to realize parent control
Semaphore (semaphore) for learning notes of concurrent programming