首页 > 其他分享 >Flutter 自定义画笔案例

Flutter 自定义画笔案例

时间:2024-08-01 14:27:50浏览次数:17  
标签:lightningSize 自定义 画笔 height paint width 23.0308 Flutter size

首先让我们来看下这张图

当UI做的设计图中有这么一个元素,我想大多数人第一反应就是叫UI切图,然后直接使用Image加载,我一开始也是这么做的,毕竟省时省力省心。

但是由于后面需要针对不同的状态设置不同的颜色,我不想写过多判断语句来切换图标(我目前的做法是实现一个枚举类,然后拓展该枚举,针对每个状态设置不同的颜色,然后直接通过枚举拿到对应状态的颜色传入)

图片分析

从图片上,我们可以看到主要由以下部分构成:

  1. 外层阴影:给图标提供立体效果。
  2. 绿色底层圆:用来描绘背景。
  3. 深绿色顶层圆:叠加在底层圆上,进一步增加层次感。
  4. 白色闪电图标:位于圆的中央,表示充电。

为什么使用画笔而不是直接使用图片

使用画笔绘制图形而非直接使用图片的好处包括:

  1. 可扩展性:矢量图形可以根据不同屏幕尺寸动态调整,而不会失真。
  2. 自定义性:使用画笔可以随意调整颜色、形状等属性,更加灵活。
  3. 性能优化:绘制图形往往比加载位图更高效,特别是在需要频繁重绘的场景中。

实现步骤

下面我们逐步实现这个效果,希望能让各位有所收获

1. 创建一个 ChargePainter

首先,我们需要创建一个 CustomPainter 的子类 ChargePainter。在这个类中,我们将定义颜色属性,并在 paint 方法中实现绘制逻辑。

class ChargePainter extends CustomPainter {
  // 底层颜色
  final Color bottomColor;
  // 顶层颜色
  final Color topColor;

  ChargePainter({
    this.bottomColor = const Color(0xFF1ACC2C),
    this.topColor = const Color(0xFF1FA22C),
  });

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    double circleSize = size.width / 2;

    // 代码的详细解释将放在后续步骤中
  }

  @override
  bool shouldRepaint(covariant ChargePainter oldDelegate) {
    return oldDelegate.bottomColor != bottomColor || oldDelegate.topColor != topColor;
  }
}

解释一下上面的代码,我们继承了一个CustomPainter,它给提供了两个方法,分别是 paintshouldRepaint,我们画笔实现的所有内容均在 paint里,而shouldRepaint是用来判断是否重绘画笔的,接下来让我们绘制一个带阴影的圆

2. 绘制外层阴影

paint 方法中,首先绘制一个带阴影的圆,这个圆位于底层,提供立体效果。

// 绘制第一个圆
paint.shader = const LinearGradient(
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
  colors: [Color(0xFF494949), Color(0xFF494949)],
).createShader(Rect.fromCircle(
    center: Offset(size.width / 2, size.height / 2),
    radius: size.width / 2));
paint.style = PaintingStyle.fill;
paint.color = Colors.black.withOpacity(0.6);
canvas.drawCircle(
  Offset(size.width / 2, size.height / 2),
  circleSize,
  paint,
);

将上面代码编写完成后,你将会获得一个灰色的圆形,如下图:

image

3. 绘制底层绿色圆

接下来,我们绘制一个稍小一点的绿色圆,作为底层背景。

// 绘制第二个圆
paint.shader = null;
paint.color = bottomColor;
canvas.drawCircle(
  Offset(size.width / 2, size.height / 2),
  circleSize - 1.5298,
  paint,
);

完成上面代码编写,你将会得到一个比外层阴影圆小一些的一个圆形,如下图:

image

4. 绘制顶层深绿色圆

然后,绘制另一个更小的深绿色圆,进一步增加层次感。

// 绘制第三个圆
paint.color = topColor;
canvas.drawCircle(
  Offset(size.width / 2, size.height / 2),
  circleSize - 7.9043,
  paint,
);

效果如下:

image

5. 绘制中间椭圆形的光效

接下来,使用渐变绘制一个椭圆形的光效,增强立体感。

