零:本文效果简述
本文来通过一个小案例,介绍一下 Flutter 绘制
和 Flutter 动画
的使用。如下,是一个七彩的圆环,其中有两个动画效果:
[1]
. 四周有的光晕会进行扩散和收缩动画
。[2]
. 圆的外圈有一段流光
围绕圆环旋转。
一、静态效果绘制
1.外圈的绘制
下面定义一个 CircleHalo
组件用于展示 CircleHaloPainter
画板绘制的内容。由于后面要进行动画,使用这样定义为 StatefulWidget
。
class CircleHalo extends StatefulWidget {
const CircleHalo({Key key}) : super(key: key);
@override
_CircleHaloState createState() => _CircleHaloState();
}
class _CircleHaloState extends State<CircleHalo> {
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(200, 200),
painter: CircleHaloPainter(),
);
}
}
先来画个圈,这应该对大家来说不是什么难事。
代码语言:javascript复制class CircleHaloPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;
final Path circlePath = Path();
circlePath.addOval(
Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100));
canvas.drawPath(circlePath, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
现在来看下如何产生光晕:Paint
对象可以设置 maskFilter
属性,可以通过 MaskFilter.blur
让画笔进行模糊,BlurStyle.solid
模式会让画笔绘制时,四周产生模糊的阴影。第二参决定模糊程度
,如下分别是模糊程度
为 2、4、6
的效果。
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;
// 设置 maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);
接下来就是设置彩色画笔,可以通过 Paint
对象的 shader
属性设置着色器,如下是一个多彩的扫描渐变着色。
import 'dart:ui' as ui ;
List<Color> colors = [
Color(0xFFF60C0C),
Color(0xFFF3B913),
Color(0xFFE7F716),
Color(0xFF3DF30B),
Color(0xFF0DF6EF),
Color(0xFF0829FB),
Color(0xFFB709F4),
];
colors.addAll(colors.reversed.toList());
final List<double> pos = List.generate(colors.length, (index) => index / colors.length);
// 设置 shader
paint.shader =
ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0, 2 * pi);
// 设置 maskFilter
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);
到这,我们就完成了 1/4
,光晕扩散和收缩动画
其实就是动态更改模糊遮罩的 sigma
值而已。
2.外圈流光静态效果
外圈旋转的静态效果如下最左侧,是一个月牙形
的圆弧。可以通过两个圆路径通过 difference
进行联合得到,其中两个圆心在横向有略微的偏距,偏距越大,月牙也就越胖,下面是 偏距 =1
的效果。
class CircleHaloPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
final Paint paint = Paint()
..style = PaintingStyle.stroke;
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4);
//路径1
final Path circlePath = Path()..addOval(
Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100));
//路径2
Path circlePath2 = Path()..addOval(
Rect.fromCenter(center: Offset(-1, 0), width: 100, height: 100));
//联合路径
Path result = Path.combine(PathOperation.difference, circlePath, circlePath2);
//颜色填充
paint..style = PaintingStyle.fill..color = Color(0xff00abf2);
canvas.drawPath(result, paint); //绘制
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
二、外圈的动画
首先来实现如下的光晕扩散和收缩动画,动画周期为 2s
,不断重复执行。
1.状态类处理
自己处理动画,首先创建动画控制器。由于动画控制器构造时需要一个 TickerProvider
入参,可以让 _CircleHaloState
混入 SingleTickerProviderStateMixin
使状态类本身成为 TickerProvider
的实现类。如下,在 initState
中创建了一个 2s
的动画器,并通过 repeat
方法进行重复动画。在构造 CircleHaloPainter
时,将动画器作为入参。
class _CircleHaloState extends State<CircleHalo> with SingleTickerProviderStateMixin {
AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
_ctrl.repeat();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(200, 200),
painter: CircleHaloPainter(_ctrl),
);
}
}
2.画板的动画处理
我们的目的是让 MaskFilter.blur
的第二参数值随动画器进行改变,从而达到动画的效果。CustomPainter
子类构造可以通过 super(repaint:可见听对象)
,来关联 Listenable
对象。因为动画器 Animation
是 Listenable
的子类,这里关联动画器,这样动画器数值改变时,就会通知画板重绘。
下面处理中,比较重要的点是通过 TweenSequence
定义一个来回变化的 Tween
,比如动画时长为 2s
, 在第1秒在 0~4
间变化,第2秒在 4~0
间变化,这样就可以达到在一个动画周期中,数值一来一回。另外通过 chain
方法传入CurveTween
对象 ,可以让当前 Animatable
的数值变化增加动画曲线效果。
class CircleHaloPainter extends CustomPainter {
Animation<double> animation;
CircleHaloPainter(this.animation) : super(repaint: animation);
final Animatable<double> breatheTween = TweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: 4),
weight: 1,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 4, end: 0),
weight: 1,
),
],
).chain(CurveTween(curve: Curves.decelerate));
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;
Path circlePath = Path()
..addOval(Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100));
List<Color> colors = [
Color(0xFFF60C0C), Color(0xFFF3B913), Color(0xFFE7F716),
Color(0xFF3DF30B), Color(0xFF0DF6EF), Color(0xFF0829FB),
Color(0xFFB709F4),
];
colors.addAll(colors.reversed.toList());
final List<double> pos = List.generate(colors.length, (index) => index / colors.length);
paint.shader =
ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0, 2 * pi);
paint.maskFilter =
MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
canvas.drawPath(circlePath, paint);
}
@override
bool shouldRepaint(covariant CircleHaloPainter oldDelegate) =>
oldDelegate.animation != animation;
}
三、流光的动画
拆开来看,外圈的旋转效果如下。动画实现也非常简单,就是根据动画器的值,让圆弧不断旋转而已。
代码语言:javascript复制@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;
paint.maskFilter =
MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
final Path circlePath = Path()..addOval(
Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100));
Path circlePath2 = Path()..addOval(
Rect.fromCenter(center: Offset(-1, 0), width: 100, height: 100));
Path result = Path.combine(PathOperation.difference, circlePath, circlePath2);
canvas.save();
canvas.rotate(animation.value * 2 * pi);
paint..style = PaintingStyle.fill..color = Color(0xff00abf2);
canvas.drawPath(result, paint);
canvas.restore();
}
最后,绘制时,将两个东西都画出来就行了。
另外,本绘制已放入 FlutterUnit 的绘制集录中,大家可以更新查看。
下面把所有的代码贴一些,大家可以运行一下玩玩。
代码语言:javascript复制import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class CircleHalo extends StatefulWidget {
const CircleHalo({Key key}) : super(key: key);
@override
_CircleHaloState createState() => _CircleHaloState();
}
class _CircleHaloState extends State<CircleHalo>
with SingleTickerProviderStateMixin {
AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
_ctrl.repeat();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size(200, 200),
painter: CircleHaloPainter(_ctrl),
);
}
}
class CircleHaloPainter extends CustomPainter {
Animation<double> animation;
CircleHaloPainter(this.animation) : super(repaint: animation);
final Animatable<double> rotateTween = Tween<double>(begin: 0, end: 2 * pi)
.chain(CurveTween(curve: Curves.easeIn));
final Animatable<double> breatheTween = TweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: 4),
weight: 1,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 4, end: 0),
weight: 1,
),
],
).chain(CurveTween(curve: Curves.decelerate));
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
final Paint paint = Paint()
..strokeWidth = 1
..style = PaintingStyle.stroke;
Path circlePath = Path()
..addOval(Rect.fromCenter(center: Offset(0, 0), width: 100, height: 100));
Path circlePath2 = Path()
..addOval(
Rect.fromCenter(center: Offset(-1, 0), width: 100, height: 100));
Path result =
Path.combine(PathOperation.difference, circlePath, circlePath2);
List<Color> colors = [
Color(0xFFF60C0C), Color(0xFFF3B913), Color(0xFFE7F716),
Color(0xFF3DF30B), Color(0xFF0DF6EF), Color(0xFF0829FB), Color(0xFFB709F4),
];
colors.addAll(colors.reversed.toList());
final List<double> pos =
List.generate(colors.length, (index) => index / colors.length);
paint.shader =
ui.Gradient.sweep(Offset.zero, colors, pos, TileMode.clamp, 0, 2 * pi);
paint.maskFilter =
MaskFilter.blur(BlurStyle.solid, breatheTween.evaluate(animation));
canvas.drawPath(circlePath, paint);
canvas.save();
canvas.rotate(animation.value * 2 * pi);
paint
..style = PaintingStyle.fill
..color = Color(0xff00abf2);
paint.shader=null;
canvas.drawPath(result, paint);
canvas.restore();
}
@override
bool shouldRepaint(covariant CircleHaloPainter oldDelegate) =>
oldDelegate.animation != animation;
}