效果圖

今天周末,收拾東西的時(shí)候,發(fā)現(xiàn)了一個(gè)尺子,我們叫它量角器,我記得上學(xué)的時(shí)候,借給一個(gè)女同學(xué)這樣的量角器,到現(xiàn)在還沒(méi)有還我,啥意思嗎?還還我不?

看來(lái)是沒(méi)戲了,同學(xué)嗎?那么小氣干嘛?不如自己畫一個(gè)?畢竟我是最會(huì)編程的電工嗎!

在自定義量角器之前,我先說(shuō)下,小樣就是小樣,本著實(shí)現(xiàn)效果為目的,當(dāng)然可能會(huì)有更好或者更優(yōu)的方式,就像做數(shù)學(xué)題目一樣,答案只有一個(gè),但是解答思路有很多種,當(dāng)然有好的建議和方式下面留言哦!
廢話不多說(shuō),走起!
觀察量角器
拿起桌子上的量角器,看了看,想了想,欸?我還是不記得借我尺子的那個(gè)同學(xué)叫啥來(lái)著?

Sorry!
這個(gè)量角器嗎?有這幾個(gè)特征。
- 半圓形(里面有4個(gè)半圓)
- 刻度線(長(zhǎng)的、中等的、短的)
- 有刻度值(正向,反向,注:這里0和180度省去,別問(wèn)為啥,因?yàn)椴缓每矗?/li>
- 測(cè)量輔助線(10的倍數(shù))
像這種純繪制的自定義基本上就是考驗(yàn)對(duì)Canvas API使用和數(shù)學(xué)知識(shí),下面使用Flutter實(shí)現(xiàn)。
具體實(shí)現(xiàn)
創(chuàng)建Widget
1、StatelessWidget Or StatefulWidget
其實(shí)這個(gè)區(qū)分跟簡(jiǎn)單,當(dāng)一個(gè)靜態(tài)的,沒(méi)有狀態(tài)改變的自定義就使用StatelessWidget,否則使用StatefulWidget。因?yàn)槌咦邮且粋€(gè)靜態(tài)的,一旦繪制完畢就不需要去改變了,所以說(shuō)我們直接創(chuàng)建一個(gè) StatelessWidget 就可以,
//量角器Widget
class SemiCircleRulerWidget extends StatelessWidget {
const SemiCircleRulerWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ...
}
}
2、尺寸如何定義
我們剛開始一定會(huì)考慮這個(gè)寬高是如何定義,到底是直接外面?zhèn)鬟M(jìn)來(lái)?還是怎么辦?我這里直接采取使用LayoutBuilder,通過(guò)LayoutBuilder,我們可以在布局過(guò)程中拿到父組件傳遞的約束信息,然后我們可以根據(jù)約束信息動(dòng)態(tài)的構(gòu)建不同的布局。
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
double radius;
//當(dāng)寬的一半大于等于高的時(shí)候,采取高來(lái)作為半徑。
if (constraints.maxWidth / 2 >= constraints.maxHeight) {
radius = constraints.maxHeight;
}
//當(dāng)寬的一半小于高的時(shí)候,采取寬的一半作為半徑。
else {
radius = constraints.maxWidth / 2;
}
//寬 = 2*半徑, 高 = 半徑
var size = Size(radius * 2, radius);
return CustomPaint(
size: size,
painter: SemiCircleRulerCustomPainter(radius),
);
},
);
這里簡(jiǎn)單做了一下判斷,為了在已有空間里繪制最大
- 當(dāng)寬的一半大于等于高的時(shí)候,采取高來(lái)作為半徑。
- 當(dāng)寬的一半小于高的時(shí)候,采取寬的一半作為半徑。
獲取半徑后,我們就可以為我們的CustomPaint設(shè)置大小了。
- 寬 = 2*半徑, 高 = 半徑
3、創(chuàng)建CustomPainter
class SemiCircleRulerCustomPainter2 extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
...
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
shouldRepaint返回ture表示需要重繪,返回false表示不需要重繪。所以這里我們直接返回false就可以。
4、paint方法
4.1 坐標(biāo)系
有的時(shí)候,我們要是經(jīng)常不寫這種自定義的話,很容易忘記這個(gè)坐標(biāo)系和畫布在調(diào)用一些api之后的狀態(tài)是啥樣子的?所以我一般是這么做的,先寫個(gè)繪制坐標(biāo)系為了查看當(dāng)時(shí)的坐標(biāo)系情況。
/*
* 繪制坐標(biāo)系( X軸 Y軸) 為了查看坐標(biāo)系位置
*/
void drawXY(Canvas canvas) {
//X軸
canvas.drawLine(
const Offset(0, 0),
const Offset(300, 0),
Paint()
..color = Colors.green
..strokeWidth = 3,
);
//Y軸
canvas.drawLine(
const Offset(0, 0),
const Offset(0, 300),
Paint()
..color = Colors.red
..strokeWidth = 3,
);
}

