本文是Flutter中Canvas和CustomPaint API的使用实例。
首先看一下咱们要实现的效果:
结合动图演示,列出最终目标以下:git
运用的主要技术点:
Canvas和CustomPaint API。github
运行平台:
Android、iOScanvas
首先拆解前文中所列出的6个实现目标,显而易见,要实现它们,咱们须要:dom
接下来,咱们逐步实现功能拆解中所列举的6个具体功能。async
随机颜色生成器在程序启动、单击屏幕和自动变色中使用。
在Flutter中,咱们能够经过Color类对红、绿、蓝和透明度分别定义,来定义某个惟一的颜色,数值范围是0-255。对于透明度,0表示彻底透明,255表示彻底不透明。
对于随机数值,咱们使用Random类生成0-255之间的随机整数。
随机颜色生成器则主要使用上述两个类来实现,具体代码片断以下:ide
Color _color = Color.fromARGB(0, 0, 0, 0); // 改变小球颜色 void changeColor() { _color = Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255),Random().nextInt(255)); }
随机位置生成器在程序启动时使用。
要生成随机位置,方法依然是使用Random类,但要注意随机值范围。一般咱们须要小球出现的位置在屏幕内,所以,咱们须要生成两次随机数,分别表示小球初始位置的x和y轴坐标。坐标值分别小于屏幕横向尺寸和纵向尺寸。固然,它们都要大于0。
另外,咱们还须要分别获取屏幕的宽高。
所以,具体代码实现以下:函数
[获取屏幕宽高]布局
double screenX, screenY; @override Widget build(BuildContext context) { screenX = MediaQuery.of(context).size.width; screenY = MediaQuery.of(context).size.height; ... }
[生成随机位置]ui
double _x = 0, _y = 0; // 生成小球初始位置和大小 void generateBall() { _x = Random().nextDouble() * screenX; _y = Random().nextDouble() * screenY; }
随机尺寸生成器在程序启动时使用。
完成了以前两种随机值的生成,到了尺寸这里,就很轻车熟路了。因为随机尺寸和随机位置都在程序启动时调用,且操做对象都是小球,咱们将其实现都放在generateBall()方法中。最终代码以下:
double _x = 0, _y = 0, _size = 0; // 生成小球初始位置和大小 void generateBall() { _size = Random().nextDouble() * (screenY - screenX).abs(); _x = Random().nextDouble() * screenX; _y = Random().nextDouble() * screenY; }
要在界面上绘制小球,咱们须要使用CustomPaint组件。而CustomPaint组件须要一个CustomPainter实例。小球的绘制工做主要在继承了CustomPainter的类中。咱们直接看代码:
import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; class Ball extends CustomPainter { Paint _paint; double _x, _y, _size; Ball(double x, double y, double size, Color color) { _paint = new Paint(); _paint.isAntiAlias = true; _paint.color = color; this._x = x; this._y = y; this._size = size; } @override void paint(Canvas canvas, Size size) { canvas.drawOval(Rect.fromCenter(center: Offset(_x, _y), width: _size, height: _size), _paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return oldDelegate != this; } }
经过阅读上面的代码,能够发现,整个Ball类除了构造方法外,只有两个override的方法,能够说是很简单了。
在构造方法中,咱们初始化了_paint对象,它是能够看作是“画笔”;
在paint()方法中,咱们调用canvas对象的drawOval方法画圆,表示小球。canvas能够看作是“画板”;
shouldRepaint()方法表示在刷新布局的时是否须要重绘,只有在返回true时会发生重绘,这里咱们让程序自行判断就能够了。
咱们将上述代码保存为ball.dart备用。
注意,这里面不管是位置、颜色还有尺寸,都没有写固定的值。是由于该类只负责“画圆”,而具体画什么样的圆,则交给该类的使用者来定义,也就是main.dart。
在main.dart中,咱们将App设置为全屏,并添加全屏尺寸的CustomPaint组件,组件内放置Ball对象。
@override Widget build(BuildContext context) { screenX = MediaQuery.of(context).size.width; screenY = MediaQuery.of(context).size.height; return Scaffold( body: GestureDetector( child: Container( width: double.infinity, height: double.infinity, child: CustomPaint(painter: Ball(_x, _y, _size, _color))), onTap: () { // 改变小球颜色 changeColor(); }, onDoubleTap: () { // 暂停/恢复移动 _keep_move = !_keep_move; }, onLongPress: () { // 自动改变小球颜色 _auto_change_color = !_auto_change_color; }, )); }
上述代码中,GestureDetector组件负责接收用户点击事件,其中的_keep_move、_auto_change_color都是布尔类型变量,是小球移动和自动变色功能的开关。
接下来,咱们在initState()方法中调用以前的随机位置生成器、随机尺寸生成器和随机颜色生成器,赋值_x、_y、_size和_color。
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { generateBall(); changeColor(); calculateMoveAngle(); startMove(); }); }
这里面,calculateMoveAngle()和startMove()方法分别对应初始运动方向生成器以及开始运动并按期更新UI的方法。除了这两个方法外,若是如今运行程序的话,应该能够看到一个静态的小球出如今屏幕上了,而且随着每次从新运行程序,小球的样式和位置都将发生变化。
接下来,咱们就来让小球动起来吧!
要让小球准确无误地运动,咱们须要遵循如下步骤:首先生成一个随机的运动方向;而后以60FPS的频率,每次在运动方向上前进5个像素的步长(固然,你能够自定义);最后还要注意边界断定,在小球到达屏幕边缘时正确转向。
下面咱们逐个实现。
既然是随机方向,那么平面上360度范围内任何一个角度都有可能。所以,咱们这里须要先生成0-360范围内的值。而后根据三角函数和运动方向的速度,计算出横、纵坐标的速度。其实很简单,就是勾股定理。
double _step_x, _step_y, _angle; // 计算小球初始移动角度(方向) void calculateMoveAngle() { _angle = Random().nextDouble() * 360; _step_x = sin(_angle) * _speed; _step_y = cos(_angle) * _speed; }
咱们这里把运动速度(_speed)看作是三角形的斜边,横、纵坐标的移动速度(_step_x、_step_y)看作是三角形的直角边便可。没记错的话,都是初中几何知识,不会很难理解。
前文说到,咱们将以60FPS的刷新率更新界面,这也就意味着,每隔大约16ms刷新一次小球位置。由于只有小球的运动,才能让人感到界面在“更新”。这一步骤,咱们用到Timer类。并将更新器在initState()方法中调用,以便程序启动后,小球即刻运动,也就是前文代码中见到的startMove()方法。
// 开始移动 void startMove() { Timer.periodic(Duration(milliseconds: 16), (timer) { moveBall(); setState(() {}); }); } // 小球移动 void moveBall() { _x += _step_x; _y += _step_y; }
到此为止,小球已经能够开始沿着某个随机方向移动了。但很快,它将移出屏幕。
显然,小球每前进一步,都要作屏幕边界断定,以防小球移出屏幕范围。而边界断定在moveBall()方法中实现彷佛是最恰当的。
咱们能够轻松地总结出小球移动的规律,当小球移动到屏幕边缘时,咱们只需让其反向运动便可。好比,小球以3的速度移动并接触屏幕的右边缘,接下来,仍以3的速度移动并朝向屏幕的左边缘。
水平方向如此,垂直方向亦如此。
所以,咱们的边界断定逻辑以下:
// 带有便捷断定的小球移动 void moveBall() { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y; }
最后,配合用户手势及相关的布尔变量,在每次刷新小球位置时实现变色和暂停移动。
继续修改moveBall()方法:
// 带有便捷断定的小球移动 void moveBall() { if (_keep_move) { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y; if (_auto_change_color) { changeColor(); } } }
到此,程序所有实现完成。
下面放上完整的main.dart代码:
import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'ball.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { SystemChrome.setEnabledSystemUIOverlays([]); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: BounceBall(), ); } } class BounceBall extends StatefulWidget { @override _BounceBallState createState() => _BounceBallState(); } class _BounceBallState extends State<BounceBall> { final double _speed = 5; double _x = 0, _y = 0, _size = 0; double _step_x, _step_y, _angle; Color _color = Color.fromARGB(0, 0, 0, 0); bool _auto_change_color = false; bool _keep_move = true; double screenX, screenY; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { generateBall(); changeColor(); calculateMoveAngle(); startMove(); }); } @override Widget build(BuildContext context) { screenX = MediaQuery.of(context).size.width; screenY = MediaQuery.of(context).size.height; return Scaffold( body: GestureDetector( child: Container( width: double.infinity, height: double.infinity, child: CustomPaint(painter: Ball(_x, _y, _size, _color))), onTap: () { // 改变小球颜色 changeColor(); }, onDoubleTap: () { // 暂停/恢复移动 _keep_move = !_keep_move; }, onLongPress: () { // 自动改变小球颜色 _auto_change_color = !_auto_change_color; }, )); } // 开始移动 void startMove() { Timer.periodic(Duration(milliseconds: 16), (timer) { moveBall(); setState(() {}); }); } // 改变小球颜色 void changeColor() { _color = Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255)); } // 生成小球初始位置和大小 void generateBall() { _size = Random().nextDouble() * (screenY - screenX).abs(); _x = Random().nextDouble() * screenX; _y = Random().nextDouble() * screenY; } // 计算小球初始移动角度(方向) void calculateMoveAngle() { _angle = Random().nextDouble() * 360; _step_x = sin(_angle) * _speed; _step_y = cos(_angle) * _speed; } // 带有便捷断定的小球移动 void moveBall() { if (_keep_move) { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y; if (_auto_change_color) { changeColor(); } } } }
让咱们一块儿让这个程序跑起来吧!