对于一些复杂或不规则的UI,无法经过组合其他组件的办法来完成,就需求自己制作UI。简直一切的UI系统都会供给一个自绘UI的接口,比如iOS的CoreGraphics,Flutter 中供给一块2D画布Canvas,Canvas内部封装了一些根本的制作API,开发者能够经过Canvas制作各种自界说图形,在Flutter中供给了yigeCunstomPaint组件,它能够结合画笔CustomPainter来完成自界说图形制作。
CustomPaint
CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget child, //子节点,可认为空
})
- painter:布景画笔,显现在子节点后面;
- foregroundPainter: 远景绘笔,会显现在子节点的前面
- size:当child为null时,代表默许制作区域巨细,假如有child则忽略此参数,画布尺寸则为child尺寸。假如有child,但是想指定画布为特定巨细,能够运用SizeBox包裹CustomPainter完成。
- isComplex:是否复杂的制作,假如是,Flutter会使用一些缓存策略来减少重复烘托的开支。
- willChange:和isComplex合作运用,当启用缓存时,该特点代表鄙人一帧中制作是否会改动。
制作时需求供给远景或布景画笔,两者也能够一同供给。画笔需求继承CustomPainter类,在画笔类中完成真正的制作逻辑。
制作鸿沟RepaintBoundary
假如CustomPainter有子节点,为了防止子节点不必要的重绘并提高功能,通常情况下都会将子节点包裹在RepaintBoundary组件中,这样会在制作时就会创建一个新的制作层(Layer),其子组件将在新的Layer上制作,而父组件将在原来Layer上制作,也便是说RepaintBoundary子组件的制作将独立于父组件的制作,RepaintBoundary会阻隔其子节点和CustomPaint本身的制作鸿沟。
CustomPaint(
size: Size(300, 300), //指定画布巨细
painter: MyPainter(),
child: RepaintBoundary(child:...)),
)
CustomPainter与Canvas
CustomPainter中界说了一个虚函数paint
void paint(Canvas canvas, Size size);
paint有两个参数:
- Canvas:一个画布,包含各种制作办法:
API 称号 功用 drawLine 画线 drawPoint 画点 drawPath 画途径 drawImage 画图画 drawRect 画矩形 drawCircle 画圆 drawOval 画椭圆 drawArc 画弧 - Size:当前制作区域巨细
画笔Paint
画笔主要是供给各种特点:如颜色、粗细、款式等。
var paint = Paint() //创建一个画笔并配置其特点
..isAntiAlias = true //是否抗锯齿
..style = PaintingStyle.fill //画笔款式:填充
..color=Color(0x77cdb175);//画笔颜色
示例:
- 制作棋盘、棋子
class SSLPainter extends CustomPainter{
@override
// TODO: implement painter
void paint(Canvas canvas, Size size) {
var rect = Offset.zero & size;
drawChessboard(canvas, rect);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return false;
}
//制作棋盘
void drawChessboard(Canvas canvas, Rect rect){
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill
..color = const Color(0xFFDCC48C);
//制作布景
canvas.drawRect(rect, paint);
//制作网格
paint
..style = PaintingStyle.stroke
..color = Colors.black38
..strokeWidth = 1.0;
//制作横线
for (int i = 0; i <= 15; i ++){
double dy = rect.top + rect.height /15 * i;
canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
}
//制作竖线
for (int i = 0; i <= 15; i++){
double dx = rect.left + rect.width / 15 * i;
canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
}
}
}
class SSLForegroundPainter extends CustomPainter{
SSLForegroundPainter({required this.points});
Set<Offset> points;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
var rect = Offset.zero & size;
drawPieces(canvas, rect);
}
void drawPieces(Canvas canvas, Rect rect){
double width = rect.width / 15;
double height = rect.height / 15;
if (points.isNotEmpty){
var paint = Paint();
for (int i = 0; i < points.length; i ++){
Offset point = points.elementAt(i);
int x = point.dx ~/ width;
double xC = point.dx % width;
if (xC >= width/2){
x += 1;
}
int y = point.dy ~/ height;
double yC = point.dy % height;
if (yC >= height/2){
y += 1;
}
if (i%2 == 0){
//画黑子画笔
paint.style = PaintingStyle.fill;
paint.color = Colors.black;
}else{
//制作白子
paint.color = Colors.white;
}
// debugPrint("ssl touch pieces $x, $y, $width,$height\n");
canvas.drawCircle(Offset(x*width , y*height ), min(width/2.0, height/2.0), paint);
}
}
}
@override
bool shouldRepaint(covariant SSLForegroundPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
制作功能
制作是比较昂贵的操作,所以在完成自绘控件时应该考虑到功能开支。
- 尽或许的利用好shouldRepaint回来值,在UI树重新build时,空间在制作前都会调用该办法以确认是否必要重绘;假如制作的UI不依靠外部状况,即外部状况的改动不会影响到自绘的UI外观,那么应该回来false;假如依靠外部状况,那么应该在shouldRepaint中判断依靠的状况是否改动,假如改动则应该回来true来重绘,反之则应该回来false不需求重绘。
- 制作尽或许多的分层:在制作五子棋示例中,将棋盘和棋子分开制作。因为棋盘只需求制作一次,但是棋子或许制作多次,假如放一同,每次都重新制作,这是没必要的。优化时能够将棋盘独自抽为一个组件,并设置shouldRepaint为false,然后将棋盘组件作为布景,然后将棋子放到别的一个组件中,每次落子只需求制作棋子即可。减少不必要制作。
防止意外重绘
在上例中增加一个Button,点击后不做任何操作:
class SSLCustomPaintRouteTest extends StatelessWidget{
const SSLCustomPaintRouteTest({Key? key}):super(key: key);
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Custom Paint"),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CustomPaint(
size: const Size(300, 300),
painter: SSLPainter(),
),
ElevatedButton(onPressed: (){
}, child: const Text("Refresh")
),
],
)
),
);
}
}
发现控制台输出了许多“ssl painter paint”,也便是点击按钮时发生了多次重绘。但是在自界说CustomPainter时shouldRepaint回来的是false,点击改写按钮按道理讲应该是不触发重绘的。这个后面再研讨,目前简略认为按钮和CustomPaint是同一个画布,点击按钮会执行水波纹动画,水波纹动画执行过程中画布会不断的改写,导致了CustomPaint不断的重绘。解决方案是给CustomPainter或许按钮恣意一个增加RepaintBoundary父组件即可,考虑到实际场景中,或许会有多个组件,所以建议优先考虑给CustomPainter加,这样能够把CustomPaint阻隔开来:
class SSLCustomPaintRouteTestState extends State<SSLCustomPaintRouteTest>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Custom Paint"),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
children: [
CustomPaint(
size: const Size(300, 300),
painter: SSLPainter(),
),
const RepaintBoundary(
child: SSLCustomChessRoute(),
),
],
),
ElevatedButton(onPressed: (){
}, child: const Text("Refresh")
),
],
)
),
);
}
}
优化代码逻辑后,可完成正常的落子,且只会制作落子部分。