Flutter recognize touch events inside Donut chart

1,099

enter image description here

Here is my solution, it's not perfect but it’s working :)

import 'dart:math';

import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:symex_management/core/models/product_performance.dart';

class DonutChart extends StatefulWidget {
  final List<ProductPerformance> productData;

  DonutChart(this.productData);
  @override
  _DonutChartState createState() => new _DonutChartState();
}

class _DonutChartState extends State<DonutChart> {
  TapDownDetails tapDetails;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              padding: EdgeInsets.all(20),
              height: 250,
              width: 250,
              child: GestureDetector(
                onTapDown: (tapDetails) {
                  setState(() {
                    this.tapDetails = tapDetails;
                  });
                },
                child: CustomPaint(
                  painter: DonutChartPainter(widget.productData,
                      tapDetails: tapDetails),
                ),
              ),
            )
          ],
        ),
        getLegends(widget.productData)
      ],
    );
  }

  Widget getLegends(List<ProductPerformance> items) {
    return new Wrap(
        spacing: 5,
        runSpacing: 5,
        alignment: WrapAlignment.spaceBetween,
        children: items.map((item) {
          return Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Container(
                width: 10,
                height: 10,
                decoration: BoxDecoration(
                  gradient: LinearGradient(colors: item.gradient),
                ),
              ),
              SizedBox(
                width: 5,
              ),
              Text(
                item.productName,
                style: TextStyle(color: Colors.white, fontSize: 12),
              )
            ],
          );
        }).toList());
  }
}

const double radians2Degrees = 180.0 / math.pi;
double degrees(double radians) => radians * radians2Degrees;

const double degrees2Radians = math.pi / 180.0;
double radian(double degrees) => degrees * degrees2Radians;

class DonutChartPainter extends CustomPainter {
  TapDownDetails tapDetails;
  List<ProductPerformance> productData;
  List<Arc> arcData;

  DonutChartPainter(this.productData, {this.tapDetails});

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint();
    var center = Offset(
      size.width / 2,
      size.height / 2,
    );

    var centerRadius = size.width * 1 / 5;
    var radius = centerRadius + (((size.width / 2) - centerRadius) / 2);

    var arcRadius = (size.width / 2) - centerRadius;

    prepareArcData(productData, tapDetails, center, centerRadius, radius);

    paint.style = PaintingStyle.stroke;
    paint.strokeWidth = arcRadius;

