Componente autodibujado de Flutter: diagrama de abanico

Introducción

En el proceso de desarrollo, generalmente nos encontramos con algunas IU irregulares, como líneas irregulares, polígonos, gráficos estadísticos, etc., que no se pueden realizar combinando esos componentes comunes, lo que requiere que los dibujemos nosotros mismos. Se pueden dibujar varias formas manualmente usando CuntomPaintcomponentes combinados con pinceles .CustomPainter

Introducción a CustomPaint

CustomPaintEs una herencia SingleChildRenderObjectWidget, Widgetaquí introduce principalmente varios parámetros importantes:
child: CustomPaintsubcomponentes. painter: Pincel, los gráficos dibujados se mostrarán en la childparte posterior.
foregroundPainter: Pincel de primer plano, los gráficos dibujados se mostrarán al childfrente.
size: Tamaño del área de dibujo.

Introducción a CustomPainter

CustomPainterCustomPainterEs una clase abstracta que hereda de , anula painty métodos al personalizar una clase , y shouldRepaintel dibujo específico está principalmente en el paintmétodo.

Introducción a la pintura

Hay dos parámetros principales: Canvas: Canvas, que se puede utilizar para dibujar varios gráficos. Size: El tamaño del área de dibujo.

void paint(Canvas canvas, Size size)
复制代码

Descripción de shouldRepaint

Se llamará a este método antes de que se vuelva a dibujar el widget para determinar cuándo se debe volver a dibujar. El shouldRepaintretorno tureindica que es necesario volver a dibujar y el retorno indica que falseno es necesario volver a dibujar.

bool shouldRepaint(CustomPainter oldDelegate)
复制代码

Ejemplo

Aquí demostramos el proceso general de dibujo dibujando un gráfico circular.

pie_chart_view.gif

Uso de pintura personalizada

Primero, use CustomPaint, dibuje el tamaño máximo del componente principal y pase custom painter.

@override
Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.infinite,
      painter: PieChartPainter(),
    );
}
复制代码

Personalizar pintor

PieChartPainterherencia personalizadaCustomPainter

class PieChartPainters extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate != this;
  }
}
复制代码

dibujar

Luego implementamos el paintmétodo para dibujar

@override
void paint(Canvas canvas, Size size) {
    //移动到中心点
    canvas.translate(size.width / 2, size.height / 2);
    //绘制饼状图
    _drawPie(canvas, size);
    //绘制扇形分割线
    _drawSpaceLine(canvas);
    //绘制中心圆
    _drawHole(canvas, size);
}
复制代码
dibujar un gráfico circular

Tomamos el punto medio de todo el lienzo como el punto y luego calculamos el área angular de cada sector canvas.drawArcdibujando el sector.

pie_chart_view1.png

void _drawPie(Canvas canvas, Size size) {
    var startAngle = 0.0;
    var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
    for (var model in models) {
      Paint paint = Paint()
        ..style = PaintingStyle.fill
        ..color = model.color;
      var sweepAngle = model.value / sumValue * 360;
      canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero),
          startAngle * pi / 180, sweepAngle * pi / 180, true, paint);

      //为每一个区域绘制延长线和文字
      _drawLineAndText(
          canvas, size, model.radius, startAngle, sweepAngle, model);

      startAngle += sweepAngle;
    }
}
复制代码
绘制延长线以及文本

延长线的起点为扇形区域边缘中点位置,长度为一个固定的长度,转折点坐标通过半径加这个固定长度和三角函数进行计算,然后通过转折点的位置决定横线终点的方向,而横线的长度则根据文字的宽度决定,然后通过canvas.drawLine进行绘制直线。
文本绘制使用TextPainter.paint进行绘制,paint方法里面最终是通过canvas.drawParagraph进行绘制的。
最后再在文字的前面通过canvas.drawCircle绘制一个小圆点。

pie_chart_view2.png

 void _drawLineAndText(Canvas canvas, Size size, double radius,
      double startAngle, double sweepAngle, PieChartModel model) {
    var ratio = (sweepAngle / 360.0 * 100).toStringAsFixed(2);

    var top = Text(model.name);
    var topTextPainter = getTextPainter(top);

    var bottom = Text("$ratio%");
    var bottomTextPainter = getTextPainter(bottom);

    // 绘制横线
    // 计算开始坐标以及转折点的坐标
    var startX = radius * (cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
    var startY = radius * (sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

    var firstLine = radius / 5;
    var secondLine =
        max(bottomTextPainter.width, topTextPainter.width) + radius / 4;
    var pointX = (radius + firstLine) *
        (cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
    var pointY = (radius + firstLine) *
        (sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

    // 计算坐标在左边还是在右边
    // 并计算横线结束坐标
    // 如果结束坐标超过了绘制区域,则改变结束坐标的值
    var marginOffset = 20.0; // 距离绘制边界的偏移量
    var endX = 0.0;
    if (pointX - startX > 0) {
      endX = min(pointX + secondLine, size.width / 2 - marginOffset);
      secondLine = endX - pointX;
    } else {
      endX = max(pointX - secondLine, -size.width / 2 + marginOffset);
      secondLine = pointX - endX;
    }

    Paint paint = Paint()
      ..style = PaintingStyle.fill
      ..strokeWidth = 1
      ..color = Colors.grey;

    // 绘制延长线
    canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint);
    canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint);

    // 文字距离中间横线上下间距偏移量
    var offset = 4;
    var textWidth = bottomTextPainter.width;
    var textStartX = 0.0;
    textStartX =
        _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
    bottomTextPainter.paint(canvas, Offset(textStartX, pointY + offset));

    textWidth = topTextPainter.width;
    var textHeight = topTextPainter.height;
    textStartX =
        _calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
    topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight));

    // 绘制文字前面的小圆点
    paint.color = model.color;
    canvas.drawCircle(
        Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2),
        4,
        paint);
}
复制代码
绘制扇形分割线

