Flutter BoxPainterでpie_chartを描く(中心に文字が入れられる)

f:id:ta_watanabe:20211022060304p:plain

こんな感じのpie_chartを作りました。

chartの中心に文字が入れられる既存のパッケージが見つからなかったので、 flutter galleryのソースを見ながら作りました。

drawArcで大小二つのArcをかいて、内側のArcは白く塗りつぶしてます。

まずはチャートと中の文字を生成している部分

import 'dart:math' as math;

import 'package:flutter/material.dart';

// 参考ソース
// https://github.com/flutter/gallery/blob/master/lib/studies/rally/charts/pie_chart.dart

class MyChart extends StatelessWidget {
  const MyChart(
      {Key? key,
      required this.total,
      required this.rest,
      required this.outerRPerParentHeightHalf,
      this.percentageFontSize})
      : super(key: key);

  final int total;
  final int rest;
  final double outerRPerParentHeightHalf; // 外側の半径 / 親の高さの半分
  final double? percentageFontSize;

  int get percentage => (total == 0 ? 0 : (rest / total * 100).toInt());

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      return ConstrainedBox(
        constraints: constraints,
        child: DecoratedBox(
          decoration: MyDecoration(
              percentage: percentage,
              outerRPerParentHeightHalf: outerRPerParentHeightHalf),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                '$percentage%',
                style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: percentageFontSize,
                    color: Colors.blue),
              ),
              Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child: Text(
                  '$rest/$total',
                  style: TextStyle(
                      fontWeight: FontWeight.bold, color: Colors.blue),
                ),
              ),
            ],
          ),
        ),
      );
    });
  }
}

class MyDecoration extends Decoration {
  const MyDecoration(
      {required this.percentage, required this.outerRPerParentHeightHalf});

  final int percentage;
  final double outerRPerParentHeightHalf;

  @override
  BoxPainter createBoxPainter([VoidCallback? onChanged]) {
    return MyBoxPainter(
        amount: percentage,
        outerRPerParentHeightHalf: outerRPerParentHeightHalf);
  }
}

class MyBoxPainter extends BoxPainter {
  MyBoxPainter({required this.amount, required this.outerRPerParentHeightHalf});
  final double outerRPerParentHeightHalf;
  late double outerR;

  final int amount;

  double get innerR => outerR * 0.75;
  double get centerR => (outerR - innerR) / 2 + innerR;
  double get circleR => (outerR - innerR) / 2;

  // 100%=360度 としたときの角度にまずは直す
  double get endAngle => amount * 360 / 100;
  double get endRadian => -endAngle * math.pi / 180;

  // configuration.sizeは謎のpaddingを引いた分
  @override
  void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    outerR = configuration.size!.height / 2 * outerRPerParentHeightHalf;

    // まずはドーナッツのうす水色を作る
    drawRest(canvas, offset, configuration);
    // 有効なところは青く
    drawAnswer(canvas, offset, configuration);

    // 端を滑らかにするための円
    drawStartCircle(canvas, offset, configuration);
    drawEndCircle(canvas, offset, configuration);
  }

  drawRest(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final paint = Paint()..color = Colors.blue[50]!;
    canvas.drawArc(
        Rect.fromCircle(
            center: configuration.size!.center(offset), radius: outerR),
        0.0,
        360 * math.pi / 180,
        true,
        paint);

    final paint2 = Paint()..color = Colors.white;
    canvas.drawArc(
        Rect.fromCircle(
            center: configuration.size!.center(offset), radius: innerR),
        0.0,
        360 * math.pi / 180,
        true,
        paint2);
  }

  drawAnswer(Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final paint = Paint()..color = Colors.blue;
    final startRad = -90 * math.pi / 180;

    canvas.drawArc(
        Rect.fromCircle(
            center: configuration.size!.center(offset), radius: outerR),
        startRad,
        endRadian,
        true,
        paint);

    final paint2 = Paint()..color = Colors.white;
    canvas.drawArc(
        Rect.fromCircle(
            center: configuration.size!.center(offset), radius: innerR),
        startRad,
        endRadian,
        true,
        paint2);
  }

  drawStartCircle(
      Canvas canvas, Offset offset, ImageConfiguration configuration) {
    final centerOffset = configuration.size!.center(offset);
    final centerOffsetDy = centerOffset.dy;
    final circleOffsetDy = centerOffsetDy - centerR;
    final paint = Paint()..color = Colors.blue;
    canvas.drawCircle(Offset(centerOffset.dx, circleOffsetDy), circleR, paint);
  }

  drawEndCircle(
      Canvas canvas, Offset offset, ImageConfiguration configuration) {
    // drawArcと座標系が違う
    final radian = (270 - endAngle) * math.pi / 180;

    final x = centerR * math.cos(radian);
    final y = centerR * math.sin(radian);

    final centerOffset = configuration.size!.center(offset);
    final paint = Paint()..color = Colors.blue;
    canvas.drawCircle(
        // xはプラスで右、yはマイナスで上
        Offset(centerOffset.dx + x, centerOffset.dy + y),
        circleR,
        paint);
  }
}

デバッグするためのコード

import 'package:flutter/material.dart';

import 'my_chart.dart';

void main() {
  runApp(MaterialApp(
    home: MySca(),
  ));
}

class MySca extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(appBar: AppBar(), body: MyGridView()),
    );
  }
}

class MyGridView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 2,
      children: [MyBox(), MyBox()],
    );
  }
}

class MyBox extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
          color: Colors.white,
          border: Border.all(
            color: Colors.blueAccent,
            width: 2,
          ),
          borderRadius: BorderRadius.circular(15)),
      child: MyChart(total: 300, rest: 240, outerRPerParentHeightHalf: 0.5),
    );
  }
}

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