😗

【Flutter】ClipPathを使って楕円のContainerを作成した

2023/08/04に公開

はじめに

dribbbleで見ていいなと思ったUIをFlutterでClipPathを使って実現しました。

環境

flutter --version
Flutter 3.10.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f92f44110e (9 weeks ago) • 2023-06-01 18:17:33 -0500
Engine • revision 2a3401c9bb
Tools • Dart 3.0.3 • DevTools 2.23.1

ContainerをClipPathを使って上部が楕円になるようにくり抜く

ClipPathはWidgetを自由にくり抜くWidgetです。
https://api.flutter.dev/flutter/widgets/ClipPath-class.html

CustomClipper<Path>を継承した別クラスで描画の設定をし、clipperで呼び出すと親Widgetをくり抜いてくれます。

描画の設定

CustomClipper<Path>を継承したMyClipperを用意しました。

class MyClipper extends CustomClipper<Path> {
  
  Path getClip(Size size) {
    double w = size.width; //親Widgetのwidth
    double h = size.height; //親Widetのheight

    double curveHeight = 20;

    final path = Path();
    path.moveTo(0, curveHeight);
    path.lineTo(0, h);
    path.lineTo(w, h);
    path.lineTo(w, curveHeight);
    path.quadraticBezierTo(w / 2, -curveHeight, 0, curveHeight);

    path.close();
    return path;
  }

  
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}

getClipの引数である、sizeは親Widgetのサイズになります。

初期設定では、左上が描画の原点です。
原点はmoveToで移動できます。

以降は順番にlineToで描画する点を指定していきます。

カーブの描画はquadraticBezierToでします。

最後にcloseで描画を終了します。

ClipPathにclipperを追加

ClipPathに、先ほど作成したMyClipperを指定する。これで楕円のContainerの完成です。

 //省略
  ClipPath(
      clipper: MyClipper(), //指定
      child: Container(
        width: width,
        height: 120,
        color: Colors.blue,
    ),
 //省略

利用例

私はこれを下付きのボタンにしたかったので、以下のようなWidgetを作成しました。

round_container_button.dart
import 'package:flutter/material.dart';

import '../round_container.dart';

class RoundContainerButton extends StatelessWidget {
  const RoundContainerButton({
    required this.onTap,
    super.key,
  });

  final Function onTap;

  
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        onTap;
      },
      child: const RoundContainer(
        title: "円形のボタン",
      ),
    );
  }
}

コード全体

view_screen.dart
import 'package:flutter/material.dart';

import 'ui/widget/button/round_container_button.dart';

class ViewScreen extends StatelessWidget {
  const ViewScreen({Key? key}) : super(key: key);

  static Route<void> route() {
    return MaterialPageRoute<dynamic>(
      builder: (_) => const ViewScreen(),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          RoundContainerButton(
            onTap: () {
              print("onTap!!");
            },
          ),
        ],
      ),
    );
  }
}
round_container.dart
import 'package:flutter/material.dart';


class RoundContainer extends StatelessWidget {
  const RoundContainer({
    super.key,
    required this.title,
  });

  final String title;

  
  Widget build(BuildContext context) {
    final screen = MediaQuery.of(context).size;

    final double width = screen.width;

    return ClipPath(
      clipper: MyClipper(),
      child: Container(
        width: width,
        height: 120,
        color: Colors.blue,
        child: Center(
            child: Text(
          title,
          style: const TextStyle(color: Colors.white),
        )),
      ),
    );
  }
}

class MyClipper extends CustomClipper<Path> {
  
  Path getClip(Size size) {
    double w = size.width;
    double h = size.height;

    double curveHeight = 20;

    final path = Path();
    path.moveTo(0, curveHeight);
    path.lineTo(0, h);
    path.lineTo(w, h);
    path.lineTo(w, curveHeight);
    path.quadraticBezierTo(w / 2, -curveHeight, 0, curveHeight);

    path.close();
    return path;
  }

  
  bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
round_container_button.dart
import 'package:flutter/material.dart';

import '../round_container.dart';

class RoundContainerButton extends StatelessWidget {
  const RoundContainerButton({
    required this.onTap,
    super.key,
  });

  final Function onTap;

  
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        onTap;
      },
      child: const RoundContainer(
        title: "円形のボタン",
      ),
    );
  }
}

参考にしたもの

以下の動画をもとに今回のWidgetを作成しました。

https://www.youtube.com/watch?v=xuatM4pZkNk

この動画で紹介されていた、https://shapemaker.web.app/#/ を利用すれば簡単に理想の描画ができそうです。(Flutter Webでお馴染みの#が入ってますね)

最後に

ClipPathを使用すればかなり自由度が高いWidgetを作成できそうだと感じました。

次は今回のWidgetを応用したものの作成や、描画に処理にどれぐらい負荷がかかるか検証したいと思います。

ここまでご覧いただきありがとうございました。

Discussion