0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter CustomPaint

Last updated at Posted at 2026-01-03

CustomPaint を理解したい。

はじめに

Canvas は紙
紙は、角を丸くしたり(clipRRect())、まるごと回転させる(rotate())ことができる。

Paint は筆
筆は、太さ(strokeWidth)や色(color)を変えることができる。

Path は 輪郭
輪郭は、時には線(stroke)を表現し、時には面(fill)を表現する。

CustomPaint とは

RenderObject から提供されるインターフェースを介して Canvas に触れることができる、唯一の Widget

Screenshot 2025-11-20 at 6.59.22.png
https://www.youtube.com/watch?v=zcJlHVVM84I

RenderObject とは Render Tree を形成するノード(3 つの tree)。

  1. Widget tree
    1. UI の設計図
  2. Element tree
    1. Widget と RenderObject の仲介役
  3. Render tree
    1. 描画を担当する RenderObject から構成される tree

RenderObject によって実行されるのは以下の 3 つ。

Screenshot 2025-12-23 at 5.16.33.png
https://www.youtube.com/watch?v=cq34RWXegM8

  • performaLayout()
    • 描画対象のサイズや位置を決定すること
    • Constraints go down, sizes go up, parent sets position.
  • paint()
    • perfoamLayout() によって 事前に計算されたサイズ を前提とした描画を行う
  • describeSemanticsConfiguration()
    • アクセシビリティ情報の構築

paint() は performLayout() 後の描画処理

Canvas とは

GPU に対する「描画命令」を、順に積み込んで搭載することができるバッファ

「描画命令」とは具体的には以下のようなメソッドを指す。

  • Canvas.drawRect()
  • Canvas.drawPath()
  • Canvas.drawCircle()

Canvas は状態を持っている

Canvas の状態は以下によって変更することができる。

  • Canvas.translate()
  • Canvas.rotate()
  • Canvas.scale()
  • Canvas.clipXXX()

Canvas は特定の RenderObject によって保有されるものではないため、Canvas の状態に加えた変更は、その後、あらゆる描画処理に影響を与える

そのため、Canvas の状態は一時保存しておき、あとから復元することができる。

Paint とは

「どう描くか」を管理する

描画ルールを設定するためのプロパティを保持する。

  • Paint.style
  • Paint.color
  • Paint.stroleWidth
  • Paint.blendMode
  • Paint.shader

Path とは

「点」「直線」「曲線」によって定義される幾何情報

fill することで「面」になり、strole することで「輪郭」になる。

CustomPaint の基本的な使い方

PainterPaint は別クラスであることに注意。

  1. CustomPainter のサブクラスを定義する
    1-1. paint() をオーバーライドする
    1-2. shouldRepaint() をオーバーライドする
  2. 1.で定義したサブクラスを CustomPaint のパラメータ painter に設定する
    (サブクラスは foregroundPainter にも設定できる)
1. CustomPainter のサブクラスを定義する
class _MyCustomPainter extends CustomPainter {}
1-1. paint() をオーバーライドする
class _MyCustomPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // TODO: implement paint
    throw UnimplementedError();
  }
}
1-2. shouldRepaint() をオーバーライドする
class _MyCustomPainter extends CustomPainter {
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}
2. サブクラスを CustomPaint ウィジェットのパラメータ painter に設定する
class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _MyCustomPainter(),
    );
  }
}
(サブクラスは foregroundPainter にも設定できる)
class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyCustomPainter(),
    );
  }
}

描画は Canvas.drawXXX() によって初めて要求される

描画は Canvas.drawXXX() によって初めて要求される。

Path.lineTo() だけでは何も起きない。

Canvas オブジェクトは描画命令を溜め込むことができる。

An interface for recording graphical operations.
Canvas は、graphical 操作を記録するためのインターフェース。
https://api.flutter.dev/flutter/dart-ui/Canvas-class.html

基本的な使い方
void paint(Canvas canvas, Size size) {
  final path = Path()
    ..moveTo(50, 50)
    ..lineTo(150, 50)
    ..lineTo(150, 150);  // 👈 これだけでは描画されない
    
  canvas.drawPath(  // 👈 drawXXX() によって初めて描画が実行
    path,
    Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4
      ..color = Colors.blue,
  );
}

