用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)

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

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

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

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

运行平台:Android、iOS

源码地址:
Github Gitee

功能拆解

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

  1. 随机颜色生成器;
  2. 随机位置生成器;
  3. 随机尺寸生成器;
  4. 小球绘制逻辑;
  5. 小球运动逻辑:

边界判定;
初始运动方向生成器;
定向移动位置更新器。

  1. 用户手势监听器。

功能实现

接下来,我们逐步实现功能拆解中所列举的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的文章就介绍到这了,更多相关Flutter桌上弹球内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 用Flutter做桌上弹球(绘图(Canvas&CustomPaint)API)

    本文是Flutter中Canvas和CustomPaint API的使用实例. 首先看一下我们要实现的效果: 结合动图演示,列出最终目标如下: 在程序运行后,显示一个小球: 每次程序启动后,小球的样式均发生随机性变化,体现在大小.颜色和位置三点: 小球运行的规律参考桌球或三维弹球游戏: 单击屏幕,小球变色: 双击屏幕,小球暂停/恢复运动: 长按屏幕,小球开始/停止自动变色. 运用的主要技术点:Canvas和CustomPaint API. 运行平台:Android.iOS 源码地址: Githu

  • Android编程之绘图canvas基本用法示例

    本文实例讲述了Android编程之绘图canvas基本用法.分享给大家供大家参考,具体如下: MainActivity的代码如下: package example.com.myapplication; import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedIns

  • JS前端绘图canvas模糊问题示例高清图解

    目录 缘起 模糊的原因 1.canvas 的大小和 css 的大小不一致 2.当绘制的东西不足 1px,会自动补足 1px 3.受到高清屏的影响 结语 缘起 模糊在 canvas 中应该算是个经典问题了,相信大家也曾经看过很多相关文章,但总是记不住,因为概念很多,描述的也不够明确,所以我就自己总结了一篇,刨去了复杂概念,顺带画了几张高清图,以此加深理解(我觉得画的贼好,记不住就来打我). 模糊的原因 总的来说模糊的原因大致可分为以下三点: 1.canvas 的大小和 css 的大小不一致 首先让

  • Android Canvas和Bitmap结合绘图详解流程

    目录 Rect/RectF Matrix Canvas Bitmap Rect/RectF 存储四个值的矩形类:左侧.顶部.右侧和底部.可用于直接在画布上绘制或仅用于存储要绘制的对象的大小.Rect和RectF类之间的区别在于 RectF 存储浮点值,而Rect类存储整数. private static Bitmap createDrawableBitmap(Drawable drawable) { int width = drawable.getIntrinsicWidth(); int he

  • Flutter手势密码的实现示例(附demo)

    目录 前言 开始 绘制圆点 绘制手势路径 组合9个圆点盘和手势路径 手势密码组件的使用 上传自定义组件到pub仓库 后记 前言 本篇记录的是使用Flutter完成手势密码的功能,大致效果如下图所示: 该手势密码的功能比较简单,下面会详细记录实现的过程,另外还会简单说明如何将该手势密码作为插件发布到pub仓库. 开始 实现上面的手势密码并不难,大致可以拆分成如下几部分来完成: 绘制9个圆点 绘制手指滑动的线路 合并以上两个部分 绘制圆点 我们使用面向对象的方式来处理9个圆点的绘制,每个圆点作为一个

  • 如何使用Flutter实现58同城中的加载动画详解

    前言 在应用中执行耗时操作时,为了避免界面长时间等待造成假死的现象,往往会添加一个加载中的动画来提醒用户,在58同城中也不例外,而且我们并没有使用系统默认的加载动画,而是制作了一个具有58特色的加载动画. 在本篇文章中,给大家分享下笔者使用Flutter实现58同城中加载动画的过程.先看一下加载动画的效果: 动画效果乍看比较复杂,难以看出端倪,其实我们可以先调慢动画的速度,这样能够比较清晰地分析出动画的流程. 动画的流程 动画由两个圆弧的动效组成,两个圆弧的起始点角度和扫过的弧度随着时间规律变化

  • JS前端使用Canvas快速实现手势解锁特效

    目录 前言 Demo 需要实现的功能 初始化数据和页面渲染 touchstart 手指开始触摸事件 touchmove 监听手指滑动事件 touchend 监听手指触摸结束事件 页面滚动处理 连接的两颗星星之间有其他星星时 前言 之前在公司开发活动项目的时候,遇到一个项目需求要让用户使用手势画星位图来解锁星座运势,一看设计稿,这不就是我们平时的手机屏幕解锁吗?于是上网搜了一些关于手势解锁的文章,没找到可以直接复用的,于是只能自己打开canvas教程,边学习边设计实现了这个功能,同时兼容了移动端和

  • CSS可以做的几个令你叹为观止的实例分享

    在我写"你未必知道的JavaScript和CSS交互的5种方法"一文时,人们对于JavaScript和CSS是如何重叠的感到惊讶.今天,我将重点强调你能用CSS完成的7种工作--不需要通过JavaScript或图片. CSS@supports 在使用一些浏览器可能没有的特性时,每一个优秀的前端开发者都需要进行特性测试.特性测试一直以来都是由JavaScript来做的,许多人使用Modernizr,一个由许多测试良好的案例构成的优秀实用工具,来做特性测试.一个新API:@supports

  • canvas学习之API整理笔记(一)

    其实canvas本身很简单,就是去学习它的API,多看实例,多自己动手练习,多总结.但是canvas的API实在是有点多,对于初学者来说,可能学到一半就止步不前了.我也有这种感觉,在学习的过程中,编写实例,用到了其中很多的属性和方法,但是回头来看的时候总觉得什么也没用.所以决定系统性的记录一下它常用到的API,方便以后查阅,也顺便造福一下大家. 开始之前 假设html代码中有一个canvas标签: <canvas id="canvas">你的浏览器不支持canvas!<

  • Android 通过onDraw实现在View中绘图操作的示例

    Android绘图操作,通过继承View实现,在onDraw函数中实现绘图.下面是一个简单的例子: 复制代码 代码如下: public class AndroidTest extends Activity {    /** Called when the activity is first created. */    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(save

随机推荐