首先让我们来看下这张图
当UI做的设计图中有这么一个元素,我想大多数人第一反应就是叫UI切图,然后直接使用Image
加载,我一开始也是这么做的,毕竟省时省力省心。
但是由于后面需要针对不同的状态设置不同的颜色,我不想写过多判断语句来切换图标(我目前的做法是实现一个枚举类,然后拓展该枚举,针对每个状态设置不同的颜色,然后直接通过枚举拿到对应状态的颜色传入)
图片分析
从图片上,我们可以看到主要由以下部分构成:
- 外层阴影:给图标提供立体效果。
- 绿色底层圆:用来描绘背景。
- 深绿色顶层圆:叠加在底层圆上,进一步增加层次感。
- 白色闪电图标:位于圆的中央,表示充电。
为什么使用画笔而不是直接使用图片
使用画笔绘制图形而非直接使用图片的好处包括:
- 可扩展性:矢量图形可以根据不同屏幕尺寸动态调整,而不会失真。
- 自定义性:使用画笔可以随意调整颜色、形状等属性,更加灵活。
- 性能优化:绘制图形往往比加载位图更高效,特别是在需要频繁重绘的场景中。
实现步骤
下面我们逐步实现这个效果,希望能让各位有所收获
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
,它给提供了两个方法,分别是 paint
和shouldRepaint
,我们画笔实现的所有内容均在 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,
);
将上面代码编写完成后,你将会获得一个灰色的圆形,如下图:
3. 绘制底层绿色圆
接下来,我们绘制一个稍小一点的绿色圆,作为底层背景。
// 绘制第二个圆
paint.shader = null;
paint.color = bottomColor;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
circleSize - 1.5298,
paint,
);
完成上面代码编写,你将会得到一个比外层阴影圆小一些的一个圆形,如下图:
4. 绘制顶层深绿色圆
然后,绘制另一个更小的深绿色圆,进一步增加层次感。
// 绘制第三个圆
paint.color = topColor;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2),
circleSize - 7.9043,
paint,
);
效果如下:
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,
);
如上图,我们设置了一个椭圆形白色渐变的形状,这是一个很重要的效果,主要实现一个内阴影的效果实现,增强立体感。但是很明显,它的效果太白了,和设计的效果差距巨大。显然不是我们想要的效果,但实际上解决方案也很简单,但有个前提,每个圆的效果都需要使用同一个画笔。先前我做这个效果的时候,是每个圆都创建一个新的画笔,因此无法实现,不过当前文章使用的都是同一个画笔。
在发现画笔实现的效果和设计图实现的效果的区别后,我想到了PS中有一个叫图层混合的效果,我想设计图应该也是这么实现的,就查了下画笔是否有这个功能,很幸运的是,确实有这么一个功能,我们只需要在画出这个椭圆形后,添加下面这行代码到canvas.drawOval
之前
paint.blendMode = BlendMode.overlay;
这段代码指定了绘制时使用的混合模式。以下是对这段代码的详细解释:
混合模式(BlendMode)
混合模式决定了在绘制图形时,如何将新绘制的内容与已有的内容进行混合。在 Flutter 中,BlendMode
枚举类提供了多种混合模式选项,BlendMode.overlay
是其中一种。
BlendMode.overlay
的工作原理
BlendMode.overlay
结合了 BlendMode.multiply
和 BlendMode.screen
的效果。具体来说,当底色比中性灰(50% 灰色)暗时,overlay
使用 BlendMode.multiply
;当底色比中性灰亮时,overlay
使用 BlendMode.screen
。这种效果通常用于创建高对比度和富有细节的图像。
在你的代码中,设置 paint.blendMode = BlendMode.overlay;
意味着在绘制椭圆形光效时,颜色将与底层颜色混合,产生亮部更亮、暗部更暗的效果,从而增强立体感和光泽效果。
实际效果
在绘制中,BlendMode.overlay
使得椭圆形光效部分的白色渐变与底层的绿色圆形混合。这种混合效果不会完全覆盖底色,而是根据底色的亮度调整新颜色的亮度,从而产生更加自然和生动的光效。
6. 绘制闪电符号
设置闪电尺寸
首先,我们设置闪电符号的尺寸,使其相对于最小圆的半径进行缩放。
要计算闪电符号路径中的各个点,我们需要根据实际的图形形状定义每个点的位置,并通过数学公式将其缩放和定位。以下是闪电路径的构造步骤和计算公式。
闪电符号的几何构造
我们将闪电符号视为由一系列点和线段组成的多边形。每个点的坐标可以通过比例缩放来确定。
闪电符号的比例数据
为了构建闪电路径,我们需要定义每个点的相对位置。假设闪电符号的原始尺寸高度为 H
,宽度为 W
。通过以下公式可以计算每个点的位置:
-
顶点
A
:顶部点,位于中心上方- ( A_x = \frac{6.5847}{23.0308} \times lightningSize )
- ( A_y = -lightningSize )
-
顶点
B
:左下点- ( B_x = -\frac{11.0841}{23.0308} \times lightningSize )
- ( B_y = \frac{3.8118}{23.0308} \times lightningSize )
-
顶点
C
:右上点- ( C_x = -\frac{0.7198}{23.0308} \times lightningSize )
- ( C_y = \frac{3.8118}{23.0308} \times lightningSize )
-
顶点
D
:左上点- ( D_x = -\frac{5.8193}{23.0308} \times lightningSize )
- ( D_y = \frac{24.3206}{23.0308} \times lightningSize )
-
顶点
E
:右下点- ( E_x = \frac{11.6842}{23.0308} \times lightningSize )
- ( E_y = -\frac{2.9255}{23.0308} \times lightningSize )
-
顶点
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);
总结
通过定义闪电符号的顶点比例,并将其转换为实际坐标,我们可以绘制出一个相对固定比例的闪电符号。这样的方法允许我们在不同大小的圆中绘制相同比例的闪电图标。上述公式通过比例缩放和位置调整,确保闪电符号在中心对称的位置。
最终效果如下:
完整代码
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开发提供帮助。
愿君多采撷,莫负好时光。 技术若有涯,勤学步步芳。