サイズは指定しない場合 Size.zero

Canvas のサイズは、 child が指定されていれば child の大きさになり、child がなければ size で指定された大きさになる。

デフォルトは Size.zero なので、指定がない場合は何も描画されない

@override
Widget build(BuildContext context) {
  return CustomPaint(
    painter: CustomPaint(),
    child: ,
    size: ,
  );
}

基本的には指定された size の短形内で paint() されることが前提とされていて、領域外の 描画は動作が保証されない。

領域外に描画が意図せずはみ出した場合には、ClipRect で切り落とす(clip)することが推奨される。

size を超える描画は保証外
@override
void paint(Canvas canvas, Size size) {

}

CustomPaint

image.png

SingleChildRenderObjectWidget を継承した Widget

RenderObjectWidget については こちら

@override
Widget build(BuildContext context) {
  return CustomPaint(
    painter: ,
    foregroundPainter: ,
    child: ,
    size: ,
    isComplex: ,
    willChange: ,
  );
}

Canvas の座標系は CustomPaint の座標系と一致する(原点が一致)。

paint() は Flutter の 3 つの tree でいう Widget tree → Element tree → Render treeRenderObject のメソッドであるため、すでに build() の最終工程にある。

そのため paint() 内で setState()markNeedsLayout() を呼び出して再描画を要求することはできない。

painter が最背面、foregroundPainter が最前面に来る

以下の順で描画される。

  1. painter(background)
  2. child
  3. foregroundPainter

painter が最背面、foregroundPainter が最前面に来る。

child.png

背景をカスタムペイントしたい場合

背景をカスタムペイントしたい場合
@override
Widget build(BuildContext context) {
  return CustomPaint(
    painter: MyPainter(), // 👈 背景
    child: , // 背景の前面に描画するもの
  );
}

Screenshot 2025-12-26 at 11.35.44.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: _MyPainter(),
      size: const Size(200, 200),
      child: Container(
        color: Colors.blue,
        height: 100,
        width: 100,
      ),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.red;

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

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

前景をカスタムペイントしたい場合

前景をカスタムペイントしたい場合
@override
Widget build(BuildContext context) {
  return CustomPaint(
    foregroundPainter: MyPainter(), // 👈 前景
    child: , // 前景の背面に描画するもの
  );
}

Screenshot 2025-12-26 at 11.40.07.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
      child: Container(
        color: Colors.blue,
        height: 100,
        width: 100,
      ),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.red.withAlpha(100);

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

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

isComplex

true を設定すると繰り返しレンダリングするコストを、compositor が保持するキャッシュによって抑える働きがある。

設定しない場合、キャッシュすべきかどうかの判断は Flutter 側で行われる。

painterforegroundPainter の両方が null の場合、カスタムペイント対象が存在しないため、このフラグは無視される。

CustomPainter(abstract)

CustomPainterWidget ではない。

paint()

repaint が必要とされる(shouldRepaint()true を返す条件下)度に実行される。

paint() 内では上に記述したものは背面に描画され、下に記述したものは前面に描画される。

描画の順番ルール
void paint(Canvas canvas, Size size) {
  // 1 最背面
  canvas.drawXXX();

  // 2 背面
  canvas.drawXXX();

  // 3 前面
  canvas.drawXXX();

  // 4 最前面
  canvas.drawXXX();
}

shouldRepaint()

CustomPainter の状態が変わったときに再描画する必要があるかどうかを Flutter に教えるためのフラグ。

bool shouldRepaint(covariant CustomPainter oldDelegate)

covariant

MyPainterCustomPaint のサブクラス)の新しいインスタンス生成の度に呼ばれる。

MyPainter オブジェクトが新しい情報を持ち、それによって描画を行う場合は、条件付きで true を返すようにする。

@override
bool shouldRepaint(covariant _MyPainter old) => isRed != old.isRed;

Screen Recording 2026-01-01 at 6.37.09.gif

ソースコード
import 'dart:async';

import 'package:flutter/material.dart';

class MyCustomPaint extends StatefulWidget {
  const MyCustomPaint({super.key});

  @override
  State<MyCustomPaint> createState() => _MyCustomPaintState();
}

class _MyCustomPaintState extends State<MyCustomPaint> {
  late bool isRed;

