Flutter 知識(shí)梳理 (自定義組件) - CustomPaint

一、背景知識(shí)

Android中,當(dāng)我們需要自定義View的時(shí)候,需要重寫View.draw(Canvas canvas)方法,并通過Painter結(jié)合CanvasAPI實(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)方法,使用PainterCanvas提供的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)childnull,代表默認(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)CustomPaintervoid 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的變換:scaletranslaterotateskew
  • Canvassaverestore方法運(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的案例,通過FlutterAPI去實(shí)現(xiàn)一下,由于AndroidFlutter的實(shí)現(xiàn)很相似,因此有想不明白的地方也可以去參考。

下面是一些參考的資料:

二、示例

以下是三個(gè)例子:

  • 簡(jiǎn)單示例:CustomPaintCustomPainter的基本結(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

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評(píng)論 1 282
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,595評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評(píng)論 2 372