    for (var arc in arcData.where((item) => !item.hasShadow)) {
      paint.shader = arc.gradient
          .createShader(Rect.fromCircle(center: center, radius: radius));

      canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
          arc.startRadian, arc.radianValue, false, paint);
    }

    for (var arc in arcData.where((item) => item.hasShadow)) {
      paint.shader = null;
      paint.maskFilter = MaskFilter.blur(BlurStyle.normal, 5 * 0.57735 + 0.5);
      paint.color = Colors.black38;

      canvas.drawArc(Rect.fromCircle(center: center, radius: radius + 1),
          arc.startRadian - 0.001, arc.radianValue + 0.01, false, paint);

      paint.maskFilter = null;
      paint.color = Colors.black;
      paint.shader = arc.gradient
          .createShader(Rect.fromCircle(center: center, radius: radius));
      canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
          arc.startRadian, arc.radianValue, false, paint);
    }
    paint.blendMode = BlendMode.srcOver;
    paint.shader = null;
    paint.style = PaintingStyle.fill;
    paint.color = Colors.white;
    canvas.drawCircle(center, centerRadius, paint);
    paint.color = Colors.white30;
    canvas.drawCircle(center, centerRadius + 10, paint);

    for (var arc in arcData.where((item) => item.hasShadow)) {
      paint.color = Colors.black;
      final textStyle = TextStyle(
        color: Colors.black87,
        fontWeight: FontWeight.w600,
        fontSize: 13,
      );
      final textSpan = TextSpan(
        text: arc.performanceValue + '\n' + arc.label,
        style: textStyle,
      );
      final textSpan1 = TextSpan(
        text: arc.label,
        style: textStyle,
      );
      final textPainter = TextPainter(
        text: textSpan,
        textAlign: TextAlign.center,
        textDirection: TextDirection.ltr,
      );
      textPainter.layout(
        minWidth: 0,
        maxWidth: size.width,
      );

      Offset offset = new Offset(center.dx - (textPainter.width / 2),
          center.dy - (textPainter.height / 2));

      textPainter.paint(canvas, offset);
    }
  }

  @override
  bool shouldRepaint(DonutChartPainter oldDelegate) => true;

  void prepareArcData(
      List<ProductPerformance> productData,
      TapDownDetails tapDetails,
      Offset center,
      double centerRadius,
      double arcRadius) {
    arcData = new List();

    List<int> performanceValues =
        productData.map((e) => e.performanceValue).toList();
    var total = 0.0;
    for (var d in performanceValues) {
      total += d;
    }

    var startRadian = 0.0;

    for (var data in productData) {
      bool hasShadow = false;

      var radianAngle = data.performanceValue * 2 * pi / total;

      if (tapDetails?.localPosition != null) {
        final touchedPoint = tapDetails.localPosition - center;

        final touchX = touchedPoint.dx;
        final touchY = touchedPoint.dy;

        final touchR = math.sqrt(math.pow(touchX, 2) + math.pow(touchY, 2));

        double touchAngle = degrees(math.atan2(touchY, touchX));
        touchAngle =
            touchAngle < 0 ? (180 - touchAngle.abs()) + 180 : touchAngle;
        var sta = degrees(startRadian);
        var end = degrees(startRadian + radianAngle);
        final isInRadius = touchR > 0 && touchR <= arcRadius + centerRadius;

        if ((touchAngle >= sta && end > touchAngle) && isInRadius) {
          hasShadow = true;
        }
      }

      var arc = Arc(
          startRadian,
          radianAngle,
          SweepGradient(
            center: FractionalOffset.center,
            startAngle: startRadian,
            endAngle: startRadian + radianAngle,
            colors: data.gradient,
            stops: [
              0.0,
              0.5,
            ],
          ),
          data.performanceValue.toString(),
          data.productNameWithCountry,
          hasShadow: hasShadow);
      arcData.add(arc);
      startRadian += arc.radianValue;
    }

    if (tapDetails == null) {
      arcData[0].hasShadow = true;
    }
  }
}

class Arc {
  double radians2Degrees = 180.0 / math.pi;

  var hasShadow;
  double toDegrees(double radians) => radians * radians2Degrees;

  double degrees2Radians = math.pi / 180.0;
  double toRadian(double degrees) => degrees * degrees2Radians;

  final double startRadian;
  final double radianValue;
  final String performanceValue;
  final String label;

  final Gradient gradient;

  Arc(this.startRadian, this.radianValue, this.gradient, this.performanceValue,
      this.label,
      {this.hasShadow = false});

  get endRadian => startRadian + radianValue;
}
Share:
1,099
Favas Kv
Author by

Favas Kv

Working as a Mobile Application Developer @ Cinque Technologies, Ernamkulam.

Updated on November 21, 2022

Comments

  • Favas Kv
    Favas Kv over 1 year

    enter image description here

    I have Custom Painter that draws a donut chart like the above picture.

    I am using

    canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startRadian, radians[i], true, paint);

    for drawing this.

    Is there any possible way to detect a touch event inside any of these Arcs?

    If its a Rect I can use rect.contains(offset) , but here it's all Arcs.

    Note: I have access to Touch coordinates by wrapping custom painter inside GestureDetector, all I want is to distinguish the touch inside any of this Arc and highlight them accordingly.

    • key
      key almost 5 years
      have to tried to wrap them with gesturedetector or Inkwell?
    • Favas Kv
      Favas Kv almost 5 years
      @key Yes. I am using gesturedetector, and I have access to touch coordinates all I want to know whether it is inside of Arc or not.