  @override
  void initState() {
    super.initState();
    isRed = false;
    Timer.periodic(const Duration(seconds: 1), (timer) {
      setState(() {
        isRed = !isRed;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(isRed: isRed),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  _MyPainter({required this.isRed});
  final bool isRed;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.fill
      ..color = isRed ? Colors.red : Colors.blue;

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

  @override
  bool shouldRepaint(covariant _MyPainter old) => isRed != old.isRed; // 👈 常に false を返す実装をした場合、更新されない
}

Path

Path は「原点」から「終点」まで移動する。

image.png

final path = Path()
  ..moveTo(x1, y1)
  ..moveTo(x2, y2);

これによって「点」「直線」「円弧」「ベジェ曲線」を表現することができる。

Path は、開いた状態にも、閉じた状態にもなることができ、交差することも可能。

Flutter 公式では、「Path は複数の Sub-paths から構成される」と表現される。

閉じた Path は「領域」を表現することができる

線は Canvas.drawPath(Path path) で描画できる。

領域は Cnavas.clipPath(Path path) で切り落とすことができる。

理解するには、かなり数学的な領域に足を踏み入れることになりそう。

moveTo()

Path を指定された座標まで移動させる。

ペンを持ち上げて移動させるような処理になるため、新たな Sub-paths が開始される。

void moveTo(double x, double y) 

lineTo()

直線 の輪郭を追加する。

void lineTo(double x, double y)

lineTo() だけでは描画は行われない

close()

Path直線 で閉じる。

「終点」が「原点」に向かって閉じられることで「輪郭」が形成される。

fillType

「どこが内側か」を決める数学ルール。

CSS でも fill-rule として存在するアルゴリズム。

enum PathFillType {
  nonZero,
  evenOdd,
}
  • PathFillType.nonZero
    • Non-Zero Winding Rule
    • 任意の点から 反直線(任意の点から無限に伸びる直線)を引き Path が交差するたびに、Path の向きに応じて +1 / −1 を加算し、合計が 0 でなければ内側(exterior)、合計が 0 なら外側(exterior)
    • 向き とは
    • フォントや SVG 画像に対して使用
  • PathFillType.evenOdd
    • Even–odd rule
    • 任意の点から 反直線(任意の点から無限に伸びる直線)を引き、Path と交差する回数が 奇数 → 内側(interior)、偶数 → 外側(exterior)

PathFillType.nonZero

Screenshot 2025-12-28 at 19.10.13.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final path = Path()
      ..fillType = PathFillType.nonZero
      ..addRect(const Offset(0, 0) & const Size(200, 200))
      ..addRect(const Offset(50, 50) & const Size(100, 100));

    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;

    canvas.drawPath(path, paint);
  }

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

PathFillType.evenOdd

Screenshot 2025-12-28 at 19.13.26.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final path = Path()
      ..fillType = PathFillType.evenOdd
      ..addRect(const Offset(0, 0) & const Size(200, 200))
      ..addRect(const Offset(50, 50) & const Size(100, 100));

    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;

    canvas.drawPath(path, paint);
  }

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

Paint

Paint() コンストラクタ は非常にシンプルで、一切の初期化用のパラメータを持たない。

Screenshot 2025-12-26 at 11.51.46.png

各設定値(プロパティ)は デフォルト値 でインスタンス化され、.. を使ったカスケード記法で初期化する。

final paint = Paint()
  ..style = PaintingStyle.fill
  ..color = Colors.red;

既存の Paint オブジェクトから Paint.from(Paint other) でコピーすることもできる。

style

PaintingStyle
enum PaintingStyle {
  fill,
  stroke,
}

image.png
https://api.flutter.dev/flutter/dart-ui/Canvas/drawRect.html

  • PaintingStyle.fill
    • 領域内が塗り潰される
    • Path が明示的に close() していない場合、最終点から原点に向かって暗黙に close() が行われ、「領域」が形成される
  • PaintingStyle.stroke
    • 輪郭だけが描画される

PaintingStyle.fill

final paint = Paint()
  ..style = PaintingStyle.fill;

Screenshot 2025-12-27 at 6.44.42.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke  // 👈
      ..color = Colors.red;

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

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

PaintingStyle.stroke

final paint = Paint()
  ..style = PaintingStyle.stroke;

Screenshot 2025-12-27 at 6.41.53.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke  // 👈
      ..color = Colors.red;

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

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

blendMode

描く対象(pixel)の色や形(source)を、すでに描かれている色(destination)や背景とどう合成(blend)するかのアルゴリズム。

デフォルトは BlendMode.srcOver

BlendMode.src

destination を破棄してsource のみを描画する。

image.png
https://api.flutter.dev/flutter/dart-ui/BlendMode.html

BlendMode.dest

source を破棄してdestination のみを描画する。

image.png
https://api.flutter.dev/flutter/dart-ui/BlendMode.html

BlendMode.srcOver

source を destination の上に描く。destination が前面、source が背面になる。

image.png
https://api.flutter.dev/flutter/dart-ui/BlendMode.html

BlendMode.destOver

destination を source の上に描く。source が前面、destination が背面になる。

image.png
https://api.flutter.dev/flutter/dart-ui/BlendMode.html

BlendMode.clear

描いた部分を完全に透明にする。

image.png
https://api.flutter.dev/flutter/dart-ui/BlendMode.html

Rect

座標(Offset)とサイズ(Size)で構成されるオブジェクト。

生成方法がいくつも存在し、OffsetSize を用いて & 演算子で生成することもできる。

Rect myRect = const Offset(1.0, 2.0) & const Size(3.0, 4.0);
  • Rect.fromCenter({required Offset center, required double width, required double height})
  • Rect.fromCircle({required Offset center, required double radius})
  • Rect.fromLTRB(double left, double top, double right, double bottom)
  • Rect.fromLTWH(double left, double top, double width, double height)
  • Rect.fromPoints(Offset a, Offset b)

Canvas

状態を持ち、描画命令を蓄積するオブジェクト。

Flutter フレームワークの内部で使用され、開発者としては paint() で使用する。

drawRect()

void drawRect(Rect rect, Paint paint);

image.png
https://api.flutter.dev/flutter/dart-ui/Canvas/drawRect.html

サンプル
canvas.drawRect(Offset.zero & const Size(200, 200), paint);

Screenshot 2026-01-03 at 23.09.49.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.red;

    canvas.drawRect(Offset.zero & const Size(200, 200), paint);
  }

  @override
  bool shouldRepaint(covariant _MyPainter old) => false;
}

drawCircle()

void drawCircle(Offset c, double radius, Paint paint);

image.png
https://api.flutter.dev/flutter/dart-ui/Canvas/drawCircle.html

サンプル
canvas.drawCircle(const Offset(100, 100), 100, paint);

Screenshot 2026-01-03 at 23.08.49.png

ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.red;
      
