4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Flutter】カスタマイズ描画(CustomPaint)のCanvas

Last updated at Posted at 2022-07-26

カスタマイズ描画について

※本記事は下記のZenn本にまとめました。

キャンバス変換

キャンバス変換関数は下記5つある。

void skew(double sx, double sy)
void rotate(double radians)
void scale(double sx, [double sy])
void translate(double dx, double dy)
void transform(Float64List matrix4)
  • translate(double dx, double dy)
    並行移動
  • scale(double sx, [double sy])
    スケール
  • rotate(double radians)
    回転する
  • skew(double sx, double sy)
    斜め切り替え
  • transform(Float64List matrix4)
    矩形移動

translate

translateはキャンパスのの中心を移動する関数です。
下記のソースで並行移動を確認しましょう。

// キャンバスの開始点を画面の中央に移動します
canvas.translate(size.width / 2, size.height / 2);

上記の中心点移動あるとないの違いは下記の図で一目瞭然です。黄色枠は基準です。
translate

ソースコード:

class Demo extends StatefulWidget {
  @override
  _DemoState createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        elevation: 0,
        title: const Text("Demo"),
      ),
      body: Center(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Container(
              color: Colors.yellow,
              height: 100,
              width: 100,
              child: CustomPaint(
                painter: CirclePainter(false),
              ),
            ),
            Container(
              color: Colors.yellow,
              height: 100,
              width: 100,
              child: CustomPaint(
                painter: CirclePainter(true),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class CirclePainter extends CustomPainter {
  final bool show;
  CirclePainter(this.show);
  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.blue;
    if (show) {
      // キャンバスの開始点を画面の中央に移動します
      canvas.translate(size.width / 2, size.height / 2);
    }
    canvas.drawCircle(const Offset(0, 0), 50, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

並行移動を用いてグリッドの描画もできます。
考え方は下記です。

  1. 描画スタートはキャンパスの中心へ移動
  2. 線を描く
  3. 中心をX軸方向へ並行移動し,横線を描く
  4. それを繰り返して処理する
  5. 中心点をリセットする
  6. 線を描く
  7. 中心をY軸方向へ並行移動し,縦線を描く
  8. それを繰り返して処理する

ソースコード:

  // グリッドの幅
  final double space = 20;
  Paint _gridPint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..color = color;

  void _drawGridRight(Canvas canvas, Size size) {
    // キャンパスの保存
    canvas.save();
    // 横線を描く
    for (int i = 0; i < size.width / 2 / space; i++) {
      canvas.drawLine(Offset(0, 0), Offset(0, size.height / 2), _gridPint);
      canvas.translate(space, 0);
    }
    // キャンパスリセット(原点リセット)
    canvas.restore();

    canvas.save();  
    // 縦線を描く
    for (int i = 0; i < size.height / 2 / space; i++) {
      canvas.drawLine(Offset(0, 0), Offset(size.width / 2, 0), _gridPint);
      canvas.translate(0, space);
    }
    canvas.restore();
  }

効果は下記通りです。

スケール

スケールを用いて鏡像を作ることは可能です。
では、スケールを用いて上記のグリッドを完成させましょう。

  void _drawGrid(Canvas canvas, Size size) {
    // 右下のグリッド
    _drawGridRight(canvas, size);

    canvas.save();
    // X軸の镜像
    canvas.scale(1, -1);
    _drawGridRight(canvas, size);
    // リセット
    canvas.restore();

    canvas.save();
    // Y軸の镜像
    canvas.scale(-1, 1);
    _drawGridRight(canvas, size);
    canvas.restore();

    canvas.save();
    // 原点の镜像
    canvas.scale(-1, -1);
    _drawGridRight(canvas, size);
    canvas.restore();
  }

描画効果は下記です。

X軸の鏡像 Y軸の鏡像 原点の鏡像

回転する

キャンパスを回転することで、ペンの位置を移動しなくても、円を描くことができます。

void _drawDot(Canvas canvas, Paint paint) {
    const int count = 12;
    canvas.save();
    for (int i = 0; i < count; i++) {
      var step = 2 * pi / count;
      canvas.drawLine(
        const Offset(80, 0),
        const Offset(100, 0),
        paint,
      );
      // キャンパスを回転する
      canvas.rotate(step);
    }
    canvas.restore();
  }

図形の描画


  • drawPoints、drawRawPoints

  • drawLine
  • 矩形
    drawRect、drawRRect、drawDRRect

  • drawCircle,drawOval,drawArc

点のモデルは下記3種類あります。

enum PointMode {
  points,
  lines,
  polygon,
}
void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    // グリッド
    _drawGrid(canvas, size);
    // 点のPaint
    var paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10
      ..strokeCap = StrokeCap.round;
    // 点を描く
    _drawPointsWithPoints(canvas, paint);
  }

  void _drawPointsWithPoints(Canvas canvas, Paint paint) {
    // 点の集合体
    const List<Offset> points = [
      Offset(-120, -20),
      Offset(-80, -80),
      Offset(40, 40),
      Offset(0, 0),
      Offset(40, -140),
      Offset(80, -160),
      Offset(120, -100),
    ];
    canvas.drawPoints(PointMode.points, points, paint);
  }


同じ点の集合体でモデルごとの表現は下記通りです。
PointMode.pointsはdrawRawPointsと同じ効果です。
PointMode.linesの場合、集合体が奇数個は最後の1つは無効です。
PointMode.polygonは線を順番で連結し、図形にします。

points lines polygon

上記の知識によって、drawPointsだけで折れ線グラフの作成は可能です。

void _drawPointsWithPoints(Canvas canvas, Paint paint) {
    const List<Offset> points = [
      Offset(-120, -20),
      Offset(-80, -80),
      Offset(-40, -60),
      Offset(0, 0),
      Offset(40, -140),
      Offset(80, 100),
      Offset(120, -100),
    ];
    paint.strokeWidth = 10;
    // 点を描く
    canvas.drawPoints(PointMode.points, points, paint);
    // 線を描く
    paint.strokeWidth = 2;
    canvas.drawPoints(PointMode.polygon, points, paint);
  }

上記の方法以外、線を描く関数も用意されています。
座標のスタートとエンドの宣言し、点と点を繋ぎます。下記のソースから座標のXY軸を描くこと出来ました。

  void _drawAxis(Canvas canvas, Size size) {
    _gridPint
      ..color = Colors.green
      ..strokeWidth = 1.5;
    canvas.drawLine(Offset(-size.width / 2, 0), Offset(size.width / 2, 0), _gridPint);
    canvas.drawLine(Offset(0, -size.height / 2), Offset(0, size.height / 2), _gridPint);
    canvas.drawLine(Offset(0, size.height / 2), Offset(0 - 7.0, size.height / 2 - 10), _gridPint);
    canvas.drawLine(Offset(0, size.height / 2), Offset(0 + 7.0, size.height / 2 - 10), _gridPint);
    canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2 - 10, 7), _gridPint);
    canvas.drawLine(Offset(size.width / 2, 0), Offset(size.width / 2 - 10, -7), _gridPint);
  }

矩形

  • drawRect
    矩形を宣言するコンストラクターは5つあります。
// 中心から矩形を描く
Rect.fromCenter
// 左上右下から矩形を描く
Rect.fromLTRB
// 左上幅高さから矩形を描く
Rect.fromLTWH
// 内接円から矩形を描く
Rect.fromCircle
// ポイントから矩形を描く
Rect.fromPoints

例から確認しましょう

  void _drawRect(Canvas canvas) {
    _gridPint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    //中心から矩形を描く
    Rect rectFromCenter = Rect.fromCenter(
      center: const Offset(0, 0),
      width: 160,
      height: 160,
    );
    canvas.drawRect(rectFromCenter, _gridPint);
    // 左上右下から矩形を描く
    Rect rectFromLTRB = const Rect.fromLTRB(-120, -120, -80, -80);
    canvas.drawRect(rectFromLTRB, _gridPint..color = Colors.red);
    // 左上幅高さから矩形を描く
    Rect rectFromLTWH = const Rect.fromLTWH(80, -120, 40, 40);
    canvas.drawRect(rectFromLTWH, _gridPint..color = Colors.orange);
    // 内接円から矩形を描く
    Rect rectFromCircle = Rect.fromCircle(
      center: const Offset(100, 100),
      radius: 20,
    );
    canvas.drawRect(rectFromCircle, _gridPint..color = Colors.green);
    // ポイントから矩形を描く
    Rect rectFromPoints = Rect.fromPoints(
      const Offset(-120, 80),
      const Offset(-80, 120),
    );
    canvas.drawRect(rectFromPoints, _gridPint..color = Colors.purple);
  }

  • drawRRect
    角丸矩形は同じく宣言するコンストラクターは5つあります。
// 中心から矩形を描く
RRect.fromRectXY
// 左上右下から矩形を描く
RRect.fromLTRBXY
// 左上幅高さから矩形を描く
RRect.fromLTRBR
// 内接円から矩形を描く
RRect.fromLTRBAndCorners
// ポイントから矩形を描く
RRect.fromRectAndCorners

例から確認しましょう,XYは角丸の中心です。


void _drawRRect(Canvas canvas) {
    _gridPint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    // fromRectXY
    Rect rectFromCenter = Rect.fromCenter(
      center: const Offset(0, 0),
      width: 160,
      height: 160,
    );
    canvas.drawRRect(RRect.fromRectXY(rectFromCenter, 40, 40), _gridPint);

    // fromLTRBXY
    canvas.drawRRect(
      const RRect.fromLTRBXY(-120, -120, -80, -80, 10, 10),
      _gridPint..color = Colors.red,
    );

    // fromLTRBR
    canvas.drawRRect(
      RRect.fromLTRBR(80, -120, 120, -80, const Radius.circular(10)),
      _gridPint..color = Colors.orange,
    );

    // fromLTRBAndCorners
    canvas.drawRRect(
      RRect.fromLTRBAndCorners(
        80,
        80,
        120,
        120,
        topLeft: const Radius.elliptical(10, 10),
        topRight: const Radius.elliptical(10, 10),
        bottomRight: const Radius.elliptical(10, 10),
        bottomLeft: const Radius.elliptical(10, 10),
      ),
      _gridPint..color = Colors.green,
    );

    // ポイントから矩形を描く
    Rect rectFromPoints = Rect.fromPoints(
      const Offset(-120, 80),
      const Offset(-80, 120),
    );
    canvas.drawRRect(
      RRect.fromRectAndCorners(
        rectFromPoints,
        bottomLeft: const Radius.elliptical(10, 10),
        topLeft: const Radius.elliptical(10, 10),
        topRight: const Radius.elliptical(10, 10),
        bottomRight: const Radius.elliptical(10, 10),
      ),
      _gridPint..color = Colors.purple,
    );
  }

  • drawDRRect
    二つの角丸矩形を描いて、両者の差分領域を描くことができます。利用場面としては写真のフレームを描く時とか。

outerとinner2つを定義しないといけないですが、制限としてはouterの領域はinnerより大きくしないといけないです。

void drawDRRect(
  RRect outer,
  RRect inner,
  Paint paint,
)

例:

void _drawDRRect(Canvas canvas) {
    _paint
      ..color = Colors.blue
      ..strokeWidth = 1.5;
    Rect outRect = Rect.fromCenter(center: Offset(0, 0), width: 160, height: 160);
    Rect inRect = Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100);
    canvas.drawDRRect(RRect.fromRectXY(outRect, 0, 0), RRect.fromRectXY(inRect, 20, 20), _paint);
  }

効果:

  • drawCircle(円)
  • drawOval(楕円)
  • drawArc(アーク)
void drawCircle(
  Offset c,
  double radius,// 半径
  Paint paint,
)

void drawOval(
 Rect rect,// 楕円の内接矩形
 Paint paint,
)

void drawArc(
  Rect rect,// 内接矩形
  double startAngle,// 開始ラジアン
  double sweepAngle,// 終了ラジアン
  bool useCenter,// 中心に連結するか
  Paint paint,
)

下記例で確認しましょう

  void _drawRound(Canvas canvas) {
    _paint.color = Colors.green;
    canvas.save();
    canvas.translate(-100, 0);
    // 円形
    canvas.drawCircle(const Offset(0, 0), 25, _paint);
    canvas.restore();

    // 楕円
    canvas.save();
    var rect = Rect.fromCenter(
      center: const Offset(0, 0),
      height: 40,
      width: 50,
    );
    canvas.translate(-40, 0);
    canvas.drawOval(rect, _paint);
    canvas.restore();

    // アーク
    canvas.save();
    canvas.translate(40, 0);
    canvas.drawArc(rect, 0, pi / 2 * 3, true, _paint);
    canvas.restore();

    // 中心に連結しない
    canvas.save();
    canvas.translate(100, 0);
    canvas.drawArc(rect, 0, pi / 2 * 3, false, _paint);
    canvas.restore();
  }

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?