簡(jiǎn)單繪制一個(gè)坐標(biāo)系,為了更方便了解當(dāng)前的畫布情況。自定義完畢,刪了即可。
4.2 繪制量角器雛形
量角器是一個(gè)半圓,所以我們需要調(diào)用的API是
drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)
rect:定義承載圓弧形狀的矩形。通過(guò)設(shè)置該矩形可以指定圓弧的位置和大小。
startAngle: 設(shè)置圓弧是從哪個(gè)角度順時(shí)針繪畫的。順時(shí)針為正,逆時(shí)針為負(fù)(注意:這里是弧度值)
sweepAngle: 設(shè)置圓弧順時(shí)針掃過(guò)的角度。(注意:這里是弧度值)
useCenter: 繪制的時(shí)候是否使用圓心,我們繪制圓弧的時(shí)候設(shè)置為false,如果設(shè)置為true, 并且當(dāng)前畫筆的描邊屬性設(shè)置為Paint.Style.FILL的時(shí)候,畫出的就是扇形。
paint: 指定繪制的畫筆。
為了更好了解這個(gè)API,我們來(lái)看下案例
- 創(chuàng)建一個(gè)正方形
Rect rect = const Rect.fromLTWH(100, 100, 300, 300);
canvas.drawRect(
rect,
Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 3,
);

- 繪制一個(gè)圓弧
起始角度為0:
//繪制一個(gè)圓弧,從0度到90度
var paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 3;
canvas.drawArc(rect, degToRad(0), degToRad(90), false, paint);
//角度轉(zhuǎn)換為弧度
double degToRad(num deg) => deg * (pi / 180.0);
上面代碼中degToRad方法是一個(gè)角度到弧度的轉(zhuǎn)換。繪制結(jié)果是這樣的。

通過(guò)效果我們可以知道起始0度是從水平開始的,掃描是順時(shí)針掃描的。如果我們定義的起始不是0度呢?
起始角度為正數(shù):
canvas.drawArc(rect, degToRad(30), degToRad(150), false, paint);

起始角度為負(fù)數(shù):
canvas.drawArc(rect, degToRad(-90), degToRad(180), false, paint);

通過(guò)上面的了解,大概了解drawArc繪制情況了。
下面直接繪制量角器
/*
* 繪制表框(半圓)
*/
void drawBorder(Canvas canvas, Size size) {
Rect rect = Rect.fromCircle(
center: Offset(radius, radius),
radius: radius - borderStrokeWidth / 2,
);
//第四個(gè)參數(shù)設(shè)置為true,因?yàn)槌咦邮情]合的
canvas.drawArc(rect, -pi, pi, true, borderPaint);
}

4.3 繪制刻度線
繪制線使用的API是:
drawLine(Offset p1, Offset p2, Paint paint)
2點(diǎn)確定一條直線,所以p1 p2就是2點(diǎn)的坐標(biāo),只要我們按照我們需要傳入2個(gè)坐標(biāo)即可,這里就不單獨(dú)案例說(shuō)明了,直接走起。
- 定位:我想從半圓的左下角開始繪制刻度線。
- 繪制刻度線:180個(gè)刻度,繪制每個(gè)刻度,其中10的倍數(shù)為大刻度,5結(jié)尾的刻度為中刻度,其他為小刻度,這里0和180度我們省略,因?yàn)槔L制的話和圓弧底線重疊。
定位
這里說(shuō)的定位,意思是操作畫布來(lái)達(dá)到我想要繪制的起點(diǎn),方便我繪制,因?yàn)槲蚁朐谧笙陆情_始繪制刻度線,所以,我執(zhí)行下面的操作。
//畫布移動(dòng)到(radius, radius)點(diǎn)
canvas.translate(radius, radius);
//畫布旋轉(zhuǎn)-90度
canvas.rotate(degToRad(-90));
這2個(gè)操作后畫布變成啥樣子了,看我們的坐標(biāo)系就明白了。
執(zhí)行一行代碼:
canvas.translate(radius, radius);
drawXY(canvas);