// 绘制椭圆形
paint.shader = LinearGradient(
  begin: Alignment.topCenter,
  end: Alignment.bottomCenter,
  colors: [Colors.white, Colors.white.withOpacity(0)],
).createShader(Rect.fromLTWH(0, 8.4668, size.width, 65.7849));
canvas.drawOval(
  Rect.fromCenter(
      center: Offset(size.width / 2, size.height / 2 + 2.5498),
      width: circleSize * 1.87,
      height: circleSize * 1.73),
  paint,
);

image

如上图,我们设置了一个椭圆形白色渐变的形状,这是一个很重要的效果,主要实现一个内阴影的效果实现,增强立体感。但是很明显,它的效果太白了,和设计的效果差距巨大。显然不是我们想要的效果,但实际上解决方案也很简单,但有个前提,每个圆的效果都需要使用同一个画笔。先前我做这个效果的时候,是每个圆都创建一个新的画笔,因此无法实现,不过当前文章使用的都是同一个画笔。

在发现画笔实现的效果和设计图实现的效果的区别后,我想到了PS中有一个叫图层混合的效果,我想设计图应该也是这么实现的,就查了下画笔是否有这个功能,很幸运的是,确实有这么一个功能,我们只需要在画出这个椭圆形后,添加下面这行代码到canvas.drawOval之前

paint.blendMode = BlendMode.overlay;

这段代码指定了绘制时使用的混合模式。以下是对这段代码的详细解释:

混合模式(BlendMode)

混合模式决定了在绘制图形时,如何将新绘制的内容与已有的内容进行混合。在 Flutter 中,BlendMode 枚举类提供了多种混合模式选项,BlendMode.overlay 是其中一种。

BlendMode.overlay 的工作原理

BlendMode.overlay 结合了 BlendMode.multiplyBlendMode.screen 的效果。具体来说,当底色比中性灰(50% 灰色)暗时,overlay 使用 BlendMode.multiply;当底色比中性灰亮时,overlay 使用 BlendMode.screen。这种效果通常用于创建高对比度和富有细节的图像。

在你的代码中,设置 paint.blendMode = BlendMode.overlay; 意味着在绘制椭圆形光效时,颜色将与底层颜色混合,产生亮部更亮、暗部更暗的效果,从而增强立体感和光泽效果。

实际效果

在绘制中,BlendMode.overlay 使得椭圆形光效部分的白色渐变与底层的绿色圆形混合。这种混合效果不会完全覆盖底色,而是根据底色的亮度调整新颜色的亮度,从而产生更加自然和生动的光效。

image

6. 绘制闪电符号

设置闪电尺寸

首先,我们设置闪电符号的尺寸,使其相对于最小圆的半径进行缩放。

要计算闪电符号路径中的各个点,我们需要根据实际的图形形状定义每个点的位置,并通过数学公式将其缩放和定位。以下是闪电路径的构造步骤和计算公式。

闪电符号的几何构造

我们将闪电符号视为由一系列点和线段组成的多边形。每个点的坐标可以通过比例缩放来确定。

闪电符号的比例数据

为了构建闪电路径,我们需要定义每个点的相对位置。假设闪电符号的原始尺寸高度为 H,宽度为 W。通过以下公式可以计算每个点的位置:

  1. 顶点 A:顶部点,位于中心上方

    • ( A_x = \frac{6.5847}{23.0308} \times lightningSize )
    • ( A_y = -lightningSize )
  2. 顶点 B:左下点

    • ( B_x = -\frac{11.0841}{23.0308} \times lightningSize )
    • ( B_y = \frac{3.8118}{23.0308} \times lightningSize )
  3. 顶点 C:右上点

    • ( C_x = -\frac{0.7198}{23.0308} \times lightningSize )
    • ( C_y = \frac{3.8118}{23.0308} \times lightningSize )
  4. 顶点 D:左上点

    • ( D_x = -\frac{5.8193}{23.0308} \times lightningSize )
    • ( D_y = \frac{24.3206}{23.0308} \times lightningSize )
  5. 顶点 E:右下点

    • ( E_x = \frac{11.6842}{23.0308} \times lightningSize )
    • ( E_y = -\frac{2.9255}{23.0308} \times lightningSize )
  6. 顶点 F:右中点

    • ( F_x = \frac{1.6576}{23.0308} \times lightningSize )
    • ( F_y = -\frac{2.9255}{23.0308} \times lightningSize )

闪电符号的路径计算公式