    canvas.drawCircle(const Offset(100, 100), 100, paint);
  }

  @override
  bool shouldRepaint(covariant _MyPainter old) => false;
}

drawLine()

void drawLine(Offset p1, Offset p2, Paint paint);

image.png
https://api.flutter.dev/flutter/dart-ui/Canvas/drawLine.html

Screenshot 2026-01-03 at 23.06.30.png

サンプル
canvas.drawPath(path, paint);
ソースコード
import 'package:flutter/material.dart';

class MyCustomPaint extends StatelessWidget {
  const MyCustomPaint({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _MyPainter(),
      size: const Size(200, 200),
    );
  }
}

class _MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..color = Colors.red;
    final path = Path()
      ..lineTo(100, 0)
      ..lineTo(200, 100);
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant _MyPainter old) => false;
}
void drawImage(Image image, Offset offset, Paint paint);

Canavs が持つ状態

  • 座標変換行列(Transformation Matrix)
    • translate() = 平行移動
    • scale() = 拡大縮小
    • rotate() = 回転
    • skew() = 傾ける(歪む)
    • transform() = 任意変換
  • clip 領域
    • clipRect() = 四角形で clip
    • clipRRect() = 角丸四角形で clip
    • clipPath() = 任意のパスで clip

savesaveLayer() で push し、resotre() で pop することができる。

save()

現在の Canvas の状態 を一時保存する。

一時保存では stack に対して「状態」が push され、restore() によって「状態」が pop される。

getSaveCount()

save() された stack の数。