看到坐標(biāo)系圓點(diǎn)移動(dòng)到了(radius, radius)上了。
執(zhí)行2行代碼:
canvas.translate(radius, radius);
canvas.rotate(degToRad(-90));
drawXY(canvas);

上圖就是執(zhí)行完移動(dòng)和旋轉(zhuǎn)后坐標(biāo)系的位置。這個(gè)時(shí)候我們可以繪制第一個(gè)刻度,也就是0度刻度線,如果我們需要繪制刻度線為20長(zhǎng)的刻度線怎么做呢?
通過(guò)坐標(biāo)系我們可以知道,0刻度的起始位置 X軸是0,Y軸是-radius(為了好理解,這里面沒(méi)有考慮畫筆的寬度啊,后面繪制會(huì)考慮),如果我們要繪制20長(zhǎng)度刻度的話,兩點(diǎn)坐標(biāo)可以為:
- Offset(0.0, -(radius - borderStrokeWidth / 2))
- Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
這里borderStrokeWidth是畫筆的寬度,因?yàn)橐紤]畫筆所以我們需要減去。
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
scalePaint,
);

看到左下角那個(gè)紅色橫線了嗎,這個(gè)就是0刻度線,那么1刻度線如何繪制呢?我們可以想下,如果我們想保持剛才繪制0刻度和1刻度的代碼不變,是不是只要把這個(gè)半圓逆時(shí)針旋轉(zhuǎn)1度就可以了,你想想是不是呢?

但是我們不好旋轉(zhuǎn)半圓?。吭趺锤?,反過(guò)來(lái)想,我們可以順時(shí)針旋轉(zhuǎn)畫布1刻度可以達(dá)到一樣的效果。來(lái)試試
//繪制0刻度
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
scalePaint,
);
//順時(shí)針旋轉(zhuǎn) 1度
canvas.rotate(degToRad(1));
//查看坐標(biāo)系
drawXY(canvas);
//繪制1刻度
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + 20),
scalePaint,
);


如我們所希望的樣子,呦西,趁勢(shì)追擊,直接一步到位。
/*
* 繪制刻度
*/
void drawScale(Canvas canvas, Size size) {
canvas.save();
canvas.translate(radius, radius);
canvas.rotate(degToRad(-90));
for (int index = 1; index < 180; index++) {
//旋轉(zhuǎn)角度
canvas.rotate(degToRad(1));
//大刻度
if (index % 10 == 0) {
//繪制最長(zhǎng)刻度
drawLongLine(canvas, size);
}
//中刻度
else if (index % 5 == 0) {
//繪制中刻度
drawMiddleLine(canvas, size);
}
//小刻度
else {
//繪制小刻度
drawShortLine(canvas, size);
}
}
canvas.restore();
}
/*
* 繪制長(zhǎng)線
*/
void drawLongLine(Canvas canvas, Size size) {
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + longScaleSize),
scalePaint,
);
}
/*
* 繪制中線
*/
void drawMiddleLine(Canvas canvas, Size size) {
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + middleScaleSize),
scalePaint,
);
}
/*
* 繪制短線
*/
void drawShortLine(Canvas canvas, Size size) {
canvas.drawLine(
Offset(0.0, -(radius - borderStrokeWidth / 2)),
Offset(0.0, -(radius - borderStrokeWidth / 2) + shortScaleSize),
scalePaint,
);
}

