美文网首页Flutter圈子Android开发经验谈Android技术知识
Flutter自定义控件第一式,炫酷“蛛网”控件

Flutter自定义控件第一式,炫酷“蛛网”控件

作者: 文淑 | 来源:发表于2019-04-18 20:46 被阅读63次

    前言

    「万物之中,希望至美」,《肖生克的救赎》这句话一直记在心里,不论生活多么不易,心有希望,生活一定会越来越好。

    「 Hope is a good thing , maybe the best of things , and no good thing ever dies . 」

    Flutter 出现已经有一段时间了,搭好环境由于忙其他事就搁置了,恰好这次参与公司 Flutter 项目开发,是时候拾起来了,前两天了解了 Dart 语言,今天刚好看到控件这部分,于是简单写了个“蛛网”控件。

    效果图如下:


    在这里插入图片描述

    初步分析

    从效果图上可以分析得到,“蛛网” 由简单的线条组成,那需要绘制线条。在 Android 中,可以通过自定义 View ,在 onDraw 方法中调用 canvas.drawLine;或者调用 canvas.drawPath 绘制线条。那么在 Flutter 又该怎样绘制线条?

    在 Android 中有 View 提供绘制;同样在 Flutter 中有 CustomPainter 提供绘制,源码中是这么介绍的:

    ///  * [Canvas], the class that a custom painter uses to paint.
    ///  * [CustomPaint], the widget that uses [CustomPainter], and whose sample
    ///    code shows how to use the above `Sky` class.
    ///  * [RadialGradient], whose sample code section shows a different take
    ///    on the sample code above.
    abstract class CustomPainter extends Listenable {
    

    大概意思是:画家用来自定义绘画的类,暂且把它理解成 View 。你可以这么来使用它:

    class NetView extends CustomPainter{
      @override
      void paint(Canvas canvas, Size size) {
        // TODO: implement paint
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) {
        // TODO: implement shouldRepaint
        return null;
      }
    }
    

    继承 CustomPainter ,重写 paint 与 shouldRepaint 方法。paint 方法类似 Android 中的 onDraw 方法,但多了一个 Size 参数,来看下 Size 类的定义:

      const Size(double width, double height) : super(width, height);
    

    构造参数 width ,height 表示绘制区域的宽高,可以理解成画布大小。Size 需要从外部调用的地方传入,而 Android 需要在 onMeasure 进行测量,Flutter 的方式更加灵活。

    shouldRepaint 从方法名就可以知道,用于控制重绘,为了提高效率,一般可以这么写:

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

    那么我们就可以在 paint(Canvas canvas, Size size) 方法中绘制我们想要的形状。

    构思代码

    蛛网由底部的网状路径以及顶部的不规则覆盖物组成。那可以分成两部分,第一部分绘制底部网状;第二部分绘制顶部覆盖物。

    观察底部网状路径由多个正多边形组成(边长递增),那么可以拆分先绘制一个正多边形。


    在这里插入图片描述

    在 Flutter 的 canvas 中提供了 drawPath(Path path, Paint paint) 方法绘制路径,与 Android 的使用方式一样。

    通过 Size 可以拿到整个控件区域的大小,那么正多边形的中点坐标就很容易获取到:

        mCenterX = size.width / 2;
        mCenterY = size.height / 2;
    

    为了方便,这里可以把正多边形看成圆内切,想到圆,就应该想到圆的半径,同样根据控件大小获取半径:

        radius = mCenterX / mEdgeSize
    

    最后需要获取到圆内切正多边形的顶点,简单的数学公式:

      double x = mCenterX + radius * cos(degToRad(angle * j));
      double y = mCenterY + radius * sin(degToRad(angle * j));
    

    比较尴尬的是,Flutter 中并没有 Math.toRadians 函数,那只能自己撸一个,就像这样:

      num degToRad(num deg) => deg * (pi / 180.0);
    
      num radToDeg(num rad) => rad * (180.0 / pi);
    

    绘制多个正多边形,只需要改变 radius 的值:

      //  mEdgeSize 表示多边形的边数 i + 1 
      radius = mCenterX / mEdgeSize * (i + 1);
    

    到此,底部的网状路径绘制差不多,顶部的覆盖物类似,随机改变 radius 的长度:

      double value = (random.nextInt(10) + 1) / 10;
      double x = mCenterX + radiusMaxLimit * cos(degToRad(angle * i)) * value;
      double y = mCenterY + radiusMaxLimit * sin(degToRad(angle * i)) * value;
    

    接着看看代码如何写。

    起名字

    起一个接地气的名字,能够让你眼前一亮,就叫 SpiderView

    编写代码

    SpiderView类

    先来看看 SpiderView 类的成员变量:

      Paint mPaint;
      // 覆盖物画笔
      Paint mCoverPaint;
      // 文本画笔
      Paint mTextPaint;
      Path mPath;
      // 绘制边数默认为6
      int mEdgeSize = 6;
      final double CIRCLE_ANGLE = 360;
      // 整个绘制区域的中点坐标
      double mCenterX = 0;
      double mCenterY = 0;
    

    画笔,路劲初始化:

      SpiderView(this.mEdgeSize) {
        // 初始化画笔
        mPaint = new Paint();
        mPaint.color = randomRGB();
        // 设置抗锯齿
        mPaint.isAntiAlias = true;
        // 样式为描边
        mPaint.style = PaintingStyle.stroke;
    
        mPath = new Path();
    
        mCoverPaint = new Paint();
        mCoverPaint.isAntiAlias = true;
        mCoverPaint.style = PaintingStyle.fill;
        mCoverPaint.color = randomARGB();
    
        mTextPaint = new Paint();
        mTextPaint.isAntiAlias = true;
        mTextPaint.style = PaintingStyle.fill;
        mTextPaint.color = Colors.blue;
      }
    

    paint 方法:

      @override
      void paint(Canvas canvas, Size size) {
        // TODO: implement paint
        mCenterX = size.width / 2;
        mCenterY = size.height / 2;
    
        // 图层 防止刷新属性结构
        canvas.save();
        drawSpiderEdge(canvas);
        drawCover(canvas);
        drawText(canvas);
        canvas.restore();
      }
    

    绘制底部网状路劲,代码相对简单,有疑问请留言:

      /**
       * 绘制边线
       */
      void drawSpiderEdge(Canvas canvas) {
        double angle = CIRCLE_ANGLE / mEdgeSize;
        double radius = 0;
        double radiusMaxLimit = mCenterX > mCenterY ? mCenterX : mCenterY;
        for (int i = 0; i < mEdgeSize; i++) {
          mPath.reset();
          radius = radiusMaxLimit / mEdgeSize * (i + 1);
          for (int j = 0; j < mEdgeSize + 1; j++) {
            // 移动
            if (j == 0) {
              mPath.moveTo(mCenterX + radius, mCenterY);
            } else {
              double x = mCenterX + radius * cos(degToRad(angle * j));
              double y = mCenterY + radius * sin(degToRad(angle * j));
              mPath.lineTo(x, y);
            }
          }
          mPath.close();
          canvas.drawPath(mPath, mPaint);
        }
        drawSpiderAxis(canvas, radiusMaxLimit, angle);
      }
    
      /**
       * 绘制轴线
       */
      void drawSpiderAxis(Canvas canvas, double radius, double angle) {
        for (int i = 0; i < mEdgeSize; i++) {
          mPath.reset();
          mPath.moveTo(mCenterX, mCenterX);
          double x = mCenterX + radius * cos(degToRad(angle * i));
          double y = mCenterY + radius * sin(degToRad(angle * i));
          mPath.lineTo(x, y);
          canvas.drawPath(mPath, mPaint);
        }
      }
    

    绘制顶部覆盖物:

      /**
       * 绘制覆盖区域
       */
      void drawCover(Canvas canvas) {
        mPath.reset();
        Random random = new Random();
    
        double angle = CIRCLE_ANGLE / mEdgeSize;
        double radiusMaxLimit = min(mCenterY, mCenterY);
        for (int i = 0; i < mEdgeSize; i++) {
          double value = (random.nextInt(10) + 1) / 10;
          double x = mCenterX + radiusMaxLimit * cos(degToRad(angle * i)) * value;
          double y = mCenterY + radiusMaxLimit * sin(degToRad(angle * i)) * value;
          if (i == 0) {
            mPath.moveTo(x, mCenterY);
          } else {
            mPath.lineTo(x, y);
          }
        }
        mPath.close();
        canvas.drawPath(mPath, mCoverPaint);
      }
    

    源码地址:

    https://github.com/HpWens/MeiWidgetView

    总结

    Flutter 初学,希望对大家有所帮助,如果你有更好的方案,请留言哟。

    想了解更多 Flutter 自定义控件,请关注「控件人生」公众号。每日有干货推送,还有现金红包发放,原创不易,戳戳手指扫码关注。

    qrcode_for_gh_232b5a56667d_258.jpg

    相关文章

      网友评论

        本文标题:Flutter自定义控件第一式,炫酷“蛛网”控件

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