Customizing cobweb maps on Android took half a day. It took less than 20 minutes to recreate the Flutter

I have to say that the Canvas in Flutter is very friendly to Android players and feels that Flutter is more and more fun. Flutter is definitely better than native in view. In this article you will learn:

2. How to draw text in Flutter 3. Practical application of animation in drawing 4. Related methods of Canvas drawing 5. Encapsulation of a component in FlutterCopy the code

----> ------------- var show = AbilityWidget(ability: ability (duration: 1500, image: AssetImage("images/lifei.jpeg"), RADIUS: 100, color: colors.black, data: {" Language ": 40.0, "math ": 30.0," English ": 20.0, "politics ": 40.0, "music" : 80.0, "creatures" : 50.0, the "chemical" : 60.0, "geography" : 80.0,}));Copy the code

1. Static cobweb diagram

The first step is how to map a string of data to the following diagram:

Var data = {" damage ", 70.0, "life" : 90.0, "evasion" : 50.0, "critical strike" : 70.0, "abnormality" : 80.0, "block" : 100.0,};Copy the code


1.1: Create AbilityWidget components

The component of a new StatelessWidget is drawn using AbilityPainter

Member variables such as brushes and paths are defined

import 'package:flutter/material.dart'; class AbilityWidget extends StatefulWidget { @override _AbilityWidgetState createState() => _AbilityWidgetState(); } class _AbilityWidgetState extends State<AbilityWidget>{ @override Widget build(BuildContext context) { var paint = CustomPaint( painter: AbilityPainter(), ); return SizedBox(width: 200, height: 200, child: paint,); }} Class AbilityPainter extends CustomPainter {var data = {" Damage ": 70.0, "life ": 90.0," dodge ": 50.0, "Crit ": 70.0," break ": 80.0, "block ": 100.0,}; double mRadius = 100; // Paint mLinePaint; // Paint mAbilityPaint; // area Paint mFillPaint; // Fill the brush Path mLinePath; // Short linear Path Path mAbilityPath; // Scope Path AbilityPainter() {mLinePath = Path(); mAbilityPath = Path(); mLinePaint = Paint() .. color = Colors.black .. style = PaintingStyle.stroke .. StrokeWidth = 0.008 * mRadius.. isAntiAlias = true; MFillPaint = Paint() // Fill the brush.. StrokeWidth = 0.05 * mRadius.. color = Colors.black .. isAntiAlias = true; mAbilityPaint = Paint() .. color = Color(0x8897C5FE) .. isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; }}Copy the code

1.2. Draw the outer ring

In order to reduce the variable value and make the size have good correlation (equal scaling), the length and width of the small black bar will depend on the maximum radius mRadius

The length of the small black bar is mRadius*0.08. The width of the small black bar is mRadius*0.05. Therefore, R2 = mradius-mradius *0.08

@override void paint(Canvas canvas, Size size) { canvas.translate(mRadius, mRadius); DrawOutCircle (canvas); } void drawOutCircle(Canvas Canvas) {canvas.save(); DrawCircle (Offset(0, 0), mRadius, mLinePaint); // Double r2 = mRadius - 0.08 * mRadius; DrawCircle (Offset(0, 0), r2, mLinePaint); For (var I = 0.0; i < 22; I++) {// loop over the little black bar canvas.save(); Rotate (I / 180 * PI); rotate(I / 180 * PI); DrawLine (Offset(0, -mradius), Offset(0, -r2), mFillPaint); drawLine(Offset(0, -mradius), Offset(0, -r2), mFillPaint); // Line's drawing canvas.restore(); // Release the layer} canvas.restore(); // Release layer}Copy the code

1.3. Draw the inner circle

The same size as the outer circle, drawing here is a bit complicated, you need to understand the use of canvas and path

If you don’t understand, you can go to Canvas and Path. If you read these two articles and ask what are the skills of drawing, you can go to here

@override void paint(Canvas canvas, Size size) { canvas.translate(mRadius, mRadius); DrawOutCircle (canvas); drawInnerCircle(canvas); } // Draw the inner circle drawInnerCircle(Canvas Canvas) {double innerRadius = 0.618 * mRadius; DrawCircle (Offset(0, 0), innerRadius, mLinePaint); canvas.save(); for (var i = 0; i < 6; I++) {// traverse 6 lines canvas.save(); canvas.rotate(60 * i.toDouble() / 180 * pi); // Each rotation is 60° mPath. MoveTo (0, -innerradius); mPath.relativeLineTo(0, innerRadius); // line path for (int j = 1; j < 6; J++) {mPath. MoveTo (-mradius * 0.02, innerRadius / 6 * j); MPath. RelativeLineTo (mRadius * 0.02 * 2, 0); DrawPath (mPath, mLinePaint); // Draw line canvas.restore(); } canvas.restore(); }Copy the code

1.3. Draw text

Drawing text in Flutter is a bit tricky. I simply enclose a drawText function here to drawText

Remember to import the UI library, use Paragraph to set the text, and drawParagraph to draw it

import 'dart:ui' as ui; Void drawInfoText(Canvas Canvas) {double r2 = mRadius -0.08 * mRadius; // for (int I = 0; i < data.length; i++) { canvas.save(); canvas.rotate(360 / data.length * i / 180 * pi + pi); DrawText (canvas, data.keys.toList()[I], Offset(-50, R2-0.22 * mRadius), fontSize: mRadius * 0.1); drawText(canvas, data.keys.toList()[I], Offset(-50, R2-0.22 * mRadius), fontSize: mRadius * 0.1); canvas.restore(); DrawText (Canvas Canvas, String text, Offset Offset, {Color Color = color. black, double maxWith = 100, double fontSize, String fontFamily, TextAlign textAlign=TextAlign.center, FontWeight FontWeight = fontweight. bold}) {// Draw the text var paragraphBuilder = ui. paragraphBuilder (ui.ParagraphStyle( fontFamily: fontFamily, textAlign: textAlign, fontSize: fontSize, fontWeight: fontWeight, ), ); paragraphBuilder.pushStyle( ui.TextStyle(color: color, textBaseline: ui.TextBaseline.alphabetic)); paragraphBuilder.addText(text); var paragraph = paragraphBuilder.build(); paragraph.layout(ui.ParagraphConstraints(width: maxWith)); canvas.drawParagraph(paragraph, Offset(offset.dx, offset.dy)); }Copy the code

1.4. Draw scope

And the last and most difficult piece, do you have the paper ready?

DrawAbility (Canvas Canvas, List<double> value) {double step = mRadius* 0.618/6; MAbilityPath. MoveTo (0, -value[0] / 20 * step); For (int I = 1; i < 6; i++) { double mark = value[i] / 20; MAbilityPath. LineTo (mark * step * cos(PI / 180 * (-30 + 60 * (i-1)))), mark * step * sin(pi / 180 * (-30 + 60 * (i - 1)))); } mAbilityPath.close(); canvas.drawPath(mAbilityPath, mAbilityPaint); }Copy the code

2. Animation effects

Make the outer circle rotate in the opposite direction to the inner circle, so the inner and outer circles can be separated into two components in a Stack

2.1: Pull away from the outer ring
class OutlinePainter extends CustomPainter { double mRadius = 100; // Paint mLinePaint; // line Paint mFillPaint; // Fill the brush OutlinePainter() {mLinePaint = Paint().. color = Colors.black .. style = PaintingStyle.stroke .. StrokeWidth = 0.008 * mRadius.. isAntiAlias = true; MFillPaint = Paint() // Fill the brush.. StrokeWidth = 0.05 * mRadius.. color = Colors.black .. isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { drawOutCircle(canvas); } @override bool shouldRepaint(CustomPainter oldDelegate) { // TODO: implement shouldRepaint return true; } void drawOutCircle(Canvas Canvas) {canvas.save(); DrawCircle (Offset(0, 0), mRadius, mLinePaint); // Double r2 = mRadius - 0.08 * mRadius; DrawCircle (Offset(0, 0), r2, mLinePaint); For (var I = 0.0; i < 22; I++) {// loop over the little black bar canvas.save(); Rotate (I / 180 * PI); rotate(I / 180 * PI); DrawLine (Offset(0, -mradius), Offset(0, -r2), mFillPaint); drawLine(Offset(0, -mradius), Offset(0, -r2), mFillPaint); // Line's drawing canvas.restore(); // Release the layer} canvas.restore(); // Release layer}}Copy the code

2.2: Use animation

Stack is used here to Stack components

The class _AbilityWidgetState extends the State < AbilityWidget > with SingleTickerProviderStateMixin {var _angle = 0.0; AnimationController controller; Animation<double> animation; @override void initState() { super.initState(); Duration: const duration (milliseconds: 2000), // milliseconds: vsync: this); Var tween = tween (begin: 0.0, end: 360.0); Animation = Tween.animate (controller); // Create Animatable objects that vary from 25 to 150. AddListener (() {setState(() {_angle = animation.value; }); }); controller.forward(); } @override Widget build(BuildContext context) { var paint = CustomPaint( painter: AbilityPainter(), ); var outlinePainter = Transform.rotate( angle: _angle / 180 * pi, child: CustomPaint( painter: OutlinePainter(), ), ); Var img = transform. rotate(Angle: _angle / 180 * PI, child: Opacity(Opacity: animation.value / 360 * 0.4, child: ClipOval(Child: image. asset("images/ name.jpg ", width: 200, height: 200, fit: boxfit.cover,),); var center = Transform.rotate( angle: -_angle / 180 * pi, child: Transform.scale( scale: Value / 360/2, child: SizedBox(width: 200, height: 200, child: paint,)); return Center( child: Stack( alignment: Alignment.center, children: <Widget>[img, center, outlinePainter], ), ); }}Copy the code

3. Component encapsulation

Now that the logic is clear, all that is left is to wrap the component and extract some quantity

Below is a simple encapsulation, there are a lot of messy unencapsulation, such as color, animation effects and so on.

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';

class Ability {
  double radius;
  int duration;
  ImageProvider image;
  Map<String,double> data;
  Color color;

  Ability({this.radius, this.duration, this.image, this.data, this.color});

}

class AbilityWidget extends StatefulWidget {
  AbilityWidget({Key key, this.ability}) : super(key: key);

  final Ability ability;

  @override
  _AbilityWidgetState createState() => _AbilityWidgetState();
}

class _AbilityWidgetState extends State<AbilityWidget>
    with SingleTickerProviderStateMixin {
  var _angle = 0.0;
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();


    controller = AnimationController(
        ////创建 Animation对象
        duration: Duration(milliseconds: widget.ability.duration), //时长
        vsync: this);
    
    var curveTween = CurveTween(curve:Cubic(0.96, 0.13, 0.1, 1.2));//创建curveTween
    var tween=Tween(begin: 0.0, end: 360.0);
    animation = tween.animate(curveTween.animate(controller));


    animation.addListener(() {
      setState(() {
        _angle = animation.value;
        print(_angle);
      });
    });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    var paint = CustomPaint(
      painter: AbilityPainter(widget.ability.radius,widget.ability.data),
    );

    var outlinePainter = Transform.rotate(
      angle: _angle / 180 * pi,
      child: CustomPaint(
        painter: OutlinePainter(widget.ability.radius ),
      ),
    );

    var img = Transform.rotate(
      angle: _angle / 180 * pi,
      child: Opacity(
        opacity: animation.value / 360 * 0.4,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(widget.ability.radius),
          child: Image(
            image: widget.ability.image,
            width: widget.ability.radius * 2,
            height: widget.ability.radius * 2,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );

    var center = Transform.rotate(
        angle: -_angle / 180 * pi,
        child: Transform.scale(
          scale: 0.5 + animation.value / 360 / 2,
          child: SizedBox(
            width: widget.ability.radius * 2,
            height: widget.ability.radius * 2,
            child: paint,
          ),
        ));

    return Center(
      child: Stack(
        alignment: Alignment.center,
        children: <Widget>[img, center, outlinePainter],
      ),
    );
  }
}

class OutlinePainter extends CustomPainter {
  double _radius; //外圆半径
  Paint mLinePaint; //线画笔
  Paint mFillPaint; //填充画笔

  OutlinePainter(this._radius) {
    mLinePaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 0.008 * _radius
      ..isAntiAlias = true;

    mFillPaint = Paint() //填充画笔
      ..strokeWidth = 0.05 * _radius
      ..color = Colors.black
      ..isAntiAlias = true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    drawOutCircle(canvas);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return true;
  }

  //绘制外圈
  void drawOutCircle(Canvas canvas) {
    canvas.save(); //新建图层
    canvas.drawCircle(Offset(0, 0), _radius, mLinePaint); //圆形的绘制
    double r2 = _radius - 0.08 * _radius; //下圆半径
    canvas.drawCircle(Offset(0, 0), r2, mLinePaint);
    for (var i = 0.0; i < 22; i++) {
      //循环画出小黑条
      canvas.save(); //新建图层
      canvas.rotate(360 / 22 * i / 180 * pi); //旋转:注意传入的是弧度(与Android不同)
      canvas.drawLine(Offset(0, -_radius), Offset(0, -r2), mFillPaint); //线的绘制
      canvas.restore(); //释放图层
    }
    canvas.restore(); //释放图层
  }
}

class AbilityPainter extends CustomPainter {

  Map<String, double>  _data;
  double _r; //外圆半径
  Paint mLinePaint; //线画笔
  Paint mAbilityPaint; //区域画笔
  Paint mFillPaint; //填充画笔

  Path mLinePath; //短直线路径
  Path mAbilityPath; //范围路径

  AbilityPainter(this._r, this._data) {
    mLinePath = Path();
    mAbilityPath = Path();
    mLinePaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 0.008 * _r
      ..isAntiAlias = true;

    mFillPaint = Paint() //填充画笔
      ..strokeWidth = 0.05 * _r
      ..color = Colors.black
      ..isAntiAlias = true;
    mAbilityPaint = Paint()
      ..color = Color(0x8897C5FE)
      ..isAntiAlias = true;
  }

  @override
  void paint(Canvas canvas, Size size) {
    //剪切画布
    Rect rect = Offset.zero & size;
    canvas.clipRect(rect);

    canvas.translate(_r, _r); //移动坐标系
    drawInnerCircle(canvas);
    drawInfoText(canvas);
    drawAbility(canvas, _data.values.toList());
  }

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

  //绘制内圈圆
  drawInnerCircle(Canvas canvas) {
    double innerRadius = 0.618 * _r; //内圆半径
    canvas.drawCircle(Offset(0, 0), innerRadius, mLinePaint);
    canvas.save();
    for (var i = 0; i < _data.length; i++) {
      //遍历6条线
      canvas.save();
      canvas.rotate(360/_data.length * i.toDouble() / 180 * pi); //每次旋转60°
      mLinePath.moveTo(0, -innerRadius);
      mLinePath.relativeLineTo(0, innerRadius); //线的路径
      for (int j = 1; j < _data.length; j++) {
        mLinePath.moveTo(-_r * 0.02, innerRadius / _data.length * j);
        mLinePath.relativeLineTo(_r * 0.02 * 2, 0);
      } //加5条小线
      canvas.drawPath(mLinePath, mLinePaint); //绘制线
      canvas.restore();
    }
    canvas.restore();
  }

  //绘制文字
  void drawInfoText(Canvas canvas) {
    double r2 = _r - 0.08 * _r; //下圆半径
    for (int i = 0; i < _data.length; i++) {
      canvas.save();
      canvas.rotate(360 / _data.length * i / 180 * pi + pi);
      drawText(canvas, _data.keys.toList()[i], Offset(-50, r2 - 0.22 * _r),
          fontSize: _r * 0.1);
      canvas.restore();
    }
  }

  //绘制区域
  drawAbility(Canvas canvas, List<double> value) {
    double step = _r * 0.618 / _data.length; //每小段的长度
    mAbilityPath.moveTo(0, -value[0] / (100/_data.length) * step); //起点
    for (int i = 1; i < _data.length; i++) {
      double mark = value[i] /  (100/_data.length);

      var deg=pi/180*(360/_data.length * i - 90);

      mAbilityPath.lineTo(mark * step * cos(deg), mark * step * sin(deg));
    }
    mAbilityPath.close();
    canvas.drawPath(mAbilityPath, mAbilityPaint);
  }

  //绘制文字
  drawText(Canvas canvas, String text, Offset offset,
      {Color color = Colors.black,
      double maxWith = 100,
      double fontSize,
      String fontFamily,
      TextAlign textAlign = TextAlign.center,
      FontWeight fontWeight = FontWeight.bold}) {
    //  绘制文字
    var paragraphBuilder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
        fontFamily: fontFamily,
        textAlign: textAlign,
        fontSize: fontSize,
        fontWeight: fontWeight,
      ),
    );
    paragraphBuilder.pushStyle(
        ui.TextStyle(color: color, textBaseline: ui.TextBaseline.alphabetic));
    paragraphBuilder.addText(text);
    var paragraph = paragraphBuilder.build();
    paragraph.layout(ui.ParagraphConstraints(width: maxWith));
    canvas.drawParagraph(paragraph, Offset(offset.dx, offset.dy));
  }
}

Copy the code
conclusion

This is the end of this article. If you want to taste Flutter quickly, Flutter For Seven days is a must-have. If you want to explore it, follow in my footsteps and complete a Flutter tour. In addition, I have a Flutter wechat communication group. You are welcome to join and discuss Flutter issues together. My wechat account is ZDL1994328.