flutter: 角丸ボーダー付きのボックスの右上にチェックマークを入れる

f:id:ta_watanabe:20211107170140p:plain

Container + BoxDecorationだと実現できなかったのでpaintしました。

本体

import 'package:flutter/material.dart';

class CheckableBox extends CustomPainter {
  CheckableBox({required this.isChecked});
  final bool isChecked;

  @override
  void paint(Canvas canvas, Size size) {
    double w = size.width;
    double h = size.height;
    double r = 15; //<-- corner radius

    // ボーダー部分
    Paint borderPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3;

    // 内側は白く塗る
    Paint innerPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    // 右上の三角でチェックが入ってる部分を塗る
    Paint topRightArcPaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill
      ..strokeWidth = 5;

    RRect fullRect = RRect.fromRectAndRadius(
      Rect.fromCenter(center: Offset(w / 2, h / 2), width: w, height: h),
      Radius.circular(r),
    );

    Path topRightArc = Path()
      // 右上が開始なので、まず左に移動
      ..moveTo(w - 6 * r, 0)
      // arc始まりの部分まで線を引く
      ..relativeLineTo(5 * r, 0)
      // 右上のarcの部分を描くというかなぞる
      ..arcToPoint(Offset(w, r), radius: Radius.circular(r))
      // arcの終わりから下に線を引く
      ..relativeLineTo(0, 3 * r)
      // 最後は斜辺を描く
      ..relativeLineTo(-6 * r, -4 * r);

    canvas.drawRRect(fullRect, innerPaint);
    canvas.drawRRect(fullRect, borderPaint);

    if (isChecked) {
      canvas.drawPath(topRightArc, topRightArcPaint);

      final icon = Icons.check;
      TextPainter textPainter = TextPainter(textDirection: TextDirection.rtl);
      textPainter.text = TextSpan(
          text: String.fromCharCode(icon.codePoint),
          style: TextStyle(
              fontSize: 30.0,
              fontFamily: icon.fontFamily,
              color: Colors.white));
      textPainter.layout();
      textPainter.paint(canvas, Offset(w - 2.5 * r, 0));
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

実行する側のコード

import 'package:flutter/material.dart';

import 'checkable_box.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.light(),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(body: SafeArea(child: MyScreen())),
    );
  }
}

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

  static final checks = [true, false];

  @override
  Widget build(BuildContext context) {
    return Center(
        child: ListView.builder(
            itemCount: 2,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.all(8.0),
                child: MyWidget(
                  isChecked: checks.elementAt(index),
                ),
              );
            }));
  }
}

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key, required this.isChecked}) : super(key: key);

  final bool isChecked;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: MediaQuery.of(context).size.width - 32,
      height: 200,
      child: LayoutBuilder(builder: (context, constraints) {
        return CustomPaint(
            painter: CheckableBox(isChecked: isChecked),
            child: const Padding(
              padding: EdgeInsets.only(left: 8.0),
              child: Center(child: Text('hello')),
            ));
      }),
    );
  }
}

https://github.com/na8esin/flutter2_practice/tree/main/lib/src/custom_paint