一、背景知識(shí)
在Android
中,當(dāng)我們需要自定義View
的時(shí)候,需要重寫View.draw(Canvas canvas)
方法,并通過Painter
結(jié)合Canvas
的API
實(shí)現(xiàn)繪制的邏輯,Flutter
的核心實(shí)現(xiàn)跟這個(gè)很類似。
在Flutter
自定義組件中,有兩個(gè)重要的概念:
-
CustomPaint
:它是SingleChildRenderObjectWidget
的子類,我們將它放在Widget
樹的節(jié)點(diǎn)中,而CustomPainter
是它的一個(gè)屬性,負(fù)責(zé)實(shí)現(xiàn)具體的繪制邏輯。
class SimplePainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
painter: SimplePainter(),
),
);
}
}
-
CustomPainter
:通過繼承該類,并重void paint(Canvas canvas, Size size)
方法,使用Painter
和Canvas
提供的API
,完成繪制的過程。
下面,我們分別介紹一下這兩個(gè)概念。
1.1 CustomPaint
首先,CustomPaint
的定義如下:
const CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget child,
})
-
painter
:畫筆,顯示在子節(jié)點(diǎn)后面。 -
foregroundPainter
:前景畫筆,顯示在子節(jié)點(diǎn)前面。 -
size
:當(dāng)child
為null
,代表默認(rèn)大小,如果有child
則為child
大小。如果希望在有child
的時(shí)候限制大小,那么可以用SizeBox
包裹CustomPaint
。 -
isComplex
:是否復(fù)雜繪制,如果是,會(huì)應(yīng)用一些緩存策略來減少重復(fù)渲染的次數(shù)。 -
willChange
:和isComplex
配合使用,當(dāng)啟用緩存時(shí),該屬性代表在下一幀中繪制是否會(huì)改變。
在有子節(jié)點(diǎn)時(shí),為了避免子節(jié)點(diǎn)不必要的重繪并提高性能,通常會(huì)將子節(jié)點(diǎn)包裹在RepaintBoundary Widget
中。
這樣會(huì)在繪制時(shí)創(chuàng)建一個(gè)新的繪制層,其子Widget
將在新的Layer
繪制,父Widget
將會(huì)在原來的Layer
上繪制。
CustomPaint(
size: Size(300, 300),
painter: MyPainter(),
child: RepaintBoundary(child:...))
)
1.2 CustomPainter
通過實(shí)現(xiàn)CustomPainter
的void paint(Canvas canvas, Size size)
方法,并結(jié)合Paint
的屬性,完成繪制操作,如果之前有過Android
中自定義View
的經(jīng)驗(yàn),那么上手很快。
CustomPainter
涉及到的知識(shí)點(diǎn)有以下四個(gè)方面:
-
Paint
提供的屬性:圓角、顏色、線寬等等。 -
Canvas.drawXXX
方法:點(diǎn)、弧形、圓形、正方形、長(zhǎng)方形、路徑等等。 -
Canvas
的變換:scale
、translate
、rotate
、skew
。 -
Canvas
的save
和restore
方法運(yùn)用:save
方法作用是保存畫布當(dāng)前狀態(tài),restore
則是取出,例如要對(duì)畫布進(jìn)行多個(gè)動(dòng)作處理,第一個(gè)動(dòng)作進(jìn)行了縮放,如果沒有在縮放動(dòng)作處理前保存一下,那么在執(zhí)行第二個(gè)動(dòng)作時(shí)也會(huì)有縮放動(dòng)作的影響。
1.3 一些繪制 API 的參考資料
我做練習(xí)的時(shí)候分為了以下兩步,大家可以參考:
- 先過一遍
API
,對(duì)于支持哪些方法有一個(gè)基本的認(rèn)識(shí)。 - 在
Github
上,找一個(gè)Android
中自定義View
的案例,通過Flutter
的API
去實(shí)現(xiàn)一下,由于Android
和Flutter
的實(shí)現(xiàn)很相似,因此有想不明白的地方也可以去參考。
下面是一些參考的資料:
- Flutter 34: 圖解自定義 View 之 Canvas (一)
- Flutter 35: 圖解自定義 View 之 Canvas (二)
- Flutter 36: 圖解自定義 View 之 Canvas (三)
- Flutter自定義繪制(1)- 繪制基礎(chǔ)
- Canvas
- Paint
二、示例
以下是三個(gè)例子:
- 簡(jiǎn)單示例:
CustomPaint
和CustomPainter
的基本結(jié)構(gòu)。 - 棋盤示例:掌握基本的
API
。 - 儀表盤:
Canvas
變換、save/restore
、弧形、繪制文字,結(jié)合動(dòng)畫。
2.1 簡(jiǎn)單示例
一個(gè)最簡(jiǎn)單的CustomPainter
如下所示:
import 'package:flutter/material.dart';
void main() => runApp(CustomPainterWidget());
class CustomPainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("SimplePainter")),
body: SimplePainterWidget(),
),
);
}
}
class SimplePainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
painter: SimplePainter(),
),
);
}
}
class SimplePainter extends CustomPainter {
Paint painter = Paint()..color = Colors.blue
..style = PaintingStyle.fill
..strokeCap = StrokeCap.butt
..isAntiAlias = true;
@override
void paint(Canvas canvas, Size size) {
canvas.drawCircle(Offset(0, 0), 40, painter);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
效果圖:
image.png
2.2 棋盤示例
import 'package:flutter/material.dart';
import 'dart:math';
class GoCustomPainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
painter: GoCustomPainter(),
size: Size(300, 300),
),
);
}
}
class GoCustomPainter extends CustomPainter {
static const GRID_NUM = 12;
@override
void paint(Canvas canvas, Size size) {
var goWidth = min(size.width, size.height);
//1.繪制背景。
Paint paint = new Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;
canvas.drawRect(Rect.fromLTWH(0, 0, goWidth, goWidth), paint);
paint.color = Colors.black;
for (int i = 0; i <= GRID_NUM; i++) {
var index = goWidth / GRID_NUM * i;
//繪制橫線。
canvas.drawLine(Offset(0, index), Offset(goWidth, index), paint);
//繪制豎線。
canvas.drawLine(Offset(index, 0), Offset(index, goWidth), paint);
}
for (int i = 0; i < 8; i++) {
_drawDots(canvas, paint..color = Colors.white, goWidth);
_drawDots(canvas, paint..color = Colors.black, goWidth);
}
}
_drawDots(Canvas canvas, Paint paint, var goWidth) {
Random random = new Random();
var unit = goWidth / GRID_NUM;
var whiteX = unit * (1 + random.nextInt(GRID_NUM - 2));
var whiteY = unit * (1 + random.nextInt(GRID_NUM - 2));
canvas.drawCircle(Offset(whiteX, whiteY), 5, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
在主工程中引用:
import 'package:flutter/material.dart';
import 'go_custom_painter.dart';
void main() => runApp(CustomPainterWidget());
class CustomPainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("SimplePainter")),
body: GoCustomPainterWidget(),
),
);
}
}
效果圖:
image.png
2.3 儀表盤
import 'package:flutter/material.dart';
import 'dart:math';
import 'dart:ui';
class CanvasAnimateWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _CanvasAnimateWidgetState();
}
}
class _CanvasAnimateWidgetState extends State<CanvasAnimateWidget> with SingleTickerProviderStateMixin {
static const MAX_VALUE = 750.0;
static const VALUE = 500.0;
AnimationController controller;
Animation<double> animation;
var value = VALUE;
@override
void initState() {
super.initState();
controller = AnimationController(duration : Duration(seconds: 1), vsync: this);
animation = Tween(begin: 0.0, end : VALUE).animate(controller)
..addListener(() { setState(() {});});
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
painter: DashBoardPainter(value: animation.value, maxValue: MAX_VALUE),
size: Size(300, 300),
),
);
}
}
class DashBoardPainter extends CustomPainter {
static const int GRID_NUM = 24;
var maxValue;
var value;
DashBoardPainter({this.maxValue, this.value});
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint();
//1.繪制背景。
_drawBg(canvas, paint, size);
//2.繪制圓弧。
_drawArc(canvas, paint, size);
}
_drawBg(Canvas canvas, Paint paint, Size size) {
paint..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}
_drawArc(Canvas canvas, Paint paint, Size size) {
var padding = 10.0;
var width = size.width - 2*padding;
var height = size.height - padding;
canvas.save();
canvas.translate(padding, padding);
//1.繪制灰色的外環(huán)。
paint..color = Colors.white10
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
canvas.drawArc(Rect.fromCircle(center: Offset(width/2, height/2), radius: min(height, width)/2), pi, pi, false, paint);
//2.根據(jù)比例繪制白色的外環(huán)。
paint..color = Colors.white;
var faction = value / maxValue;
canvas.drawArc(Rect.fromCircle(center: Offset(width/2, height/2), radius: min(height, width)/2), pi, pi * faction, false, paint);
//3.繪制刻度的環(huán)。
var arcX = 10.0;
var arcWidth = 10.0;
paint..strokeWidth = arcWidth..color = Colors.white10;
canvas.drawArc(Rect.fromCircle(center: Offset(width/2, height/2), radius: width/2 - arcX - arcWidth/2), pi, pi, false, paint);
//4.繪制刻度的橫線,已經(jīng)跨過的部分是白色,否則為淺色。
paint.strokeWidth = 2.0;
var threadHold = (value / (maxValue / GRID_NUM));
for (var i = 0; i <= GRID_NUM; i++) {
canvas.save();
paint.color = i <= threadHold ? Colors.white : Colors.white24;
canvas.translate(width/2, height/2);
canvas.rotate(pi*i/GRID_NUM);
canvas.translate(-width/2, -height/2);
canvas.drawLine(Offset(arcX, height/2), Offset(arcX+arcWidth, height/2), paint);
canvas.restore();
}
//5.繪制文字。
TextSpan textSpan = TextSpan(
style: TextStyle(
color: Colors.white,
fontSize: 50
),
text: '${(value as double).toStringAsFixed(0)}'
);
TextPainter textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr
);
textPainter.layout();
textPainter.paint(canvas, Offset(width/2 - textPainter.width/2, height/3));
canvas.restore();
}
@override
bool shouldRepaint(DashBoardPainter oldDelegate) =>
(maxValue != oldDelegate.maxValue || value != oldDelegate.value);
}
實(shí)現(xiàn)文件。
import 'package:flutter/material.dart';
import 'dash_board_painter.dart';
void main() => runApp(CustomPainterWidget());
class CustomPainterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("SimplePainter")),
body: CanvasAnimateWidget(),
),
);
}
}
效果圖:
image.png