本文是Flutter中Canvas和CustomPaint API的使用实例。
首先看一下我们要实现的效果:

结合动图演示,列出最终目标如下:

  1. 在程序运行后,显示一个小球;
  2. 每次程序启动后,小球的样式均发生随机性变化,体现在大小、颜色和位置三点;
  3. 小球运行的规律参考桌球或三维弹球游戏;
  4. 单击屏幕,小球变色;
  5. 双击屏幕,小球暂停/恢复运动;
  6. 长按屏幕,小球开始/停止自动变色。

运用的主要技术点:
Canvas和CustomPaint API。

运行平台:
Android、iOS

源码地址:
Github
Gitee


功能拆解

首先拆解前文中所列出的6个实现目标,显而易见,要实现它们,我们需要:

  1. 随机颜色生成器;
  2. 随机位置生成器;
  3. 随机尺寸生成器;
  4. 小球绘制逻辑;
  5. 小球运动逻辑:
    • 边界判定;
    • 初始运动方向生成器;
    • 定向移动位置更新器。
  6. 用户手势监听器。

功能实现

接下来,我们逐步实现功能拆解中所列举的6个具体功能。

随机颜色生成器

随机颜色生成器在程序启动、单击屏幕和自动变色中使用。
在Flutter中,我们可以通过Color类对红、绿、蓝和透明度分别定义,来定义某个唯一的颜色,数值范围是0-255。对于透明度,0表示完全透明,255表示完全不透明。
对于随机数值,我们使用Random类生成0-255之间的随机整数。
随机颜色生成器则主要使用上述两个类来实现,具体代码片段如下:

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;...
}

[生成随机位置]

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;}@overridevoid paint(Canvas canvas, Size size) {canvas.drawOval(Rect.fromCenter(center: Offset(_x, _y), width: _size, height: _size), _paint);}@overridebool 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 {@overrideWidget 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;@overridevoid initState() {super.initState();WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {generateBall();changeColor();calculateMoveAngle();startMove();});}@overrideWidget 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();}}}
}

让我们一起让这个程序跑起来吧!

用Flutter做桌上弹球?聊聊绘图(CanvasCustomPaint)API相关推荐

  1. ios调用restful接口_做iOS上最好的REST API测试App

    对于Web开发者和移动应用开发者来说,少不了和REST API打交道.何为REST API,维基百科是这么解释的(https://zh.wikipedia.org/wiki/REST)REST(英文: ...

  2. awt绘图应用--桌上弹球

    核心代码 package com.awt; import java.util.Random; import java.awt.*; import javax.swing.*; import java. ...

  3. 开始用Flutter做游戏吧

    一点点基础 游戏主循环(GameLoop) 游戏主循环是游戏的核心,计算机一次又一次运行的一组指令,用通俗的话来说,如果游戏有生命,那么游戏主循环就是游戏的心跳. 同时为了更好的理解游戏主循环,还需要 ...

  4. ChatGPT4已经来了,30秒做一个弹球游戏!

    前两周写了关于ChatGPT的文章, 折腾了一晚!终于开通了ChatGPT plus版本! ChatGPT_Plus的功能有多强!3分钟写一个贪吃蛇游戏! 然后果断的注册了Plus, 事实证明这个决定 ...

  5. 两个半月的业余时间用Flutter做了个app-技术篇

    技术背景: 做了几年前端,会用node 写这篇文章是自己对这段时间做个技术总结,记录一些开发过程中比较难以解决的问题和经验,同时希望对Flutter感兴趣但还在观望的同学加入Flutter开发,简单易 ...

  6. 一文告诉你:闲鱼是怎么面向Flutter做技术体验升级的?

    但是随着业务快速迭代,开发者往往会疏忽大意,写出低性能的代码.理想情况是,通过性能分析工具自动定位到问题代码,及时修改.Flutter官方提供了Devtools性能分析工具,它的timeline界面可 ...

  7. MATLAB | 我也做了一套绘图配色可视化模板

    看到包括「阿昆的科研日常」等很多大佬都做了一套自己的模板用来分享一些科研绘图的配色,我也试着做了一套离散颜色版的(连续颜色敬请期待),以后遇到好看的配色可能会用这套模板分享,展示一下用ggsci库中的 ...

  8. 结合动画与键盘交互,做一个弹球小游戏

    小球在窗体范围内运动,撞到除底边外的另外三个边缘就反弹一次.如果,小球撞到底边,那么游戏结束.玩家可以通过控制在底边上的一个挡板,让小球撞击到挡板上而反弹,阻止小球撞到底边. 1. 运动的小球 创建一 ...

  9. 手把手教你用Flutter做炫酷动画

    导读:随着技术的发展,很多网页开发技术都带有动画效果,比如淡入淡出.渐变.变大变小,等等.Flutter中的动画效果可以用酷炫来形容,这也是Flutter的一大特色.现代的应用程序不仅仅需要程序稳定. ...

最新文章

  1. LASSO与Item Response Theory模型中的隐变量选择
  2. java高效率素数算法_《Core Java》里给出的算法,效率比较高。 统计2000000以内的所有的素数。...
  3. 【Luogu3383】【模板】线性筛素数
  4. 登顶Github趋势榜,非监督GAN算法U-GAT-IT大幅改进图像转换效果
  5. 富士康海外工厂遭黑客攻击 被勒索1804枚比特币
  6. javamail 解码 base64 html格式邮件_Spring整合javaMail
  7. jquery基础知识(一)
  8. eclipse目录改名,子目录及JAVA文件同步更改
  9. matlab画一个点电荷电场线,matlab画点电荷电场线
  10. Android 动画
  11. Android破解九宫格密码
  12. java高性能rpc,企业级rpc,zk调度,负载均衡,泛化调用一体的rpc服务框架
  13. 【PC工具】win10关闭自带杀毒软件,win10关闭安全软件方法
  14. java将uuid转换成大写,python生成大写32位uuid代码
  15. python内推群_重要!!!陌陌2020校招Python内推通道(含内推直链)
  16. includes的用法
  17. 2019年山东省第十届ACM程序设计竞赛 比赛总结
  18. 蜘蛛爬行html语言的顺序,SEO优化 蜘蛛的爬行规则以及让蜘蛛爬行的快速办法
  19. 一阶数字低通滤波器的实现
  20. 无MCU,灰常简单实用的按键电平保持电路!

热门文章

  1. 修改redis服务器的端口号,Centos7搭建redis,同一服务器启动两个端口的redis
  2. 如何用pscc 2018圆角插件mac版制作倒圆角
  3. 雨流计数法Matlab代码
  4. 最详尽的PyCharm 实用教程,值得一看!
  5. 赢在起跑线上,还不如赢在时间的管理上~~~
  6. linux的设备类型
  7. 【MySQL】如何使用SQL语句获取表结构和获取全部表名
  8. python anaconda下载包_anaconda下载_anaconda正式版下载_3DM单机
  9. 微信小程序中的本地存储
  10. vue项目打包部署在windows或linux服务器上