Flutter CustomPaint自定義繪畫示例詳解

正文

CustomPaint是Flutter中用於自由繪制的一個widget,它與android原生的繪制規則基本一致,以當前Canves(畫佈)的左上角為原點進行繪制。在有些場景中,我們會需要繪制一些高度定制化的組件,比如 UI 設計師給我們出瞭個難題 —— 弄一個奇形怪狀的邊框。這個時候我們就不能直接使用 Flutter 自帶的那些組件瞭,而是需要手動繪制組件,那就會需要用到 CuntomPaint 組件。CustomPaint 組件和前端的 Canvas差不多,允許我們在一個畫佈上繪制各種元素,包括點、線、矩形、圓弧、文字、圖片等等。

CustomPaint 介紹

CustomPaint是一個 Widget,其中有三個重要的參數:

CustomPaint(
  child: childWidget(),
  foregroundPainter: foregroundPainter(),
  painter: backgroundPainter(),
)

childCustomPaint的子組件;

painterforegroundPainter:都是 CustomPainter 類,用於定義 canvas 繪制的內容。區別在於,首先是執行 painter 的繪制指令。然後是在背景上渲染 child 子組件。最後,foregroundPainter 的內容會繪制在 child 上一層。

案例展示:

import 'package:demo202112/utils/common_appbar.dart';
import "package:flutter/material.dart";
class MyPaint extends StatelessWidget {
  const MyPaint({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: getAppBar('CustomPaint'),
      body: CustomPaint(
        painter: MyPainer(),
        child: Container(height: 80,width: 80,child: Text('child測試'),color: Colors.red,),
        foregroundPainter: MyForeGroundPainer(),
      ),
    );
  }
}
class MyPainer extends CustomPainter{
  late Paint _paint;
  @override
  void paint(Canvas canvas, Size size) {
    _paint = Paint();
    _paint.color = Colors.blue;
    canvas.drawCircle(Offset(100, 100), 100, _paint);
    canvas.drawLine(Offset(300, 300), Offset(400, 400), _paint);
    // TODO: implement paint
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}
class MyForeGroundPainer extends CustomPainter{
  late Paint _paint;
  @override
  void paint(Canvas canvas, Size size) {
    _paint = Paint();
    _paint.color = Colors.green;
    canvas.drawCircle(Offset(100, 100), 70, _paint);
    // canvas.drawLine(Offset(300, 300), Offset(400, 400), _paint);
    // TODO: implement paint
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}

運行效果:

child: 紅色區域,傳入一個子widget,這個widget圖層會在painter在上,在foregroundPainter之下。

painter:藍色區域。

foregroundPainter:綠色區域,它與painter都是CustomPainter類型的。通過名字大概也就知道瞭,它會在painter的上層,也就是說在同樣的位置去繪制,foregroundPainter 會覆蓋painter。

CustomPainter提供瞭一個paint繪圖方法供我們繪制圖形,該方法攜帶canvassize兩個參數,其中 canvas 是畫佈,size 是畫佈大小。canvas 提供瞭很多繪制圖形的方法,比如繪制路徑、矩形、圓形和線條等等。

//畫圓
drawCircle(Offset c, double radius, Paint paint) → void
//畫圖片
drawImage(Image image, Offset p, Paint paint) → void
//畫九宮圖
drawImageNine(Image image, Rect center, Rect dst, Paint paint) → void
//畫線
drawLine(Offset p1, Offset p2, Paint paint) → void
//畫橢圓
drawOval(Rect rect, Paint paint) → void
//畫文字
drawParagraph(Paragraph paragraph, Offset offset) → void
//畫Rect區域
drawRect(Rect rect, Paint paint) → void
//畫陰影
drawShadow(Path path, Color color, double elevation, bool transparentOccluder) → void

繪制點

class MyPoints extends CustomPainter{
  Paint _paint = Paint()
  ..color = Colors.red
  ..strokeWidth = 15;
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    var points =[
      Offset(0, 0),
      Offset(size.width/2, size.height/2),
      Offset(size.width, size.height),
    ];
    canvas.drawPoints(PointMode.points, points, _paint);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}

運行效果:

PointMode3種模式

