前言
在移动开发中,无论是平常的按钮还是炫酷的动画都离不开绘制,每个平台都有自己的绘制体系,系统绘制工具以及随时间积累下来的各种优秀的开源框架。日常开发中,我们不仅要会使用这些轮子,往往为了更好的满足设计的需求,还需要对其进行修修补补,升级改造。这个时候,我们就需要对绘制的知识有更深一步的了解,作为一名移动开发者,我们可能对 Android/iOS 的绘制有所了解,所谓触类旁通,今天一起了解下 Flutter 中的绘制知识。
绘制 API
代码基于 Flutter 2.2 版本
绘制元素
生活中的绘画离不开笔、墨、纸,程序中的绘制也是如此。
- 画布:Canvas
- 画笔:Paint
- 路径:Path
- 颜色:Color
下面我们来认识一下这些绘制元素(不需要全部记住,用的时候会找就行):
1.画布:Canvas
用于记录图形操作的界面。
Canvas 对象用于创建 Picture 对象,这些对象本身可以与 SceneBuilder 一起使用来构建 Scene。然而,在正常使用中,这一切都由框架处理。
画布具有应用于所有操作的当前转换矩阵。最初,变换矩阵是恒等变换。可以使用 translate、scale、rotate、skew 和 transform 方法修改它。
画布还有一个当前剪辑区域,该区域适用于所有操作。最初,剪辑区域是无限的。可以使用 clipRect、clipRRect 和 clipPath 方法修改它 。
可以使用由 save、saveLayer 和 restore 方法管理的堆栈来保存和恢复当前的变换和剪辑。
画布裁剪:
-
void clipPath(Path path, {bool doAntiAlias = true})
将剪辑区域缩小到当前剪辑和给定 Path 的交点。
-
void clipRect(Rect rect, {ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true})
将剪辑区域缩小到当前剪辑和给定矩形的交点。
-
void clipRRect(RRect rrect, {bool doAntiAlias = true})
将剪辑区域缩小到当前剪辑和给定圆角矩形的交点。
画布绘制:
-
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
根据给定的 PointMode 绘制一系列点。
-
void drawLine(Offset p1, Offset p2, Paint paint)
使用给定的颜料在给定的点之间画一条线。线条被描边,此调用将忽略 paint.style 的值。
-
void drawRect(Rect rect, Paint paint)
使用给定的Paint绘制一个矩形。矩形是填充还是描边(或两者)由 paint.style 控制。
-
void drawOval(Rect rect, Paint paint)
绘制一个轴对齐的椭圆,用给定的 Paint 填充给定的轴对齐矩形。椭圆是填充还是描边(或两者)由 paint.style 控制。
-
void drawCircle(Offset c, double radius, Paint paint)
以第一个参数给出的点为中心绘制一个圆,其半径由第二个参数给出,Paint 在第三个参数中给出。圆圈是填充还是描边(或两者)由 paint.style 控制。
-
void drawImage(Image image, Offset p, Paint paint)
将给定的 Image 绘制到画布中,其左上角位于给定的 Offset。使用给定的 Paint 将图像合成到画布中。
-
void drawParagraph(Paragraph paragraph, Offset offset)
在给定的Offset处将给定 Paragraph 中的文本绘制到此画布中 。
-
void drawColor(Color color, BlendMode blendMode)
将给定的 Color 绘制到画布上,应用给定的 BlendMode,给定的颜色是源,背景是目标。
-
void drawPath(Path path, Paint paint)
使用给定的Paint绘制给定的 Path。
-
void drawPaint(Paint paint)
用给定的 Paint 填充画布。
-
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
为表示给定材质标高的 Path 绘制阴影。
-
void drawPicture(Picture picture)
将给定的图片绘制到画布上。要创建图片,请参阅 PictureRecorder。
画布变换:
-
void skew(double sx, double sy)
向当前变换添加轴对齐倾斜,第一个参数是围绕原点顺时针旋转的水平倾斜,第二个参数是围绕原点顺时针旋转的垂直倾斜。
-
void rotate(double radians)
向当前变换添加旋转,参数是顺时针弧度。
-
void scale(double sx, [double sy])
向当前变换添加轴对齐比例,按水平方向的第一个参数和垂直方向的第二个参数进行缩放。
-
void translate(double dx, double dy)
向当前变换添加平移,通过第一个参数水平移动坐标空间,通过第二个参数垂直移动坐标空间。
-
void transform(Float64List matrix4)
将当前变换乘以指定为列优先顺序的值列表的指定 4⨉4 变换矩阵。
画布状态:
-
void save()
在保存堆栈中保存当前变换和剪辑的副本。
-
void saveLayer(Rect bounds, Paint paint)
在保存堆栈上保存当前变换和剪辑的副本,然后创建一个新组,后续调用将成为其中的一部分。当稍后弹出保存堆栈时,该组将被展平为一个图层并应用给定paint 的 Paint.colorFilter 和 Paint.blendMode。
-
void restore()
如果有任何东西要弹出,则弹出当前保存堆栈。否则,什么都不做。
-
int getSaveCount()
返回保存堆栈上的项目数,包括初始状态。这意味着它为干净的画布返回 1,并且每次调用 save 和 saveLayer 都会增加它,并且每次匹配的恢复调用都会减少它。
2.画笔:Paint
在 Canvas 上绘图时使用的样式的描述。
Canvas 上的大多数 API 都采用 Paint 对象来描述用于该操作的样式。
Paint的属性:
- blendMode--绘制形状或合成图层时应用的混合模式
- color--颜色
- colorFilter--颜色滤镜
- filterQuality--滤镜的质量控制器
- imageFilter--图片滤镜
- invertColors--颜色是否反转
- isAntiAlias--是否对画布上绘制的线条和图像应用抗锯齿
- maskFilter--遮罩滤镜(例如:模糊)
- shader--着色器
- strokeCap--线帽类型,在绘制的线条末端放置的饰面类型
- strokeJoin--线接类型,在段之间的连接上放置的饰面类型
- strokeMiterLimit--在段上绘制斜接的限制
- strokeWidth--线宽
- style--画笔样式
- enableDithering--绘制图像时是否抖动输出
3.路径:Path
平面的一个复杂的一维子集。
一条路径由许多子路径和一个当前点组成。
子路径由各种类型的线段组成,例如直线、圆弧或贝塞尔曲线。子路径可以是开放的或封闭的,并且可以自相交。
封闭的子路径根据当前的 fillType 包围平面的(可能不连续的)区域。
在当前点最初是在原点。在每次向子路径添加段的操作之后,当前点被更新到该段的末尾。
路径可以使用 Canvas.drawPath 在画布上绘制,并且可以使用 Canvas.clipPath 创建剪辑区域。
常用方法:
-
void addPath(Path path, Offset offset, {Float64List? matrix4})
添加一个新的子路径,该子路径由给定的path偏移量组成offset。
-
void addRect ( Rect rect )
添加一个由四行组成的新子路径,这些行勾勒出给定的矩形。
-
void addOval(Rect oval)
添加一个新的子路径,该路径由一条曲线组成,该曲线形成填充给定矩形的椭圆。
-
void moveTo(double x, double y)
在给定坐标处开始一个新的子路径。
-
void lineTo(double x, double y)
从当前点到给定点添加一条直线段。
-
void conicTo(double x1, double y1, double x2, double y2, double w)
使用控制点 (x1,y1) 和权重 w,添加一条从当前点弯曲到给定点 (x2,y2) 的贝塞尔曲线段。如果权重大于1,则曲线为双曲线;如果权重等于 1,则为抛物线;如果小于 1,则为椭圆。
-
void relativeMoveTo(double dx, double dy)
在距当前点的给定偏移处开始新的子路径。
-
void relativeLineTo(double dx, double dy)
从当前点到距当前点给定偏移的点添加一条直线段。
-
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2)
使用距当前点偏移 (x1,y1) 处的控制点,添加从当前点弯曲到距当前点偏移 (x2,y2) 处的点的二次贝塞尔曲线段。
-
void relativeConicTo(double x1, double y1, double x2, double y2, double w)
添加一条贝塞尔曲线段,该曲线从当前点弯曲到距当前点偏移量 (x2,y2) 处的点,使用距当前点偏移量 (x1,y1) 处的控制点和权重 w。如果权重大于1,则曲线为双曲线;如果权重等于 1,则为抛物线;如果小于 1,则为椭圆。
-
void close()
关闭最后一个子路径,就像从当前点到子路径的第一个点画了一条直线。
-
void reset()
清除所有子路径的Path对象,将其返回到创建时的相同状态。在当前点复位到原点。
4.颜色:Color
ARGB 格式的不可变 32 位颜色值。
考虑一下 Flutter 标志的浅蓝绿色。它是完全不透明的,红色通道值为 0x42 (66),绿色通道值为 0xA5 (165),蓝色通道值为 0xF5 (245)。在颜色值的常见散列语法中,它将被描述为
#42A5F5
.以下是它可以构建的一些方法:
Color c = const Color(0xFF42A5F5); Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5); Color c = const Color.fromARGB(255, 66, 165, 245); Color c = const Color.fromRGBO(66, 165, 245, 1.0);
如果您遇到的问题
Color
是,您的颜色似乎没有上色,请检查以确保您指定的是完整的 8 个十六进制数字。如果仅指定6位,则假定前两位数字为零,这意味着完全透明:
Color c1 = const Color(0xFFFFFF); // fully transparent white (invisible) Color c2 = const Color(0xFFFFFFFF); // fully opaque white (visible)
也可以看看:
Colors,它定义了 Material Design 规范中的颜色。
Color 类最常用的方式就是直接使用构造函数进行一个颜色值的创建,除此之外,还有比较复杂的应用。如:通道的变换、混合模式的应用、着色器、过滤器等多种高级用法,后面用到时再进行介绍。
简单应用
画点和线
class Paper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
alignment: Alignment.center,
child: CustomPaint(
// 使用CustomPaint 背景画板
painter: MyPainter(),
),
);
}
}
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final Paint paintLine = Paint()
..color = Colors.black
..strokeWidth = 3;
// 绘制线
canvas.drawLine(Offset(-100, 0), Offset(100, 0), paintLine);
canvas.drawLine(Offset(0, 100), Offset(0, -100), paintLine);
// 创建画笔
final Paint paintPoint = Paint()..color = Colors.red;
// 绘制圆点
canvas.drawCircle(Offset(0, 0), 10, paintPoint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
效果:
使用 Path 画一个五角星
class MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.red
..strokeWidth = 3
..style = PaintingStyle.stroke;
Path path = Path();
// 路径移动至起点
path.moveTo(0,-100);
// 连接各个端点
path.lineTo(59,81);
path.lineTo(-95,-31);
path.lineTo(95,-31);
path.lineTo(-59,81);
path.lineTo(0,-100);
// 绘制路径
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
通过给 Text 的 forground 添加一个 Paint 来实现文字描边的效果
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
alignment: Alignment.center,
child: Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
// 边框文字
Text(
'BillionBottle',
style: TextStyle(
fontSize: 100,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
// 通过Paint实现文字描边
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6
..color = Colors.blue[700],
),
),
// 填充文字,两层绘制叠加,完成描边效果
Text(
'BillionBottle',
style: TextStyle(
fontSize: 100,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
],
),
),
);
}
效果:
使用图片着色器 ImageShader 给描边加上效果
图片着色器功能强大。它能让画笔自带图片进行绘制,和 Photoshop 中的笔刷有些相像。构造方法中需要传入
ui.Image
对象、水平、竖直排列模式及变换矩阵 Float64List。
class Paper extends StatefulWidget {
@override
_PaperState createState() => _PaperState();
}
class _PaperState extends State<Paper> with SingleTickerProviderStateMixin {
ui.Image _image;
@override
void initState() {
super.initState();
// 资源图片需要提前加载
loadImage().then((value) {
setState(() {
_image = value;
});
});
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
alignment: Alignment.center,
child: Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
// 边框文字
Text(
'BillionBottle',
style: TextStyle(
fontSize: 120,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = 14
..color = Colors.black
// 使用图片着色器将资源图片绘制到边框文字的前景色上
..shader = ImageShader(
_image,
TileMode.repeated,
TileMode.repeated,
Float64List.fromList([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
])),
),
),
// 填充文字,两层绘制叠加,完成描边效果
Text(
'BillionBottle',
style: TextStyle(
fontSize: 120,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
color: Colors.white,
),
),
],
),
),
);
}
}
// 读取 assets 中的图片
Future<ui.Image> loadImage() async {
ByteData byteData = await rootBundle.load('assets/images/test.jpg');
ui.Codec codec = await ui.instantiateImageCodec(byteData.buffer.asUint8List());
ui.FrameInfo frameInfo = await codec.getNextFrame();
return frameInfo.image;
}
资源图:
效果:
使用绘制知识实现一个波浪动效
波浪加载特效在日常开发中比较常见,如:
在客户端实现一个波浪效果,有多种多样的方式,除了gif的加载、动画等方式,还可以通过绘制贝塞尔曲线来实现。
相信很多同学都知道“贝塞尔曲线”这个词,我们在很多地方都能经常看到。但是,可能并不是每位同学都清楚地知道,到底什么是“贝塞尔曲线”,又是什么特点让它有这么高的知名度。
贝塞尔曲线的数学基础是早在 1912 年就广为人知的伯恩斯坦多项式。但直到 1959 年,当时就职于雪铁龙的法国数学家 Paul de Casteljau 才开始对它进行图形化应用的尝试,并提出了一种数值稳定的 de Casteljau 算法。然而贝塞尔曲线的得名,却是由于 1962 年另一位就职于雷诺的法国工程师 Pierre Bézier 的广泛宣传。他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。
正是因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域迅速得到了广泛的应用。不仅如此,在计算机图形学领域,尤其是矢量图形学,贝塞尔曲线也占有重要的地位。今天我们最常见的一些矢量绘图软件,如 Flash、Illustrator、CorelDraw 等,无一例外都提供了绘制贝塞尔曲线的功能。甚至像 Photoshop 这样的位图编辑软件,也把贝塞尔曲线作为仅有的矢量绘制工具(钢笔工具)包含其中。
贝塞尔曲线的具体公式,这里不做讲解,我们直接在 Flutter 中来实现一条简单的贝塞尔曲线:
Path path = Path();
// 路径对象
Paint paint = Paint()// 画笔对象
..color = Colors.orange// 颜色
..style = PaintingStyle.stroke// 线条
..strokeWidth = 2;// 线宽
// 给path设置由当前点(0,0)控制点(0,80)终点(100,100)三个点组成的二次贝塞尔曲线
path.quadraticBezierTo(0,80,100,100);
// 绘制
canvas.drawPath(path, paint);
效果(为了方便查看点位,背景绘制了坐标系):
绘制波浪
// 波长
final double waveWidth = 80;
// 振幅
final double waveHeight = 40;
@override
void paint(Canvas canvas, Size size) {
// 画布起始点移动到屏幕中心点
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
// 设置画笔
Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill// 这里改为填充模式,不再是线条
..strokeWidth = 2;
// 路径
Path path = Path();
// 绘制贝塞尔曲线 相对位移方法
// 使用距当前点偏移 (x1,y1) 处的控制点,添加从当前点弯曲到距当前点偏移 (x2,y2) 处的点的二次贝塞尔曲线段。
// 对应的点位为起始点(0,0)控制点(40,-80)终点(80,0)三个点,此时当前点为(80,0)
path.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
// 第二条曲线,对应的点位为起始点(80,0)控制点(40,80)终点(160,0)三个点
path.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
// 绘制
canvas.drawPath(path, paint);
}
效果:
绘制连续的波,经过指定边框内:
// 设置画笔
Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill
..strokeWidth = 2;
// 保存画布状态
canvas.save();
Path path = Path();
// 第一组波浪
path.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
// 第二组波浪
path.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
// 当前结束点(x,y)与(x+0,y+100)进行连接
path.relativeLineTo(0, 100);
// 当前结束点(x,y)与(x-80*2*2,y)进行连接
path.relativeLineTo(-waveWidth*2 * 2.0, 0);
// 路径关闭
path.close();
// 绘制
canvas.drawPath(path, paint);
// 恢复画布
canvas.restore();
// 矩形边框
Path pathRect = Path();
pathRect.addRect(Rect.fromLTWH(160,-100, 160, 200));
// 设置矩形边框画笔
Paint paintCircle = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2;
// 绘制矩形
canvas.drawPath(pathRect, paintCircle);
效果:
接下来通过添加动画和裁剪就可以展示初级的波浪效果了:
Flutter中的动画本质就是在数据不断变化时,让画布随之重绘,达到连续的视觉效果。根据这个原理,我们通过对波浪X坐标的不断重复右移,然后进行重绘,使之产生运动的效果。
这里我们主要使用 AnimationController 这个类
AnimationController 的构造器中需要传入
TickerProvider
对象可以将 State 对象混入
SingleTickerProviderStateMixin
来成为该对象
lowerBound
是运动的下限,upperBound
是运动的上限,duration
是运动时长。
class _PaperState extends State<Paper2> with SingleTickerProviderStateMixin {
// 动画控制器 x轴
AnimationController _controllerX;
@override
void initState() {
super.initState();
// 设置动画控制器参数,运动时长为600ms
_controllerX = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)..repeat();// 重复模式
}
@override
void dispose() {
_controllerX.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(_controllerX),
),
);
}
}
class MyPainter extends CustomPainter {
// 波长
final double waveWidth = 80;
// 振幅
final double waveHeight = 40;
//x轴动画
final Animation<double> repaintX;
const MyPainter(this.repaintX):super(repaint: repaintX);
@override
void paint(Canvas canvas, Size size) {
// 画布移动屏幕中央
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
// 画布保存状态
canvas.save();
// 根据进度修改画布x轴的位置,然后不断重绘进行动效的实现
canvas.translate(2 * waveWidth * repaintX.value, 0);
// 保存画布状态
Path path = Path();
// 第一组波浪
path.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
// 第二组波浪
path.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
// 当前结束点(x,y)与(x+0,y+60)进行连接
path.relativeLineTo(0, 60);
// 当前结束点(x,y)与(x-80*2*2,y)进行连接
path.relativeLineTo(-waveWidth*2 * 2.0, 0);
// 路径关闭
path.close();
// 绘制
canvas.drawPath(path, paint);
// 恢复画布
canvas.restore();
// 矩形边框
Path pathRect = Path();
pathRect.addRect(Rect.fromLTWH(160,-140, 160, 200));
// 设置矩形边框画笔
Paint paintCircle = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2;
// 绘制矩形
canvas.drawPath(pathRect, paintCircle);
}
// 动画进度未完成就进行重绘
@override
bool shouldRepaint(MyPainter oldDelegate) => oldDelegate.repaintX != repaintX;
}
效果:
添加裁剪,让波浪只在指定区域内显示。
// 添加裁剪
canvas.clipRect((Rect.fromCenter(center: Offset( waveWidth*3, -60),width: waveWidth*2,height: 200.0)));
效果:
如果想让y轴方向也进行移动,我们可以新建一个y轴方响的动画控制器
class _PaperState extends State<Paper2> with SingleTickerProviderStateMixin {
// 动画控制器 x轴
AnimationController _controllerX;
// 动画控制器 y轴
AnimationController _controllerY;
@override
void initState() {
super.initState();
// 设置x轴动画控制器参数,运动时长为600ms
_controllerX = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
)..repeat();//重复模式
// 设置y轴动画控制器参数,运动时长为5000ms
_controllerY = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
)..repeat();//重复模式
}
@override
void dispose() {
_controllerX.dispose();
_controllerY.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
painter: MyPainter(_controllerX,_controllerY),
),
);
}
}
class MyPainter extends CustomPainter {
// 波长
final double waveWidth = 80;
// 振幅
final double waveHeight = 40;
// x轴动画
final Animation<double> repaintX;
// y轴动画
final Animation<double> repaintY;
const MyPainter(this.repaintX,this.repaintY);
@override
void paint(Canvas canvas, Size size) {
...
// 根据进度修改画布x轴和y轴的位置,然后不断重绘进行动效的实现
canvas.translate(2 * waveWidth * repaintX.value, -200*repaintY.value);
...
}
@override
bool shouldRepaint(MyPainter oldDelegate) => true;
}
这个时候运行会遇到一个错误
意思是当你的 state with SingleTickerProviderStateMixin 的状态下只能使用单个的动画控制器,具体 TickerProviderStateMixin 的原理这里不深究,我们根据提示做一些修改,并完善波浪效果。
class Paper2 extends StatefulWidget {
@override
_PaperState createState() => _PaperState();
}
class _PaperState extends State<Paper2> with TickerProviderStateMixin {
// 动画控制器 x轴
AnimationController _controllerX;
// 动画控制器 y轴
AnimationController _controllerY;
@override
void initState() {
super.initState();
_controllerX = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)..repeat();
_controllerY = AnimationController(
duration: const Duration(milliseconds: 5000),
vsync: this,
)..repeat();
}
@override
void dispose() {
super.dispose();
_controllerX.dispose();
_controllerY.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
// 使用CurveTween动画曲线来控制x轴运动速率
painter: MyPainter(CurveTween(curve: Curves.linear).animate(_controllerX), _controllerY),
),
);
}
}
class MyPainter extends CustomPainter {
// 波长
final double waveWidth = 80;
// 振幅
final double waveHeight = 10;
final Animation<double> repaintX;
final Animation<double> repaintY;
const MyPainter(this.repaintX, this.repaintY) : super(repaint: repaintY);
@override
void paint(Canvas canvas, Size size) {
// 调整画布起始为屏幕中心
canvas.translate(ScreenUtils.screenWidth / 2, ScreenUtils.screenHeight / 2);
// 添加裁剪
canvas.clipRect(
(Rect.fromCenter(center: Offset(waveWidth, -60), width: waveWidth * 2, height: 200.0)));
// 设置画笔
Paint paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 2;
Path path = Path();
// 添加3组波浪路径
path.relativeQuadraticBezierTo(waveWidth / 2, -waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, -waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, -waveHeight * 2, waveWidth, 0);
path.relativeQuadraticBezierTo(waveWidth / 2, waveHeight * 2, waveWidth, 0);
// 当前结束点(x,y)与(x+0,y+300)进行连接
path.relativeLineTo(0, 300);
// 当前结束点(x,y)与(x-80*3*2,y)进行连接
path.relativeLineTo(-waveWidth * 3 * 2.0, 0);
canvas.save();
// 绘制背景波浪 速度比前景快一倍,让波浪更有动感
canvas.translate(-4 * waveWidth * repaintX.value, -220 * repaintY.value);
canvas.drawPath(path, paint..color = Colors.orange.withAlpha(88));
canvas.restore();
canvas.save();
// 绘制前景波浪
canvas.translate(-2 * waveWidth * repaintX.value, -220 * repaintY.value);
canvas.drawPath(path, paint..color = Colors.red);
canvas.restore();
// 矩形边框
Path pathRect = Path();
pathRect.addRect(Rect.fromLTWH(0, -160, 160, 200));
// 设置矩形边框画笔
Paint paintCircle = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 2;
// 绘制矩形
canvas.drawPath(pathRect, paintCircle);
}
// y轴动画没走完就继续重绘
@override
bool shouldRepaint(MyPainter oldDelegate) => oldDelegate.repaintY != repaintY;
}
效果:
这样就比较符合我们印象中的波浪 loading 效果了,我们可以把他放进不同的形状里,实现不同的加载 loading 动画,接下来我们将其应用到我们的 Logo 里面。
@override
void paint(Canvas canvas, Size size) {
// 调整位置
canvas.translate(0, ScreenUtils.screenHeight / 2 + 100);
// 保存画布状态
canvas.save();
// 调整位置
canvas.translate(0, -320);
// 绘制红色文字放置底层
_drawTextWithParagraph(canvas, TextAlign.center, Colors.red);
// 恢复画布状态
canvas.restore();
// 矩形边框
Path pathRect = Path();
pathRect.addRect((Rect.fromCenter(
center: Offset(waveWidth * 5, -100), width: waveWidth * 8.5, height: 140.0)));
// 设置画笔
Paint paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 2;
Path path = Path();
// 制作背景波浪路径 速度比前景快一倍,让波浪更有动感
path.moveTo(-4 * waveWidth * repaintX.value, -220 * repaintY.value);
// 添加波浪路径
for (double i = -waveWidth; i < ScreenUtils.screenWidth; i += waveWidth) {
path.relativeQuadraticBezierTo(waveWidth / 4, -waveHeight, waveWidth / 2, 0);
path.relativeQuadraticBezierTo(waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
path.relativeLineTo(0, 500);
path.relativeLineTo(-waveWidth * 6 * 2.0, 0);
// 合并路径,取波浪和矩形边框的交集
var combine = Path.combine(PathOperation.intersect, pathRect, path);
// 绘制
canvas.drawPath(combine, paint..color = Colors.orange.withAlpha(88));
// 路径还原
path.reset();
// 制作前景波浪路径
path.moveTo(-2 * waveWidth * repaintX.value, -220 * repaintY.value);
// 添加波浪路径
for (double i = -waveWidth; i < ScreenUtils.screenWidth; i += waveWidth) {
path.relativeQuadraticBezierTo(waveWidth / 4, -waveHeight, waveWidth / 2, 0);
path.relativeQuadraticBezierTo(waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
path.relativeLineTo(0, 500);
path.relativeLineTo(-waveWidth * 6 * 2.0, 0);
path.close();
// 合并路径,取波浪和矩形边框的交集
var combine1 = Path.combine(PathOperation.intersect, pathRect, path);
// 绘制
canvas.drawPath(combine1, paint..color = Colors.red);
// 画布剪切
canvas.clipPath(combine1);
// 调整位置
canvas.translate(0, -320);
// 绘制与波浪交集的白色文字
_drawTextWithParagraph(canvas, TextAlign.center, Colors.white);
}
// 绘制文字
void _drawTextWithParagraph(Canvas canvas, TextAlign textAlign, Color colors) {
var builder = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: textAlign,
fontSize: 40,
textDirection: TextDirection.ltr,
maxLines: 1,
));
builder.pushStyle(
ui.TextStyle(
fontSize: 120,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
color: colors,
),
);
builder.addText("BillionBottle");
ui.Paragraph paragraph = builder.build();
paragraph.layout(ui.ParagraphConstraints(width: 800));
canvas.drawParagraph(paragraph, Offset(0, 150));
}
效果:
总结:
本文主要介绍了在 Flutter 绘制中,使用画笔、画笔、路径等元素,通过位移、变换、裁剪以及滤镜效果来实现简单的效果。
除了本文的简单展示,通过更加深入的了解和学习,Flutter 绘制还可以创造出更多的可能性。虽然各种社区里已经有很多比较完善的开源组件,但亲手造一次轮子能让我们对相关知识有更加清晰的认知。很多炫酷的效果正是从第一个点、第一条线绘制开始的,九层之台,起于累土,与君共勉。