初期状態は 1 で、save()saveLayer() によってインクリメントする。

反対に restore() によってデクリメントする。

restore()

Canvas の状態 を、最後に save() した状態まで pop して戻す。

saveLayer()

現在の Canvas の状態 を保存し、以降の描画命令を新しい一時的な layer(オフスクリーンバッファ)に積むことができる。

ここでいう layer とは、GPU メモリ上に作られる一時的な描画バッファ(描画前 = オフスクリーン)を指す。

save() が状態を保存するだけなのに対して、saveLayer() は描画先そのものを切り替える という点が最大の違い。

void saveLayer(Rect? bounds, Paint paint)
  • Rect? bounds
    • 作成する layer の範囲
    • 可能な限り狭く指定することで GPU のコスト減、消費メモリ減できる
  • Paint paint
    • restore() を呼んだ際に saveLayer() 前の元の Canvas を合成(flatten)する際に使用される
    • blendModecolorFilterimageFilter

Rect? bounds

saveLayer() はコストが高い

通常 GPU は描画先(Canvas)に対する描画命令を都度、処理するのではなく、まとめてバッチ処理する。このときパフォーマンス向上のために描画命令の順番を入れ替えるなどの最適化が行われる。

一方で savelayer() の呼び出しは描画先 Canvas を切り替えてしまうため、GPU 内部の最適化が分断され、バッファの強制書き込み(flush) が発生し、バッチ処理の利点が失われる。

そのためパラメータの Rect? bounds によって、可能な限り作成する layer の範囲を狭めておくことが重要。

Paint paint

引数 Paint paint は作成する layer に対して影響を持つのではなく、restore() 後の元の layer に影響を持つ点に注意する。