在绘制完扇形之后,然后在扇形的开始的那条边上绘制一条直线,起点为圆点,长度为扇形半径,终点的位置根据半径和扇形开始的那条边的角度用三角函数进行计算,然后通过canvas.drawLine进行绘制。

pie_chart_view3.png

void _drawSpaceLine(Canvas canvas) {
    var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
    var startAngle = 0.0;
    for (var model in models) {
      _drawLine(canvas, startAngle, model.radius);
      startAngle += model.value / sumValue * 360;
    }
}

void _drawLine(Canvas canvas, double angle, double radius) {
    var endX = cos(angle * pi / 180) * radius;
    var endY = sin(angle * pi / 180) * radius;
    Paint paint = Paint()
      ..style = PaintingStyle.fill
      ..color = Colors.white
      ..strokeWidth = spaceWidth;
    canvas.drawLine(Offset.zero, Offset(endX, endY), paint);
  }
复制代码
绘制内部中心圆

这里可以通过传入的参数判断是否需要绘制这个圆,使用canvas.drawCircle进行绘制一个与背景色一致的圆。

pie_chart_view4.png

void _drawHole(Canvas canvas, Size size) {
    if (isShowHole) {
      holePath.reset();
      Paint paint = Paint()
        ..style = PaintingStyle.fill
        ..color = Colors.white;
      canvas.drawCircle(Offset.zero, holeRadius, paint);
    }
}
复制代码

触摸事件处理

接下来我们来处理点击事件,当我们点击某一个扇形区域时,此扇形需要突出显示,如下图:

pie_chart_view5.png

重写hitTest方法
注意
这个方法的返回值决定是否响应事件。
默认情况下返回null,事件不会向下传递,也不会进行处理; 如果返回true则当前组件进行处理事件; 如果返回false则当前组件不会响应点击事件,会向下一层传递;

我直接在这里处理点击事件,通过该方法传入的offset确定点击的位置,如果点击位置是在圆形区域内并且不在中心圆内则处理事件同时判断所点击的具体是哪个扇形,反之则恢复默认状态。

@override
bool? hitTest(Offset offset) {
    if (oldTapOffset.dx==offset.dx && oldTapOffset.dy==offset.dy) {
      return false;
    }
    oldTapOffset = offset;
    for (int i = 0; i < paths.length; i++) {
      if (paths[i].contains(offset) &&
          !holePath.contains(offset)) {
        onTap?.call(i);
        oldTapOffset = offset;
        return true;
      }
    }
    onTap?.call(-1);
    return false;
}
复制代码

至此,我们通过onTap向上传递出点击的是第几个扇形,然后进行处理,更新UI就可以了。

动画实现

pie_chart_view.gif

这里通过Widget继承ImplicitlyAnimatedWidget来实现,ImplicitlyAnimatedWidget是一个抽象类,继承自StatefulWidget,既然是StatefulWidget那肯定还有一个StateState继承AnimatedWidgetBaseState(此类继承自ImplicitlyAnimatedWidgetState),感兴趣的小伙伴可以直接去看源码

实现AnimatedWidgetBaseState里面的forEachTween方法,主要是用于来更新Tween的初始值。

@override
void forEachTween(TweenVisitor<dynamic>visitor) {
   customPieTween = visitor(customPieTween, end, (dynamic value) {
      return CustomPieTween(begin: value, end: end);
    }) as CustomPieTween;
}
复制代码

自定义CustomPieTween继承自Tween,重写lerp方法,对需要做动画的参数进行处理

class CustomPieTween extends Tween<List<PieChartModel>> {
  CustomPieTween({List<PieChartModel>? begin, List<PieChartModel>? end})
      : super(begin: begin, end: end);

  @override
  List<PieChartModel> lerp(double t) {
    List<PieChartModel> list = [];
    begin?.asMap().forEach((index, model) {
      list.add(model
        ..radius = lerpDouble(model.radius, end?[index].radius ?? 100.0, t));
    });
    return list;
  }

  double lerpDouble(double radius, double radius2, double t) {
    if (radius == radius2) {
      return radius;
    }
    var d = (radius2 - radius) * t;
    var value = radius + d;
    return value;
  }
}
复制代码

完整代码

感兴趣的小伙伴可以直接看源码 GitHub:chart_view

Supongo que te gusta

Origin juejin.im/post/7098140878945927175
Recomendado
Clasificación