注意:上面代碼中出現(xiàn)的2行代碼
- canvas.save();
- canvas.restore();
這2位是成對(duì)出現(xiàn),不能單獨(dú)使用,他們的目的就是在你操作畫布(平移,旋轉(zhuǎn)等)之前,先調(diào)用save()方法對(duì)當(dāng)前畫布狀態(tài)的保存,當(dāng)你操作畫布繪制好圖形后,在調(diào)用restore()還原之前畫布的狀態(tài),不影響后面繪制操作。
4.3 繪制刻度值
刻度值是在10的倍數(shù)才繪制,所以我們直接可以在繪制刻度線代碼中,在繪制長(zhǎng)刻度線的地方,多繪制刻度值即可,還有就是這里有2種刻度值,一種順時(shí)針,一種逆時(shí)針,我們只要順時(shí)針直接采用當(dāng)前角度顯示即可,逆時(shí)針使用(180-當(dāng)前角度)即可。
這里因?yàn)樽鴺?biāo)系都是在對(duì)應(yīng)位置,所以直接繪制就行。
/*
* 繪制數(shù)字
*/
void drawScaleNum(Canvas canvas, int i) {
//繪制最外圈刻度值
textPainter.text = TextSpan(
text: "$i",
style: TextStyle(
color: Colors.black,
fontSize: numTextSize,
));
textPainter.layout();
double textStarPositionX = -textPainter.size.width / 2;
double textStarPositionY = -radius + outNumSize;
textPainter.paint(canvas, Offset(textStarPositionX, textStarPositionY));
//繪制內(nèi)圈刻度值
textPainter.text = TextSpan(
text: "${180 - i}",
style: TextStyle(
color: Colors.black,
fontSize: numTextSize,
));
textPainter.layout();
double textStarPositionX2 = -textPainter.size.width / 2;
double textStarPositionY2 = -radius + inNumSize;
textPainter.paint(canvas, Offset(textStarPositionX2, textStarPositionY2));
}

這里主要是對(duì)textPainter API的使用,這里主要1個(gè)注意點(diǎn),就是2個(gè)刻度值的間距,通過(guò)控制y坐標(biāo)來(lái)控制下就行,我這里 inNumSize=60,outNumSize=30,具體多少自己根據(jù)自己的審美修改就行,不做過(guò)多的解釋。
4.3 繪制內(nèi)部半圓
因?yàn)槲覀兩厦嬉呀?jīng)了解了繪制半圓的API,所以這里主要注意的點(diǎn)就是控制好半圓和刻度值的位置,避免重疊,其實(shí)也就是UI審美問(wèn)題。
/*
* 繪制外半圓
*/
void drawOuterSemicircle(Canvas canvas, Size size) {
Rect rect = Rect.fromCircle(
center: Offset(radius, radius),
radius: radius - borderStrokeWidth / 2 - interSemicircleSize,
);
canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
}
/*
* 繪制內(nèi)半圓
*/
void drawInnerSemicircle(Canvas canvas, Size size) {
Rect rect = Rect.fromCircle(
center: Offset(radius, radius),
radius: radius - borderStrokeWidth / 2 - outerSemicircleSize,
);
canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
}

4.4 繪制角度測(cè)量輔助線
角度測(cè)量輔助線也是10的倍數(shù)的刻度才繪制,所以也是在繪制刻度尺代碼中繪制10刻度的if里加上繪制角度測(cè)量輔助線代碼即可。輔助線起點(diǎn):Offset(radius, radius),終點(diǎn)是:內(nèi)半圓為結(jié)束,我們可以把每個(gè)半圓和刻度值距離邊緣的距離定義成變量,方便后續(xù)其他地方使用。
void drawScale(Canvas canvas, Size size) {
...
for (int index = 1; index < 180; index++) {
...
//大刻度
if (index % 10 == 0) {
...
// 繪制刻度線
drawScaleLine(canvas, size);
}
...
}
canvas.restore();
}
/*
* 繪制刻度線(10倍數(shù))
*/
void drawScaleLine(Canvas canvas, Size size) {
canvas.drawLine(
const Offset(0, 0),
Offset(0, -radius + scalePaintWidth + interSemicircleSize),
scalePaint,
);
}


什么鬼?不好看,下面那個(gè)輔助線起點(diǎn)太多時(shí),導(dǎo)致比較的密集,所以我想優(yōu)化下,在搞個(gè)小半圓給他蓋住,不讓別人知道你的丑。
4.5 繪制小半圓遮住你的美
我們直接畫個(gè)半圓,并且使用PaintingStyle.fill類型蓋住他,為了好看我在畫個(gè)半圓作為邊框,不愧是我啊。
/*
* 繪制最小的半圓
*/
void drawSmallSemicircle(Canvas canvas, Size size) {
//繪制半圓區(qū)域
Rect rect = Rect.fromCircle(
center: Offset(radius, radius - borderStrokeWidth / 2),
radius: radius / 10,
);
//這里先繪制半圓邊框
canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
//繪制白色半圓
semicirclePaint.color = Colors.white;
semicirclePaint.style = PaintingStyle.fill;
canvas.drawArc(rect, -pi, pi, true, semicirclePaint);
}

搞定!文章書寫不易,多多關(guān)注!