根据上述顶点的相对位置,我们可以计算出每个点的实际坐标,并绘制闪电路径。


// 绘制闪电路径
double lightningSize = (circleSize - 7.9043) / 1.5; // 使闪电比第二个圆小

Path path = Path();
path.moveTo(
  size.width / 2 + 6.5847 * lightningSize / 23.0308,
  size.height / 2 - 23.0308 * lightningSize / 23.0308,
);
path.lineTo(
  size.width / 2 - 11.0841 * lightningSize / 23.0308,
  size.height / 2 + 3.8118 * lightningSize / 23.0308,
);
path.lineTo(
  size.width / 2 - 0.7198 * lightningSize / 23.0308,
  size.height / 2 + 3.8118 * lightningSize / 23.0308,
);
path.lineTo(
  size.width / 2 - 5.8193 * lightningSize / 23.0308,
  size.height / 2 + 24.3206 * lightningSize / 23.0308,
);
path.lineTo(
  size.width / 2 + 11.6842 * lightningSize / 23.0308,
  size.height / 2 - 2.9255 * lightningSize / 23.0308,
);
path.lineTo(
  size.width / 2 + 1.6576 * lightningSize / 23.0308,
  size.height / 2 - 2.9255 * lightningSize / 23.0308,
);
path.close();


Paint lightningPaint = Paint()
  ..color = Colors.white.withOpacity(0.8)
  ..style = PaintingStyle.fill;

canvas.drawPath(path, lightningPaint);

总结

通过定义闪电符号的顶点比例,并将其转换为实际坐标,我们可以绘制出一个相对固定比例的闪电符号。这样的方法允许我们在不同大小的圆中绘制相同比例的闪电图标。上述公式通过比例缩放和位置调整,确保闪电符号在中心对称的位置。
最终效果如下:

image

完整代码

class ChargePainter extends CustomPainter {
  final Color bottomColor;
  final Color topColor;

  ChargePainter({
    this.bottomColor = const Color(0xFF1ACC2C),
    this.topColor = const Color(0xFF1FA22C),
  });

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint();
    double circleSize = size.width / 2;

    // 绘制第一个圆
    paint.shader = const LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: [Color(0xFF494949), Color(0xFF494949)],
    ).createShader(Rect.fromCircle(
        center: Offset(size.width / 2, size.height / 2),
        radius: size.width / 2));
    paint.style = PaintingStyle.fill;
    paint.color = Colors.black.withOpacity(0.6);
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      circleSize,
      paint,
    );

    // 绘制第二个圆
    paint.shader = null;
    paint.color = bottomColor;
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      circleSize - 1.5298,
      paint,
    );

    // 绘制第三个圆
    paint.color = topColor;
    canvas.drawCircle(
      Offset(size.width / 2, size.height / 2),
      circleSize - 7.9043,
      paint,
    );

    // 绘制椭圆形
    paint.shader = LinearGradient(
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
      colors: [Colors.white, Colors.white.withOpacity(0)],
    ).createShader(Rect.fromLTWH(0, 8.4668, size.width, 65.7849));
    paint.blendMode = BlendMode.overlay;
    canvas.drawOval(
      Rect.fromCenter(
          center: Offset(size.width / 2, size.height / 2 + 2.5498),
          width: circleSize * 1.87,
          height: circleSize * 1.73),
      paint,
    );

    // 绘制闪电路径
    double lightningSize = (circleSize - 7.9043) / 1.5; // 使闪电比第二个圆小

    Path path = Path();
    path.moveTo(
      size.width / 2 + 6.5847 * lightningSize / 23.0308,
      size.height / 2 - 23.0308 * lightningSize / 23.0308,
    );
    path.lineTo(
      size.width / 2 - 11.0841 * lightningSize / 23.0308,
      size.height / 2 + 3.8118 * lightningSize / 23.

0308,
    );
    path.lineTo(
      size.width / 2 - 0.7198 * lightningSize / 23.0308,
      size.height / 2 + 3.8118 * lightningSize / 23.0308,
    );
    path.lineTo(
      size.width / 2 - 5.8193 * lightningSize / 23.0308,
      size.height / 2 + 24.3206 * lightningSize / 23.0308,
    );
    path.lineTo(
      size.width / 2 + 11.6842 * lightningSize / 23.0308,
      size.height / 2 - 2.9255 * lightningSize / 23.0308,
    );
    path.lineTo(
      size.width / 2 + 1.6576 * lightningSize / 23.0308,
      size.height / 2 - 2.9255 * lightningSize / 23.0308,
    );
    path.close();

    Paint lightningPaint = Paint()
      ..color = Colors.white.withOpacity(0.8)
      ..style = PaintingStyle.fill;

    canvas.drawPath(path, lightningPaint);
  }

  @override
  bool shouldRepaint(covariant ChargePainter oldDelegate) {
    return oldDelegate.bottomColor != bottomColor || oldDelegate.topColor != topColor;
  }
}

