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

前言

「万物之中,但愿至美」,《肖生克的救赎》这句话一直记在内心,不论生活多么不易,心有但愿,生活必定会愈来愈好。java

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

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

效果图以下: web

在这里插入图片描述

初步分析

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

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

/// * [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 。你能够这么来使用它:dom

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 类的定义:ide

const Size(double width, double height) : super(width, height);
复制代码

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

shouldRepaint 从方法名就能够知道,用于控制重绘,为了提升效率,通常能够这么写:this

@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);
  }
复制代码

源码地址

总结

Flutter 初学,但愿对你们有所帮助,若是你有更好的方案,请留言哟。

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

相关文章
相关标签/搜索