代表的なユースケース

  • 複数の描画を一旦まとめて、透明化(半透明化)したいとき
    • 個別に半透明にすると重なりが暗くなるが、layer でまとめると、重なりを無視してグループ全体を透明にできる
  • BlendMode を使う時
    • BlendMode(例:dstIn / clear / srcIn 等)を使う合成は「既に描画された pixel」を source にして演算を行うため、一時 layer に描いてから合成しないと、期待どおりに動かないことがある
  • アンチエイリアス を処理するとき
    • clipXXX() で縁のアンチエイリアスを正しく処理するために、clipXXX の直後に saveLayer を置くことが推奨されている(公式サイト

アンチエイリアス

anti-aliased

1 pixel の中に対象の図形がどれくらいの割合で含まれているかを α(透明度)で表現し、滑らかな曲線を実現するための技術

厳密には α は「そのピクセルにどれだけ図形がかかっているか」を表し、それを「透明度」として扱って合成(blend)する近似モデルを指す。

アンチエイリアスによってガタガタした角丸ではなく、綺麗な角丸を表現することが可能になる。

image.png
https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%B3%E3%83%81%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9

  • α = 1.0
    • 透明度 100%(不透明)
    • その pixel には、対称図形が 100 % 含まれる
  • α = 0.5
    • 透明度 50%(半透明)
    • その pixel には、対称図形が 50 % 含まれる

Screenshot 2025-12-31 at 7.12.38.png

Screenshot 2025-12-31 at 7.18.57.png

α が 1 より小さい pixel では、pixel の色が合成(blend)される。

pixel の色の合成方法は Paint.blendMode によって制御できる。

例えば、デフォルトの BlendMode.srcOver では以下のような合成方法になっている。

BlendMode.srcOver
color = src * α + dst * (1 - α)

saveLayer() をせずに描画を行う場合、描画しようとしている対象物の pixel は常に「すでに画面上にいる pixel」と合成される

一方で saveLayer() の後に描画を行うと、描画しようとしている対象物の pixel は、まず saveLayer() によって作成された「透明な layer 上の pixel」と合成され、restore() の瞬間にだけ、「すでに画面上にいる pixel」と合成される。

Flutter 公式サイト ではこの挙動を理解するための以下 3 例が紹介されている。

推奨例
@override
void paint(Canvas canvas, Size size) {
  Rect rect = Offset.zero & size;
  
  canvas.save();
  canvas.clipRRect(RRect.fromRectXY(rect, 100.0, 100.0));

  // 透明な layer が作成される
  canvas.saveLayer(rect, Paint());
  
  // ===== ここから作成した layer に対する描画
  canvas.drawPaint(Paint()..color = Colors.red);
  canvas.drawPaint(Paint()..color = Colors.white);
  // ===== ここまで

  // saveLayer 分を restore する
  canvas.restore();

  // clopRRect 分を restore する
  canvas.restore();
}

Screenshot 2025-12-30 at 16.57.07.png

何を描画しているかが最初わからなかったので、白を消してみた。

@override
void paint(Canvas canvas, Size size) {
  Rect rect = Offset.zero & size;
  canvas.save();
  canvas.clipRRect(RRect.fromRectXY(rect, 100.0, 100.0));
  canvas.saveLayer(rect, Paint());
  canvas.drawPaint(Paint()..color = Colors.red);
  // canvas.drawPaint(Paint()..color = Colors.white);
  canvas.restore();
  canvas.restore();
}

Screenshot 2025-12-31 at 10.11.27.png

次に NG とされている例。

Screenshot 2025-12-30 at 16.58.42.png

推奨例と比較すると、赤い縁が見える。

推奨例 NG 例
Screenshot 2025-12-30 at 16.57.07.png Screenshot 2025-12-30 at 16.58.42.png
NG 例
void paint(Canvas canvas, Size size) {
  // (this example renders poorly, prefer the example above)
  Rect rect = Offset.zero & size;
  canvas.save();
  canvas.clipRRect(RRect.fromRectXY(rect, 100.0, 100.0));
  canvas.drawPaint(Paint()..color = Colors.red);
  canvas.drawPaint(Paint()..color = Colors.white);
  canvas.restore();
}

saveLayer() をしないことで、「赤 pixel」が drawPaint() 時点で「すでに画面上にいる背景 pixel」と合成される。

その状態で「白 pixel」が drawPaint() されるため、さらに「背景 + 赤 pixel」に対して「白 pixel」が再び合成される。

これによって「背景 + 赤 + 白 pixel」となって、赤が残り、結果として赤い縁があるように見えてしまっている。


続いて最後の例。

この例では saveLayer() を実行していないが、アンチエイリアスが効いている。

Screenshot 2025-12-30 at 16.59.39.png

void paint(Canvas canvas, Size size) {
  canvas.save();
  canvas.clipRRect(RRect.fromRectXY(Offset.zero & (size / 2.0), 50.0, 50.0));
  canvas.drawPaint(Paint()..color = Colors.white);
  canvas.restore();
  canvas.save();
  canvas.clipRRect(RRect.fromRectXY(size.center(Offset.zero) & (size / 2.0), 50.0, 50.0));
  canvas.drawPaint(Paint()..color = Colors.white);
  canvas.restore();
}

clipRRect() が 2 回実行され、それぞれで 1 回しか描画(pixel の合成)が実行されていないため、赤い縁が残らない。

以上をまとめると、saveLayer() は「描画途中の pixel を背景 pixel から layer として一度切り離し、最終結果を一度だけ背景と合成するための仕組み」と言える。

ベジェ曲線

Bézier Curve

ベジェ曲線とは 「制御点」が直線を引っ張ることで形が決まる曲線 を指す。

制御点は「速度」と「方向」を制御する。

DeCasteljau_quadratic.gifDeCasteljau_cubic.gif
https://ja.wikipedia.org/wiki/%E3%83%99%E3%82%B8%E3%82%A7%E6%9B%B2%E7%B7%9A

Flutter では以下の API を使用してベジェ曲線を描くことができる。

  • Path.quadraticBezierTo()
    • 制御点が 1 個
    • シンプルだができることも限られる
  • Path.cubicTo()
    • 制御点が 2 個
    • 複雑だが自由度が高い

Path.quadraticBezierTo()

制御点 (x1, y1) を使って (x2, y2) まで移動するベジェ曲線。

void quadraticBezierTo(double x1, double y1, double x2, double y2)

image.png
https://api.flutter.dev/flutter/dart-ui/Path/quadraticBezierTo.html

Path.cubicTo()

制御点 (x1, y1) / (x2, y2) を使って (x3, y3) まで移動するベジェ曲線。

void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)

image.png
https://api.flutter.dev/flutter/dart-ui/Path/cubicTo.html

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?