我們知道,應用開發如果單純只靠系統提供的控制元件,對於那些較為絢爛介面效果來說是遠遠不夠的,這就需要開發者自己去自定義繪製Widget。
當然,自定義Widget講究靈活性,同一種效果可以由多種實現方案,我們要做的就是找到代價最小、最高效的解決方案。
Flutter自定義繪製Widget
從如何使用Canvas draw/paint我們瞭解到,在Flutter中使用自繪方式自定義Widget,大致需要以下步驟:
繼承CustomPainter並重寫paint方法和shouldRepaint方法
在寫paint方法中繪製內容
使用CustomPaint來構建Widget
舉個栗子,比如下面程式碼就實現了30*30的網格
class BackGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
// 使用CustomPaint
child: CustomPaint(
size: Size(300, 300),
painter: MyPainter(),
),
),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
double eWidth = size.width / 30;
double eHeight = size.height / 30;
//畫棋盤背景
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill //填充
..color = Color(0x77cdb175); //背景為紙黃色
canvas.drawRect(Offset.zero & size, paint);
//畫棋盤網格
paint
..style = PaintingStyle.stroke //線
..color = Color(0xFF888888)
..strokeWidth = 1.0;
for (int i = 0; i <= 30; ++i) {
double dy = eHeight * i;
canvas.drawLine(Offset(0, dy), Offset(size.width, dy), paint);
}
for (int i = 0; i <= 30; ++i) {
double dx = eWidth * i;
canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}複製程式碼
顯示效果:
1、Flutter繪製相關知識
和Android開發中的自定義View類似,Flutter中的繪製也是依靠Canvas和Paint來實現的
1.1 Canvas
畫布,為開發者提供了點、線、矩形、圓形、巢狀矩形等繪製方法。
1.2 Paint
畫筆,可以設定抗鋸齒,畫筆顏色,粗細,填充模式等屬性,繪製時可以定義多個畫筆以滿足不同的繪製需求。
1.3 Offset
座標,可以用來表示某個點在畫布中的座標位置。
1.4 Rect
矩形,在圖形的繪製中,一般都是分割槽域繪製的,這個區域一般都是一個矩形,在繪製中通常使用Rect來儲存繪製的位置資訊。
1.5 座標系
在Flutter中,座標系原點(0,0)位於左上角,X軸向右變大,Y軸向下變大。
下面我們看一下Paint的常用基本屬性設定
Paint _paint = new Paint()
// 畫筆顏色
..color = Colors.red
// 畫筆筆觸型別
// round-畫筆筆觸呈半圓形輪廓開始和結束
// butt-筆觸開始和結束邊緣平坦,沒有外延
// square-筆觸開始和結束邊緣平坦,向外延伸長度為畫筆寬度的一半
..strokeCap = StrokeCap.round
//是否啟動抗鋸齒
..isAntiAlias = true
//繪畫風格,預設為填充,有fill和stroke兩種
..style=PaintingStyle.fill
..blendMode=BlendMode.exclusion//顏色混合模式
..colorFilter=ColorFilter.mode(Colors.blueAccent, BlendMode.exclusion)//顏色渲染模式
..maskFilter=MaskFilter.blur(BlurStyle.inner, 3.0)//模糊遮罩效果
..filterQuality=FilterQuality.high//顏色渲染模式的質量
..strokeWidth = 15.0;//畫筆的寬度複製程式碼
2、Flutter繪製方法介紹
接下來我們瞭解一下Flutter中Canvas的各種繪製方法
PS:以下各種繪製方法所使用的_paint定義如下:
var _paint = Paint()
..color = Color(0xFFFf0000)
..strokeWidth = 4
..style = PaintingStyle.stroke
..isAntiAlias = true;複製程式碼
2.1 繪製點
List<Offset> points = [
Offset(0, 0),
Offset(30, 50),
Offset(20, 80),
Offset(100, 40),
Offset(150, 90),
Offset(60, 110),
Offset(260, 160),
];
canvas.drawPoints(PointMode.points, points, _paint);
複製程式碼
PointMode是一個列舉類:
enum PointMode {
points,// 繪製點
lines,// 繪製點,且陣列內隔點相連,如1-2相連,3-4相連。如最後只剩下一個點,則不去繪製該點
polygon,// 陣列內相鄰點連線
}複製程式碼
2.2 繪製線
var _startPoint = Offset(30, 30);// 起始點
var _endPoint = Offset(100, 170);// 終點
canvas.drawLine(_startPoint, _endPoint, _paint);複製程式碼
2.3 繪製矩形
首先我們瞭解一下矩形Rect的構建:
// 利用矩形左邊的X座標、矩形頂部的Y座標、矩形右邊的X座標、矩形底部的Y座標確定矩形的大小和位置
Rect.fromLTRB(double left, double top, double right, double bottom)
// 利用矩形的左邊的X軸、頂邊的Y軸位置配合設定矩形的長和寬來確定矩形的大小和位置
Rect.fromLTWH(double left, double top, double width, double height)
// 利用矩形的中心點和矩形所在圓的半徑來確定矩形的大小和位置,此方法確定的是一個正方形
Rect.fromCircle({ Offset center, double radius })
// 利用矩形的左上角和右下角的座標確定矩形的大小和位置,
// 需要注意的是兩點座標中,如果X軸相同或者Y軸相同,確定的是一條線,
// 如果兩點XY座標都是同一個數字,確定的是一個點。
Rect.fromPoints(Offset a, Offset b) 複製程式碼
繪製矩形的方法
void drawRect(Rect rect, Paint paint)複製程式碼
2.4 繪製圓角矩形
圓角矩形的構建方式有以下幾種:
// 利用矩形的左邊的X座標、矩形頂部的Y座標、矩形右邊的X座標、矩形底部的Y座標
// 以及可選的四個頂點圓角來確定圓角矩形
RRect.fromLTRBAndCorners(
double left,
double top,
double right,
double bottom, {
Radius topLeft: Radius.zero,
Radius topRight: Radius.zero,
Radius bottomRight: Radius.zero,
Radius bottomLeft: Radius.zero,
})
// 需要先定義一個Rect,使用方式和fromLTRBAndCorners類似
RRect.fromRectAndCorners(
Rect rect,
{
Radius topLeft: Radius.zero,
Radius topRight: Radius.zero,
Radius bottomRight: Radius.zero,
Radius bottomLeft: Radius.zero
}
)
// 頂點X軸Y軸的圓角設定相同
const Radius.circular(double radius)
// 頂點X軸Y軸的圓角設定可以不相同
const Radius.elliptical(this.x, this.y)複製程式碼
繪製圓角矩形的方法
void drawRRect(RRect rrect, Paint paint)複製程式碼
// 利用矩形的左邊的X座標、矩形頂部的Y座標、矩形右邊的X座標、矩形底部的Y座標
// 以及一個Radius來確定圓角矩形
RRect.fromLTRBR(double left, double top, double right, double bottom, Radius radius)
// 需要先定義一個Rect,使用方式和fromLTRBR類似
RRect.fromRectAndRadius(Rect rect, Radius radius)複製程式碼
// 利用矩形的左邊的X座標、矩形頂部的Y座標、矩形右邊的X座標、矩形底部的Y座標
// 以及設定四個頂點的X軸相同圓角,設定四個頂點的Y軸相同圓角來確定圓角矩形
RRect.fromLTRBXY(double left, double top, double right, double bottom, double radiusX, double radiusY)
// 需要先定義一個Rect,使用方式和fromLTRBXY類似
RRect.fromRectXY(Rect rect, double radiusX, double radiusY)複製程式碼
圓角矩形繪製有一個需要注意的地方,如果設定矩形的半徑和圓角的半徑相等,則繪製出來的是一個圓
// 設定矩形的半徑和圓角的半徑不相等,效果如下面左圖
Rect rect = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 50.0);
RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(10.0));
// 設定矩形的半徑和圓角的半徑相等,效果如下面右圖
Rect rect = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 50.0);
RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(50.0));複製程式碼
2.5 繪製雙圓角矩形
Flutter中提供了繪製雙圓角矩形的方法
void drawDRRect(RRect outer, RRect inner, Paint paint)複製程式碼
這裡需要注意的是,內圓角矩形的半徑不能大於外圓角矩形的半徑,否則無法繪製。
// 第一種:外圓角矩形的半徑大於內圓角矩形半徑,如下面左圖
Rect rectOut = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 80.0);
Rect rectInner = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 40.0);
// 第二種:外圓角矩形的半徑等於內圓角矩形半徑,如下面中圖
Rect rectOut = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 80.0);
Rect rectInner = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 80.0);
// 第三種:外圓角矩形的半徑小於內圓角矩形半徑,無法繪製,如下面右圖
Rect rectOut = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 80.0);
Rect rectInner = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 81.0);複製程式碼
接下來繪製上面三種情況下的雙圓角矩形
RRect rRectOut = RRect.fromRectAndRadius(rectOut, Radius.circular(10.0));
RRect rRectInner = RRect.fromRectAndRadius(rectInner, Radius.circular(30.0));
canvas.drawDRRect(rRectOut, rRectInner, _paint);複製程式碼
2.6 繪製圓
// 根據圓心座標和圓的半徑確定圓的位置和大小void drawCircle(Offset c, double radius, Paint paint)複製程式碼
畫一個圓
// 繪製圓,Paint預設填充PaintingStyle.fill,如下面左圖canvas.drawCircle(Offset(100.0, 100.0), 50.0, _paint);// 繪製圓,Paint設定為不填充,如下面右圖_paint.style = PaintingStyle.stroke;canvas.drawCircle(Offset(100.0, 100.0), 50.0, _paint);複製程式碼
2.7 繪製橢圓
// Rect矩形區域的確定參見前文
void drawOval(Rect rect, Paint paint)
// 橢圓的寬度大於高度,如下面左圖
Rect rect= Rect.fromPoints(Offset(50.0, 50.0), Offset(130.0, 100.0));
// 橢圓的寬度小於高度,如下面中圖
Rect rect= Rect.fromPoints(Offset(40.0, 80.0), Offset(80.0, 170.0));
// 橢圓的寬度等於高度,繪製出的是圓,如下面右圖
Rect rect= Rect.fromPoints(Offset(80.0, 70.0), Offset(180.0, 170.0));複製程式碼
繪製一個橢圓
canvas.drawOval(rect, _paint);複製程式碼
2.8 繪製圓弧
// rect:矩形區域// startAngle:開始的弧度// sweepAngle:掃過的弧度,正數順時針,負數逆時針// useCenter:是否使用中心點繪製void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)複製程式碼
弧度
一週的弧度數為2πr/r=2π,360°角=2π弧度。
因此,1弧度約為57.3°,即57°17’44.806’’,1°為π/180弧度,近似值為0.01745弧度。
周角為2π弧度,平角(即180°角)為π弧度,直角為π/2弧度。
下表是一些特殊的角度和弧度之間的換算
角度 | 弧度 |
---|---|
0° | 0 |
30° | π/6 |
45° | π/4 |
60° | π/3 |
90° | π/2 |
120° | 2π/3 |
180° | π |
270° | 3π/2 |
360° | 2π |
使用π需要引入:
import 'dart:math';複製程式碼
下面我們嘗試繪製一些圓弧
var rect = Rect.fromCircle(center: Offset(100.0, 100.0), radius: 50.0);
// 開始度數為90度,順時針掃度數90度,如下方左圖
canvas.drawArc(rect, -pi / 2, pi / 2, false, _paint);
// 開始度數為0度,順時針掃度數60度,如下方中圖
canvas.drawArc(rect, 0, pi / 3, false, _paint);
// 開始度數為0度,逆時針掃度數180度,使用中心點繪製,如下方右圖
canvas.drawArc(rect, 0, -pi, true, _paint);複製程式碼
image-12.png
2.9 繪製路徑
void drawPath(Path path, Paint paint)複製程式碼
Path方法介紹
// 將路徑的起始點移動到指定的座標點上
void moveTo(double x, double y)
// 將起始點和終點連成線段,如果沒有顯式的指明起始點座標,預設為(0,0)
void lineTo(double x, double y)
// 起始點(30,30),終點(100,100),如下面左圖
Path path = new Path();
path.moveTo(30, 30);
path.lineTo(100, 100);
canvas.drawPath(path, _paint);
// 起始點預設(0,0),終點(100,100),如下面右圖
Path path = new Path();
path.lineTo(100, 100);
canvas.drawPath(path, _paint);複製程式碼
// 相對於當前位置座標在X軸上偏移dx,在Y軸上偏移dy
void relativeMoveTo(double dx, double dy)
// 起始點(0,0),和(20,20)之間連線,後移動到(30,30),和(70,70)之間連線
Path path = new Path();
path.lineTo(20, 20);
path.moveTo(30, 30);
path.lineTo(70, 70);
canvas.drawPath(path, _paint);
// 起始點(0,0),和(20,20)之間連線,
// 後以當前(20,20)座標為起始點X軸偏移30,Y軸偏移30,
// 即偏移後的新座標(50,50)和(70,70)之間連線
Path path = new Path();
path.lineTo(20, 20);
path.relativeMoveTo(30, 30);
path.lineTo(70, 70);
canvas.drawPath(path, _paint);複製程式碼
// 確定相對於當前位置座標,在X軸上偏移dx,在Y軸上偏移dy後的新座標,
// 確定好之後當前位置座標和新座標之間連線
void relativeLineTo(double dx, double dy)
// (0,0)和(10,10)連線,(10,10)和(20,20)連線,如下面左圖
Path path = new Path();
path.lineTo(10, 10);
path.lineTo(20, 20);
canvas.drawPath(path, _paint);
// (0,0)和(10,10)連線,(10,10)和當前位置偏移後的新座標(30,30)連線
Path path = new Path();
path.lineTo(10, 10);
path.relativeLineTo(20, 20);
canvas.drawPath(path, _paint);複製程式碼
// 繪製二階貝塞爾曲線
// 繪製需要一個起始點、一個終點和一個控制點
// 此方法的前兩個引數是控制點的座標,後兩個引數是終點的座標
void quadraticBezierTo(double x1, double y1, double x2, double y2)
// 繪製二階貝塞爾曲線
// 繪製需要一個起始點、一個終點和一個控制點
// 此方法的前兩個引數是控制點的座標,後兩個引數是終點的座標
// 和quadraticBezierTo不同在於終點座標的位置是當前起點位置座標X軸偏移x2,Y軸偏移y2後的座標
// 舉例(略)
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2)
// 繪製曲線,如下面左圖
Path path = new Path();
path.moveTo(150, 100);
path.quadraticBezierTo(250, 40, 250, 200);
canvas.drawPath(path, _paint);
// 繪製控制點
_paint.strokeWidth = 8;
_paint.color = Colors.blue;
canvas.drawPoints(PointMode.points, [Offset(250, 40)], _paint);
// 繪製曲線,如下面中圖
Path path = new Path();
path.moveTo(150, 100);
path.quadraticBezierTo(200, 200, 250, 200);
canvas.drawPath(path, _paint);
// 繪製控制點
_paint.strokeWidth = 8;
_paint.color = Colors.blue;
canvas.drawPoints(PointMode.points, [Offset(200, 200)], _paint);
// 繪製曲線,如下面右圖
Path path = new Path();
path.moveTo(150, 100);
path.quadraticBezierTo(60, 130, 150, 200);
canvas.drawPath(path, _paint);
// 繪製控制點
_paint.strokeWidth = 8;
_paint.color = Colors.blue;
canvas.drawPoints(PointMode.points, [Offset(60, 130)], _paint);複製程式碼
// 繪製三階貝塞爾曲線
// 繪製需要一個起始點、一個終點和兩個控制點
// 此方法的前四個引數分別是兩個控制點的XY座標,後兩個引數是終點的座標
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
// 繪製三階貝塞爾曲線
// 繪製需要一個起始點、一個終點和兩個控制點
// 此方法的前四個引數分別是兩個控制點的XY座標,後兩個引數是終點的座標
// 和cubicTo不同在於終點座標的位置是當前起點位置座標X軸偏移x2,Y軸偏移y2後的座標
// 舉例(略)
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3)複製程式碼
下面使用cubicTo繪製心形效果
var width = 300;
var height = 300;
Path leftPath = new Path();
// 畫筆移動到左邊曲線起點
leftPath.moveTo(width / 2, height / 4 + 20);
leftPath.cubicTo(
width / 8, height / 10,
width / 13, (height * 2) / 5,
width / 2, height - height / 4);
// 繪製心形左邊曲線
canvas.drawPath(leftPath, _paint);
Path rightPath = new Path();
// 畫筆移動到右邊曲線起點
rightPath.moveTo(width / 2, height / 4 + 20);
rightPath.cubicTo(
width - width / 8, height / 10,
(width * 12) / 13, (height * 2) / 5,
width / 2, height - height / 4);
// 繪製心形右邊曲線
canvas.drawPath(rightPath, _paint);
// 繪製兩個控制點
_paint.strokeWidth = 8;
_paint.color = Colors.blue;
canvas.drawPoints(
PointMode.points,
[
Offset(width / 8, height / 10),
Offset(width / 13, (height * 2) / 5),
Offset(width - width / 8, height / 10),
Offset((width * 12) / 13, (height * 2) / 5)
],
_paint);複製程式碼
// 畫弧線
// rect:矩形區域
// startAngle:開始的弧度
// sweepAngle:掃過的弧度,正數順時針,負數逆時針
// forceMoveTo:true-畫筆從當前位置抬起來,移動到弧線起點
// false-畫筆從當前位置不抬起來直接連線到弧線起點
void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
// 效果如下面左圖,畫筆從(80, 100)抬起來,移動到弧線起點
boo forceMoveTo = true;
// 效果如下面右圖,畫筆從(80, 100)不抬起來直接連線到弧線起點
boo forceMoveTo = false;
Path path = new Path();
path.moveTo(20, 40);
path.lineTo(80, 100);
path.arcTo(new Rect.fromLTWH(60, 60, 100, 100), 0, pi / 2, forceMoveTo);
canvas.drawPath(path, _paint);複製程式碼
// 封閉當前路徑,以上面程式碼為例,如下圖
void close();複製程式碼
複製程式碼
其他方法
// 為path新增矩形
void addRect(Rect rect);
// 為path新增圓角矩形
void addRRect(RRect rrect);
// 為path新增橢圓
void addOval(Rect oval)
// 為path新增圓弧
void addArc(Rect oval, double startAngle, double sweepAngle);
// 為path新增多邊形,points-座標陣列,close-是否首位封閉
void addPolygon(List<Offset> points, bool close);
// 為path新增一個path,偏移offset座標
void addPath(Path path, Offset offset, {Float64List matrix4})
// path路徑是否包含point座標點
bool contains(Offset point)
// 重置路徑
void reset();
// 給路徑做matrix4變換
Path transform(Float64List matrix4)複製程式碼
下面舉一個addPath的例子
Path path = new Path();
path.lineTo(20, 20);
Path path1 = new Path();
path1.lineTo(40, 40);
// 如下圖,addPath方法就是在(30,30)座標位置偏移(40,40)並連線
path.addPath(path1, Offset(30, 30));
canvas.drawPath(path, _paint);複製程式碼
由於篇幅原因,不可能將Canvas和Path的所有方法一一列舉,想要進一步瞭解可檢視相關原始碼方法。
總結
本文列舉了Flutter開發中,Canvas繪製流程常用的方法並提供了簡單的示例,可以看出,和Android的Canvas還是很相似的,上手也非常的快。要做出酷炫的Widget,最好還是需要配合動畫效果,當然,用canvas做些簡單的icon也是可以的。
作者簡介
風少,銅板街客戶端開發工程師,2013年5月加入團隊,目前主要負責APP端專案開發。
更多精彩內容,請掃碼關注 “銅板街技術” 微信公眾號。