通过这种方式,不仅提高了代码的可扩展性和性能,还使我们能够轻松适应不同的设计需求和状态变化。希望这篇教程能为大家的Flutter开发提供帮助。

愿君多采撷,莫负好时光。 技术若有涯,勤学步步芳。

标签:lightningSize,自定义,画笔,height,paint,width,23.0308,Flutter,size
From: https://www.cnblogs.com/inthecloud/p/18336611

相关文章

  • el-progress 自定义线状进度条右边的文字
    需要展示类似下面的效果 搜了很多slot的方式试了都不行,好像是因为我后面的文字太长了导致了换行,加上这边需要加其他的样式,最后干脆将原始的文字变成空的,自己写右边的文字加样式了<divstyle="margin:10px020px0"v-f......
  • 关键错误:“工具”。 CrewAI 在制作自定义工具时出错?
    我开发了一个团队来从不同的URL获取一些信息。到目前为止总共有大约3个URL,所以我创建了5个代理。1是编辑器(经理),1是其他3个带到表中的所有数据的编译器。如果这有帮助的话,这是我的文件夹结构university_scraper/│├──src/│├──__init__.py│......
  • vue3 自定义渲染,渲染函数实现,配置渲染render函数,低代码配置自定义渲染函数核心实现
    代码父组件<template><divclass="component-name"><!--全局自动的icon--><Extend:render="render"/></div></template><scriptsetuplang="ts">import{ref,reac......
  • 自定义线程池实现(一)
    预期目标1.实现一个相对完备的线程池2.自定义拒绝策略(下一节)线程池的基本参数1.核心线程数2.超时时间3.拒绝策略(在下一篇中添加)4.工作队列5.任务队列工作机制当添加一个任务到线程池中时,线程池会判断工作线程数量是否小于核心线程数,若小于创建工作线程,执行任务;反......
  • 懂个锤子Vue 自定义指定、插槽:
    Vue自定义指定、插槽......
  • Quart自定义文件导出名
    直接上代码fromquartimportQuart,send_fileimportioimportxlwtapp=Quart(__name__)@app.route('/download-excel',methods=["POST"])asyncdefdownload_excel():#创建一个简单的Excel文件workbook=xlwt.Workbook()sheet=workb......
  • 自定义Obsidian输入栏宽度
    自定义Obsidian输入栏宽度以Obsidian的主题Minimal为案例,进行输入栏宽度的调整;若是没有此主题Minimal,通过设置找到外观后,主题那栏点击管理输入Minimal进行下载(这个主题还是挺不错的);点击左下角设置,选择外观,点击文件夹,找到对应的.css文件文件夹的......
  • springboot自学(5)自定义starter
      测试文件可以删除掉了,配置文件改一下后缀修改pom业务代码开发添加自动配置类,并且加上spring.factories到此为止就初步完成了,install到本地的maven仓库然后在使用的项目里加上依赖就行了导入项目,并调用定时任务报表开发先做个表格的打印方法表格......
  • 自定义的基于线程的监控如何影响 celery 任务的启动时间?
    我使用Flask和celery来构建后端api。为了防止任务运行时间过长,我实现了一个自定义的基于线程的监视类来监视任务,并在任务运行时间过长时停止它们。这是我的实现。importosimportsysfromflaskimportFlaskfromceleryimportCelery,Taskimportloggingf......
  • mapbox聚合使用自定义图标
    1mapboxgl.accessToken='YOUR_MAPBOX_ACCESS_TOKEN';2varmap=newmapboxgl.Map({3container:'map',4style:'mapbox://styles/mapbox/streets-v11',5center:[-74.5,40],6zoom:9.57});89map.on(�......