🐍

【Flutter】CustomPainterを使って曲線形状のWidgetを作成し、変形させる

2024/02/08に公開

今回やること

実行例
このような BottomNavigationBar を作ったのですが、Icon を上下に動かすのは AnimatedBuilder などを使って Transform や Positioned で座標を動かしてあげれば実装できます。
ポイントとなるのがにゅっと形状が変形する部分です。
今回は形状をアニメーションを使って変形させる部分を解説します。

CustomPainter を使って曲線形状を作成する

実行例
CustomPainter には様々な形状を作成するメソッドが用意されていますが、今回は cubicTo()を使って上記のようなベジェ曲線を作成します。

cubicTo()では 3 点を指定する事で曲線が作成されます。上記の図では p1~p3、p4~p6 の 2 つの曲線を作成しています。

各点の座標(1マス10)
// yは上がマイナス、下がプラスの値
const Offset p1 = Offset(20, 0);
const Offset p2 = Offset(20, -30);
const Offset p3 = Offset(40, -30);
const Offset p4 = Offset(60, -30);
const Offset p5 = Offset(60, 0);
const Offset p6 = Offset(80, 0);
各点を元にpathを生成
final path = Path();
path.cubicTo(p1.dx,p1.dy,p2.dx,p2.dy,p3.dx,p3.dy,);
path.cubicTo(p4.dx,p4.dy,p5.dx,p5.dy,p6.dx,p6.dy,);

この path を描画するために paint()で描画スタイルを指定する。
今回は、色が青、太さ 2.0 の線とします。

最後に drawPath()でキャンバスに path を描画する。

paint定義
final paint = Paint()
    ..color = Colors.blue
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2.0;
キャンバスへ描画
canvas.drawPath(path, paint);

描画されたものがこちらです。
実行例

https://api.flutter.dev/flutter/rendering/CustomPainter-class.html

形状を変形させる

なんとなく分かると思いますが、上で指定した座標の y を全て 0 とすると直線になります。
つまり、この y の値を animation で変化させてやればそうアレになる訳です。

動的な座標
// animateValue(0〜1)は親から受け取る
const double peakHeight = 30; // 変形する高さの最大値
final double animatedHeight = peakHeight * animateValue; // 変形する高さ

const Offset p1 = Offset(20, 0);
Offset p2 = Offset(20, -animatedHeight);
Offset p3 = Offset(40, -animatedHeight);
Offset p4 = Offset(60, -animatedHeight);
const Offset p5 = Offset(60, 0);
const Offset p6 = Offset(80, 0);

実際の動きがこちら。にゅっとなっています。
左側は塗りつぶしバージョン
実行例

塗りつぶし
final paint = Paint()
    ..color = Colors.blue
    ..style = PaintingStyle.fill // 塗りつぶし
    ..strokeWidth = 2.0;
CustomPainter全文
class MyCustomPainter extends CustomPainter {
  MyCustomPainter({required this.animateValue, required this.style});

  final double animateValue; // アニメーションの値
  final PaintingStyle style; // PaintingStyle.fill or PaintingStyle.stroke

  
  void paint(Canvas canvas, Size size) {
    const double peakHeight = 30; // 変形する高さの最大値
    final double animatedHeight = peakHeight * animateValue; // 変形する高さ

    const Offset p1 = Offset(20, 0);
    Offset p2 = Offset(20, -animatedHeight);
    Offset p3 = Offset(40, -animatedHeight);
    Offset p4 = Offset(60, -animatedHeight);
    const Offset p5 = Offset(60, 0);
    const Offset p6 = Offset(80, 0);

    final paint = Paint()
      ..color = Colors.blue
      ..style = style
      ..strokeWidth = 2.0;// ペイントのスタイルを設定

    final path = Path();
    path.cubicTo(p1.dx,p1.dy,p2.dx,p2.dy,p3.dx,p3.dy,);
    path.cubicTo(p4.dx,p4.dy,p5.dx,p5.dy,p6.dx,p6.dy,);

    canvas.drawPath(path, paint);// キャンバスに描画
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

終わりに

このように path を指定して独自形状の Widget を作成できます。
また、path や座標を動的に扱うことでさらにカスタマイズで来ます。
正直なところ、複雑な path を実装したい場合は figma などの GUI でポチポチやりたいですが...

最初に紹介した CustomBottomNavigationBar のにコードを参考に貼っておきます。
https://github.com/shogoisaji/custom_bottom_navbar

参考記事

https://qiita.com/ling350181/items/745c0c6c04c4037298de

Discussion