CustomPaint を理解したい。
はじめに
Canvas は紙。
紙は、角を丸くしたり(clipRRect())、まるごと回転させる(rotate())ことができる。
Paint は筆。
筆は、太さ(strokeWidth)や色(color)を変えることができる。
Path は 輪郭。
輪郭は、時には線(stroke)を表現し、時には面(fill)を表現する。
CustomPaint とは
RenderObject から提供されるインターフェースを介して Canvas に触れることができる、唯一の Widget。

https://www.youtube.com/watch?v=zcJlHVVM84I
RenderObject とは Render Tree を形成するノード(3 つの tree)。
- Widget tree
- UI の設計図
- Element tree
- Widget と RenderObject の仲介役
- Render tree
-
描画を担当する
RenderObjectから構成される tree
-
描画を担当する
RenderObject によって実行されるのは以下の 3 つ。

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.stylePaint.colorPaint.stroleWidthPaint.blendModePaint.shader
Path とは
「点」「直線」「曲線」によって定義される幾何情報。
fill することで「面」になり、strole することで「輪郭」になる。
CustomPaint の基本的な使い方
※ Painter と Paint は別クラスであることに注意。
-
CustomPainterのサブクラスを定義する
1-1.paint()をオーバーライドする
1-2.shouldRepaint()をオーバーライドする - 1.で定義したサブクラスを
CustomPaintのパラメータpainterに設定する
(サブクラスはforegroundPainterにも設定できる)
class _MyCustomPainter extends CustomPainter {}
class _MyCustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
throw UnimplementedError();
}
}
class _MyCustomPainter extends CustomPainter {
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
class MyCustomPaint extends StatelessWidget {
const MyCustomPaint({super.key});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _MyCustomPainter(),
);
}
}
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)することが推奨される。
@override
void paint(Canvas canvas, Size size) {
}
CustomPaint
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 tree の RenderObject のメソッドであるため、すでに build() の最終工程にある。
そのため paint() 内で setState() や markNeedsLayout() を呼び出して再描画を要求することはできない。
painter が最背面、foregroundPainter が最前面に来る
以下の順で描画される。
-
painter(background) childforegroundPainter
painter が最背面、foregroundPainter が最前面に来る。
背景をカスタムペイントしたい場合
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MyPainter(), // 👈 背景
child: , // 背景の前面に描画するもの
);
}
ソースコード
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: , // 前景の背面に描画するもの
);
}
ソースコード
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 側で行われる。
painter と foregroundPainter の両方が null の場合、カスタムペイント対象が存在しないため、このフラグは無視される。
CustomPainter(abstract)
CustomPainter は Widget ではない。
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)
MyPainter(CustomPaint のサブクラス)の新しいインスタンス生成の度に呼ばれる。
MyPainter オブジェクトが新しい情報を持ち、それによって描画を行う場合は、条件付きで true を返すようにする。
@override
bool shouldRepaint(covariant _MyPainter old) => isRed != old.isRed;
ソースコード
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 は「原点」から「終点」まで移動する。
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)
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
ソースコード
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
ソースコード
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() コンストラクタ は非常にシンプルで、一切の初期化用のパラメータを持たない。
各設定値(プロパティ)は デフォルト値 でインスタンス化され、.. を使ったカスケード記法で初期化する。
final paint = Paint()
..style = PaintingStyle.fill
..color = Colors.red;
既存の Paint オブジェクトから Paint.from(Paint other) でコピーすることもできる。
style
enum PaintingStyle {
fill,
stroke,
}

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;
ソースコード
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;
ソースコード
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 のみを描画する。

https://api.flutter.dev/flutter/dart-ui/BlendMode.html
BlendMode.dest
source を破棄してdestination のみを描画する。

https://api.flutter.dev/flutter/dart-ui/BlendMode.html
BlendMode.srcOver
source を destination の上に描く。destination が前面、source が背面になる。

https://api.flutter.dev/flutter/dart-ui/BlendMode.html
BlendMode.destOver
destination を source の上に描く。source が前面、destination が背面になる。

https://api.flutter.dev/flutter/dart-ui/BlendMode.html
BlendMode.clear
描いた部分を完全に透明にする。

https://api.flutter.dev/flutter/dart-ui/BlendMode.html
Rect
座標(Offset)とサイズ(Size)で構成されるオブジェクト。
生成方法がいくつも存在し、Offset と Size を用いて & 演算子で生成することもできる。
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);

https://api.flutter.dev/flutter/dart-ui/Canvas/drawRect.html
canvas.drawRect(Offset.zero & const Size(200, 200), 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;
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);

https://api.flutter.dev/flutter/dart-ui/Canvas/drawCircle.html
canvas.drawCircle(const Offset(100, 100), 100, 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;
canvas.drawCircle(const Offset(100, 100), 100, paint);
}
@override
bool shouldRepaint(covariant _MyPainter old) => false;
}
drawLine()
void drawLine(Offset p1, Offset p2, Paint paint);

https://api.flutter.dev/flutter/dart-ui/Canvas/drawLine.html
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
-
save や saveLayer() で 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)する際に使用される -
blendMode、colorFilter、imageFilter
-
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)する近似モデルを指す。
アンチエイリアスによってガタガタした角丸ではなく、綺麗な角丸を表現することが可能になる。

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% 含まれる
α が 1 より小さい pixel では、pixel の色が合成(blend)される。
pixel の色の合成方法は Paint.blendMode によって制御できる。
例えば、デフォルトの 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();
}
何を描画しているかが最初わからなかったので、白を消してみた。
@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();
}
次に NG とされている例。
推奨例と比較すると、赤い縁が見える。
| 推奨例 | 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() を実行していないが、アンチエイリアスが効いている。
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
ベジェ曲線とは 「制御点」が直線を引っ張ることで形が決まる曲線 を指す。
制御点は「速度」と「方向」を制御する。


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)

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)




















