CustomPainter 與 CustomPaint


在 Flutter 中想要繪圖的話,可以繼承 CustomPainter,它有兩個方法必須實作,paintshouldRepaint,前者用來定義如何繪圖,後者決定何時必須重繪。

這邊從簡單的繪圖開始,如果想在使用者每次觸控螢幕時的位置增加一個圓,那麼每次的觸控必須記錄使用者點過的每個位置,這些位置要傳給你的 CustomPainter 實作,例如:

class CirclesPainter extends CustomPainter {
  final List<Offset> points;

  CirclesPainter({this.points});

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.deepOrangeAccent;

    var radius = (size.width + size.height) / 20;
    points.forEach((point) {
      canvas.drawCircle(point, radius, paint);
    });
  }

  @override
  bool shouldRepaint(CirclesPainter other) => points.length != other.points.length; 
}

建立 CirclesPainter 時必須指定 points,表示使用者點過的每個點,而在 paint 方法部份,canvas 是代表畫布的物件,size 參數是畫布的尺寸,在方法中,Paint 實例就像是畫筆,在範例中只指定了顏色,而在繪製圓時,使用 size 來計算出圓的半徑,並透過 canvasdrawCircle 來畫圓。

shouldRepaint 表示何時該重繪圓,在這邊指定了 points 的長度與新的 CirclesPainter 中的 points 不同就重繪。

接著來定義一個 Widget,可以處理使用者的觸控事件,並記錄每次觸控的位置:

class Circles extends StatefulWidget {
  Circles({Key key}) : super(key: key);

  @override
  _CirclesState createState() => _CirclesState();
}

class _CirclesState extends State<Circles> {
  final List<Offset> points = [];

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      child: CustomPaint(
        size: Size.infinite,
        painter: CirclesPainter(points: List.from(points)),
      ),
      onPanDown: (details) {
        setState(() {
          points.add(details.localPosition);
        });
      },
    );
  }
}

在這邊可以看到使用了 GestureDetectoronPanDown,透過其 details 可以取得觸控的點,而其 childCustomPaint,它需要搭配一個 CustomPainter 實例來進行繪圖。

CustomPaint 可以有子 Widget,如果指定了 child,那麼會 size 會是子 Widget 的尺寸,若沒有指定 child,就會使用 size 的設定值,而 size 的預設值是 Size.zero

這邊的範例沒有指定 child,如果沒有指定 size 的話,那麼你的畫布尺寸就是 Size.zero,也就畫不出東西來了,這邊將 size 設為 Size.infinite,並不是就擁有了無限尺寸的畫布,別忘了〈OVERFLOW 是啥?〉中談過的,CustomPaint 最後實際尺寸,會受到父元件的約束,簡單來說,這邊 size 設為 Size.infinite目的,只是希望儘可能取得最大可用的繪圖尺寸。

接著只要將以上的元件組裝一下就可以了:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Openhome.cc')),
        body: Circles(),
      ),
    );
  }
}

來看一下執行效果:

CustomPainter 與 CustomPaint