对于一些复杂或不规则的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);//画笔颜色

示例:

  1. 制作棋盘、棋子
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")
              ),
            ],
          )
      ),
    );
  }
}

优化代码逻辑后,可完成正常的落子,且只会制作落子部分。