美文网首页FlutterFlutter之旅Flutter
[-Flutter 自定义组件-] 蛛网图+绘制+动画实践

[-Flutter 自定义组件-] 蛛网图+绘制+动画实践

作者: 张风捷特烈 | 来源:发表于2019-07-31 13:26 被阅读8次

    在Android的时候自定义过蛛网图,花了半天时间。复刻到Flutter只用了不到20分钟
    不得不说Flutter中的Canvas对安卓玩家还是非常友好的,越来越觉得Flutter非常有趣。
    在视图方面,Flutter确实要比原生更胜一筹。本文你将学到:

    1.三角函数的使用
    2.Flutter中如何用绘制文字
    3.动画在绘图中的实际运用
    4.Canvas绘图的相关相关方法
    5.Flutter中一个组件的封装
    
    image image
    ---->[使用方法]-------------
    var show = AbilityWidget(
        ability: Ability(duration: 1500, 
        image: AssetImage("images/lifei.jpeg"),
        radius: 100,
            color: Colors.black,
            data: {
            "语文": 40.0,
            "数学": 30.0,
            "英语": 20.0,
            "政治": 40.0,
            "音乐": 80.0,
            "生物": 50.0,
            "化学": 60.0,
            "地理": 80.0,
    
        }));
    

    1.静态蛛网图

    第一步就是如何将一串数据映射成下面的图表:

    var data = {
      "攻击力": 70.0,
      "生命": 90.0,
      "闪避": 50.0,
      "暴击": 70.0,
      "破格": 80.0,
      "格挡": 100.0,
    };
    
    image
    1.1:创建AbilityWidget组件

    线新建一个StatelessWidget的组件使用AbilityPainter进行绘制
    这里先定义画笔、路径等成员变量

    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 = {
        "攻击力": 70.0,
        "生命": 90.0,
        "闪避": 50.0,
        "暴击": 70.0,
        "破格": 80.0,
        "格挡": 100.0,
      };
    
      double mRadius = 100; //外圆半径
      Paint mLinePaint; //线画笔
      Paint mAbilityPaint; //区域画笔
      Paint mFillPaint;//填充画笔
    
      Path mLinePath;//短直线路径
      Path mAbilityPath;//范围路径
    
      AbilityPainter() {
        mLinePath = Path();
        mAbilityPath = Path();
        mLinePaint = Paint()
          ..color = Colors.black
          ..style = PaintingStyle.stroke
          ..strokeWidth=0.008 * mRadius
          ..isAntiAlias = true;
    
        mFillPaint = Paint() //填充画笔
          ..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;
      }
    }
    

    1.2.绘制外圈

    为了减少变量值,让尺寸具有很好的联动性(等比扩缩),小黑条的长宽将取决于最大半径mRadius
    则:小黑条长:mRadius*0.08 小黑条宽:mRadius*0.05 所以r2=mRadius-mRadius*0.08

    外圈绘制.png
    @override
    void paint(Canvas canvas, Size size) {
        canvas.translate(mRadius, mRadius); //移动坐标系
        drawOutCircle(canvas);
    }
    
    //绘制外圈
    void drawOutCircle(Canvas canvas) {
      canvas.save();//新建图层
      canvas.drawCircle(Offset(0, 0), mRadius, mLinePaint);//圆形的绘制
      double r2 = mRadius - 0.08 * mRadius; //下圆半径
      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, -mRadius), Offset(0, -r2), mFillPaint);//线的绘制
        canvas.restore();//释放图层
      }
      canvas.restore();//释放图层
    }
    

    1.3.绘制内圈

    同样尺寸和最外圆看齐,这里绘制有一丢丢复杂,你需要了解canvas和path的使用
    看不懂的可转到canvaspath,如果看了这两篇还问绘制有什么技巧的,可转到这里

    内圈绘制.png
    @override
    void paint(Canvas canvas, Size size) {
        canvas.translate(mRadius, mRadius); //移动坐标系
        drawOutCircle(canvas);
        drawInnerCircle(canvas);
    }
    
    //绘制内圈圆
    drawInnerCircle(Canvas canvas) {
      double innerRadius = 0.618 * mRadius;//内圆半径
      canvas.drawCircle(Offset(0, 0), innerRadius, mLinePaint);
      canvas.save();
      for (var i = 0; i < 6; i++) {//遍历6条线
        canvas.save();
        canvas.rotate(60 * i.toDouble() / 180 * pi); //每次旋转60°
        mPath.moveTo(0, -innerRadius);
        mPath.relativeLineTo(0, innerRadius); //线的路径
        for (int j = 1; j < 6; j++) {
          mPath.moveTo(-mRadius * 0.02, innerRadius / 6 * j);
          mPath.relativeLineTo(mRadius * 0.02 * 2, 0);
        } //加5条小线
        canvas.drawPath(mPath, mLinePaint); //绘制线
        canvas.restore();
      }
      canvas.restore();
    }
    

    1.3.绘制文字

    Flutter中绘制文字可有点略坑,我这里简单的封了一个drawText函数用来画文字
    记得导入ui库,使用Paragraph进行文字的设置,drawParagraph进行绘制

    image
    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);
        canvas.restore();
      }
    }
    
    //绘制文字
    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));
    }
    

    1.4.绘制范围

    最后也是最难的一块,你准备好草稿纸了吗?

    image
    //绘制区域
    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);
    }
    

    2.动画效果

    让外圈转和内圈相反方向转,所以可以让内圈和外圈分成两个组件放在一个Stack里

    2.1:抽离外圈
    class OutlinePainter extends CustomPainter {
      double mRadius = 100; //外圆半径
      Paint mLinePaint; //线画笔
      Paint mFillPaint; //填充画笔
    
      OutlinePainter() {
        mLinePaint = Paint()
          ..color = Colors.black
          ..style = PaintingStyle.stroke
          ..strokeWidth = 0.008 * mRadius
          ..isAntiAlias = true;
    
        mFillPaint = Paint() //填充画笔
          ..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(); //新建图层
        canvas.drawCircle(Offset(0, 0), mRadius, mLinePaint); //圆形的绘制
        double r2 = mRadius - 0.08 * mRadius; //下圆半径
        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, -mRadius), Offset(0, -r2), mFillPaint); //线的绘制
          canvas.restore(); //释放图层
        }
        canvas.restore(); //释放图层
      }
    }
    

    2.2:使用动画

    这里用Stack进行组件的堆叠

    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: const Duration(milliseconds: 2000), //时长
            vsync: this);
        var tween = Tween(begin: 0.0, end: 360.0); //创建从25到150变化的Animatable对象
        animation = tween.animate(controller); //执行animate方法,生成
        animation.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/娜美.jpg",
                width: 200,
                height: 200,
                fit: BoxFit.cover,
              ),
            ),
          ),
        );
        var center = Transform.rotate(
            angle: -_angle / 180 * pi,
            child: Transform.scale(
              scale: 0.5 + animation.value / 360 / 2,
              child: SizedBox(
                width: 200,
                height: 200,
                child: paint,
              ),
            ));
        return Center(
          child: Stack(
            alignment: Alignment.center,
            children: <Widget>[img, center, outlinePainter],
          ),
        );
      }
    }
    

    3.组件封装

    到现在逻辑上没有问题了,剩下的就是对组件的封装,将一些量进行提取
    下面就是简单封装了一下,还有很多乱七八糟的没封装,比如颜色,动画效果等。

    image
    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));
      }
    }
    
    
    结语

    本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
    另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。

    相关文章

      网友评论

        本文标题:[-Flutter 自定义组件-] 蛛网图+绘制+动画实践

        本文链接:https://www.haomeiwen.com/subject/pywzrctx.html