  • points:點
  • lines:將2個點繪制為線段,如果點的個數為奇數,最後一個點將會被忽略
  • polygon:將整個點繪制為一條線

繪制線 和路徑

class MyGraph extends CustomPainter{
  final Paint _paint = Paint()
    ..color = Colors.red
    ..strokeWidth = 15;
  final Paint _paintPath = Paint()
    ..color = Colors.blue
    ..strokeWidth = 5
  ..style = PaintingStyle.fill;
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    //繪制線
    canvas.drawLine(Offset(0, 30),Offset(size.width-30, size.height), _paint);
    //繪制路徑
    var _path = Path()
    ..moveTo(0, 0)
    ..lineTo(size.width, 0)
    ..lineTo(size.width, size.height)
    ..close();
    canvas.drawPath(_path, _paintPath);
    //這裡註意Paint.style,還可以設置為PaintingStyle.fill,
    //繪制圓形
    canvas.drawCircle(Offset(size.width/2+50, size.height/2+50), 20, _paint);
    //繪制橢圓
    canvas.drawOval(Rect.fromLTRB(0, 0, size.width, size.height/2), _paint);
    //繪制弧
    canvas.drawArc(Rect.fromLTRB(0, 0, size.width, size.height), 0, pi/2, true, _paint);
    //繪制圓角矩形
    canvas.drawRRect(RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(10)), _paint);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}

運行效果:

繪制五子棋

首先繪制背景,淡黃色,再繪制棋盤網格線,隨後繪制黑白子,具體代碼:

class CustomPaintRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomPaint(
        size: Size(300, 300), //指定畫佈大小
        painter: MyPainter(),
      ),
    );
  }
}
class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    double eWidth = size.width / 15;
    double eHeight = size.height / 15;
    //畫棋盤背景
    var paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.fill //填充
      ..color = Color(0x77cdb175); //背景為紙黃色
    canvas.drawRect(Offset.zero & size, paint);
    //畫棋盤網格
    paint
      ..style = PaintingStyle.stroke //線
      ..color = Colors.black87
      ..strokeWidth = 1.0;
    for (int i = 0; i <= 15; ++i) {
      double dy = eHeight * i;
      canvas.drawLine(Offset(0, dy), Offset(size.width, dy), paint);
    }
    for (int i = 0; i <= 15; ++i) {
      double dx = eWidth * i;
      canvas.drawLine(Offset(dx, 0), Offset(dx, size.height), paint);
    }
    //畫一個黑子
    paint
      ..style = PaintingStyle.fill
      ..color = Colors.black;
    canvas.drawCircle(
      Offset(size.width / 2 - eWidth / 2, size.height / 2 - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
    //畫一個白子
    paint.color = Colors.white;
    canvas.drawCircle(
      Offset(size.width / 2 + eWidth / 2, size.height / 2 - eHeight / 2),
      min(eWidth / 2, eHeight / 2) - 2,
      paint,
    );
  }
  //在實際場景中正確利用此回調可以避免重繪開銷,本示例我們簡單的返回true
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

運行效果:

繪制是比較昂貴的操作,所以我們在實現自繪控件時應該考慮到性能開銷,下面是兩條關於性能優化的建議:

  • 盡可能的利用好shouldRepaint返回值;在UI樹重新build時,控件在繪制前都會先調用該方法以確定是否有必要重繪;假如我們繪制的UI不依賴外部狀態,那麼就應該始終返回false,因為外部狀態改變導致重新build時不會影響我們的UI外觀;如果繪制依賴外部狀態,那麼我們就應該在shouldRepaint中判斷依賴的狀態是否改變,如果已改變則應返回true來重繪,反之則應返回false不需要重繪。
  • 繪制盡可能多的分層;在上面五子棋的示例中,我們將棋盤和棋子的繪制放在瞭一起,這樣會有一個問題:由於棋盤始終是不變的,用戶每次落子時變的隻是棋子,但是如果按照上面的代碼來實現,每次繪制棋子時都要重新繪制一次棋盤,這是沒必要的。優化的方法就是將棋盤單獨抽為一個Widget,並設置其shouldRepaint回調值為false,然後將棋盤Widget作為背景。然後將棋子的繪制放到另一個Widget中,這樣落子時隻需要繪制棋子。

總結

CustomPaint class提供瞭讓用戶自定義widget的能力,它暴露瞭一個canvas,可以通過這個canvas來繪制widget,CustomPaint會先調用painter繪制背景,然後再繪制child,最後調用foregroundPainter來繪制前景。

canvas–畫佈,真正的繪制是由canvas跟paint來完成的,畫佈提供瞭各種繪制的接口來繪制圖形,除此以外畫佈還提供瞭平移、縮放、旋轉等矩陣變換接口,畫佈都有固定大小跟形狀,還可以使用畫佈提供的裁剪接口來裁剪畫佈的大小形狀等等

Paint—筆畫,是用來設置在畫佈上面繪制圖形時的一些筆畫屬性,如:顏色、線寬、繪制模式、抗鋸齒等等.

自繪控件非常強大,理論上可以實現任何2D圖像外觀,想更深入的瞭解,可以找到其對應的RenderObject對象,如Text Widget最終會通過RenderParagraph對象來通過Canvas實現文本繪制邏輯。瞭解瞭更底層的繪制邏輯,才能更好的在實際項目中靈活應用。

以上就是Flutter CustomPaint自定義繪畫示例詳解的詳細內容,更多關於Flutter CustomPaint 繪畫的資料請關註WalkonNet其它相關文章!

推薦閱讀: