本文是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;
} @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();
}
}
}
}

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

Flutter中的绘图(Canvas&CustomPaint)API的更多相关文章

  1. html5 canvas常用api总结(二)--绘图API

    canvas可以绘制出很多奇妙的样式和美丽的效果,通过几个简单的api就可以在画布上呈现出千变万化的效果,还可以制作网页游戏,接下来就总结一下和绘图有关的API. 绘画的时候canvas相当于画布,而 ...

  2. Android为TV端助力 转载:Android绘图Canvas十八般武器之Shader详解及实战篇(上)

    前言 Android中绘图离不开的就是Canvas了,Canvas是一个庞大的知识体系,有Java层的,也有jni层深入到Framework.Canvas有许多的知识内容,构建了一个武器库一般,所谓十 ...

  3. Android为TV端助力 转载:Android绘图Canvas十八般武器之Shader详解及实战篇(下)

    LinearGradient 线性渐变渲染器 LinearGradient中文翻译过来就是线性渐变的意思.线性渐变通俗来讲就是给起点设置一个颜色值如#faf84d,终点设置一个颜色值如#CC423C, ...

  4. HTML5之canvas基本API介绍及应用 1

    一.canvas的API: 1.颜色.样式和阴影: 2.线条样式属性和方法: 3.路径方法: 4.转换方法: 5.文本属性和方法: 6.像素操作方法和属性: 7.其他: drawImage:向画布上绘 ...

  5. Flutter 中渐变的高级用法

    Flutter 中渐变有三种: LinearGradient:线性渐变 RadialGradient:放射状渐变 SweepGradient:扇形渐变 看下原图,下面的渐变都是在此图基础上完成. Li ...

  6. [html5] 初识绘图canvas

    这个星期被调到别的项目组专门做了一会儿前端,没办法,人太少,我也只能硬着头皮上... 说起来,html5的canvas真的好用,可以画色块,可以嵌入图片,可以通过定位在图片上写字等等 举例如下 在ht ...

  7. 在Flutter中嵌入Native组件的正确姿势是...

    引言 在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择.本文希望向大家介绍AndroidView的使用方式 ...

  8. Flutter 中文文档网站 flutter.cn 正式发布!

    在通常的对 Flutter 介绍中,最耳熟能详的是下面四个特点: 精美 (Beautiful):充分的赋予和发挥设计师的创造力和想象力,让你真正掌控屏幕上的每一个像素. ** 极速 (Fast)**: ...

  9. 理解 Flutter 中的 Key

    概览 在 Flutter 中,大概大家都知道如何更新界面视图: 通过修改 Stata 去触发 Widget 重建,触发和更新的操作是 Flutter 框架做的. 但是有时即使修改了 State,Flu ...

随机推荐

  1. 为什么Spring Security看不见登录失败或者注销的提示

    有很多人在利用Spring Security进行角色权限设计开发时,一般发现正常登录时没问题,但是注销.或者用户名时,直接就回到登录页面了,在登录页面上看不见任何提示信息,如“用户名/密码有误”或“注 ...

  2. 动手实现一个简单的 rpc 框架到入门 grpc (上)

    rpc 全称 Remote Procedure Call 远程过程调用,即调用远程方法.我们调用当前进程中的方法时很简单,但是想要调用不同进程,甚至不同主机.不同语言中的方法时就需要借助 rpc 来实 ...

  3. day31 反射,内置方法,元类

    目录 一.反射 1 什么是反射 2 如何实现反射 二.内置方法 1 什么是内置方法 2 为什么要用内置方法 3 如何使用内置方法 3.1 str 3.2 del 三.元类 1 什么是元类 2 clas ...

  4. EF实现简单的增删改查

    1.在项目中添加ADO.NET实体数据模型: 2.接着根据提示配置数据库连接,配置完毕之后项目中生成了大致如下的内容(EF6.x): 其中TestData.tt中的Consumer,Stores是创建 ...

  5. Git操作(二)

    很久以前写的git入门,最近又巩固了一下Git分支操作,下面是我的一些整理. 1.分支与合并 #创建并切换到该分支 git checkout -b xxx #查看当前分支 git branch #进行 ...

  6. 数据结构中有关顺序表的问题:为何判断插入位置是否合法时if语句中用length+1,而移动元素的for语句中只用length?

    bool ListInsert(SqList &L,int i, ElemType e){ if(i<||i>L.length+) //判断i的范围是否有效 return fals ...

  7. git分支间切换注意点和bug分支的处理

    目录 备注: 知识点 记一次分支合并问题状况 从分支点开始,不同分支修改工作区的内容(不添加到暂存区和提交),切换分支,工作区的内容是一样的. 必须在提交或者暂存当前暂存区的状态后,再切换或合并分支 ...

  8. oracle 在物理机上添加磁盘操作

    物理机上添加磁盘操作 注意:1)物理机上添加磁盘操作,不涉及到start_udev的动作.2)磁盘分区的操作,需要谨慎进行,核准无误后再操作. (1)查看磁盘名称命名 # su - grid$ sql ...

  9. java大数据最全课程学习笔记(5)--MapReduce精通(一)

    目前CSDN,博客园,简书同步发表中,更多精彩欢迎访问我的gitee pages 目录 MapReduce精通(一) MapReduce入门 MapReduce定义 MapReduce优缺点 优点 缺 ...

  10. [spring] -- bean作用域跟生命周期篇

    作用域 singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的. prototype : 每次请求都会创建一个新的 bean 实例. request : 每一次HT ...