canvas drawbitmap不出现_用Flutter做桌上弹球?聊聊绘图(Canvas&CustomPaint)API

2023-10-27

67c5e46acb1ccb855e8b503efa2ecdea.gif

本文是Flutter中Canvas和CustomPaint API的使用实例。


首先看一下列出最终目标:

  1. 在程序运行后,显示一个小球;

  2. 每次程序启动后,小球的样式均发生随机性变化,体现在大小、颜色和位置三点;

  3. 小球运行的规律参考桌球或三维弹球游戏;

  4. 单击屏幕,小球变色;

  5. 双击屏幕,小球暂停/恢复运动;

  6. 长按屏幕,小球开始/停止自动变色。

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

运行平台:
Android、iOS

源码地址:
请点击阅读原文


功能拆解

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

  1. 随机颜色生成器;

  2. 随机位置生成器;

  3. 随机尺寸生成器;

  4. 小球绘制逻辑;

  5. 小球运动逻辑:

  • 边界判定;

  • 初始运动方向生成器;

  • 定向移动位置更新器。

用户手势监听器。

功能实现

接下来,我们逐步实现功能拆解中所列举的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;@overrideWidget 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对象。
@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;        },    ));}

上述代码中,GestureDetector组件负责接收用户点击事件,其中的_keep_move、_auto_change_color都是布尔类型变量,是小球移动和自动变色功能的开关。
接下来,我们在initState()方法中调用之前的随机位置生成器、随机尺寸生成器和随机颜色生成器,赋值_x、_y、_size和_color。

@overridevoid 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();      }    }  }}

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

canvas drawbitmap不出现_用Flutter做桌上弹球?聊聊绘图(Canvas&CustomPaint)API 的相关文章

随机推荐

  • Container命令ctr,crictl的用法

    Container命令ctr crictl的用法 版本 ctr containerd io 1 4 3 containerd 相比于docker 多了namespace概念 每个image和container 都会在各自的namespace
  • 通过Vue.js的axios请求WFS数据并处理请求回来的XML文件

    前端小白的第一个博客 前言 这个是在GIS开发过程中遇到的一个小问题 因为里面包含了蛮多的知识点 故将其记录 废话不多说进入正文 正文 此次需要解决的问题是通过wfs接口来获取到一些需要的内容 然后以这些内容为基础进行一系列的操作 以下展示
  • 1.3远程控制及文件传输

    我们经常用的是Windows操作系统 又经常需要与Ubuntu进行文件传输 同时为了能在Windows上操作我们的Ubuntu 这里推荐一个文件传输和一个远程控制的程序 文件传输WinSCP 官方下载地址 https sourceforge
  • VsCode官网快速下载

    VsCode官网 以Win10下载为例 问题描述 下载时 发现速度很慢 甚至会没有下载速度 如下图 解决方法 右键复制这个下载链接 将其前半部分修改为vscode cdn azure cn 例如 原下载链接 https az764295 v
  • Codeforces 600C Make Palindrome 【贪心 找字典序最小回文串】

    一 题目概述 C Make Palindrome time limit per test 2 seconds memory limit per test 256 megabytes input standard input
  • 如何在Anaconda安装opencv,下面分享一下教程

    1 首先下载opencv安装包 下载地址 https download csdn net download qq 42375391 12333992 2 安装完成后 在Anaconda Prompt内使用pip install完整路径文件名
  • 算法:模拟思想算法

    文章目录 实现原理 算法思路 典型例题 替换所有问号 提莫攻击 N字型变换 外观序列 总结 本篇总结的是模拟算法 实现原理 模拟算法的实现原理很简单 就是依据题意实现题意的目的即可 考察的是你能不能实现题目题意的代码能力 算法思路 没有很明
  • openwrt 自动签到插件-食用指南

    目录 openwrt 自动签到插件下载 openwrt 插件安装需要的依赖 openwrt 插件安装 文件上传 openwrt 自动签到配置 设置详情 Cookie获取失败 解决方法 Charles 抓包获取Cookie openwrt 配
  • 计算机设备问题代码43,双击unknown device由于该设备有问题Windows已将其停止(代码 43)怎么办解决教程...

    金士顿U盘做的启动盘 8G 在别人的电脑上储存文件正常 但在自己的电脑上无法识别 在设备管理器中显示黄色叹号 属性显示 该设备存在问 题 windows已将其停止 代码43 本机win7系统 别人电脑为XP系统 已将 禁用 注册表 dos设
  • Linux 阻塞IO(等待队列)原理及架构

    一 阻塞操作 阻塞操作是指在执行折本操作时 若不能获得自愿 则挂起进程直到满足可操作性的条件后在进行操作 被挂起的进程进入休眠状态 被从调度器的运行队列移走 直到等待的条件被满足 假设recvfrom函数是一个系统调用 阻塞不是低效率 如果
  • vue -- 验证码

  • 25-python函数(低阶)

    一 函数的作用 函数的本质就是将一段具有独立功能的代码块整合到一个整体并命名 在需要的时候通过调用函数名完成某种需求 以提高代码的利用率 从而在稳定系统的同时减轻程序员的工作 二 函数的使用过程 函数使用分为两个步骤 先定义 后调用 定义函
  • Java设计模式之状态模式

    本文继续介绍23种设计模式系列之策略模式 何时使用 State模式在实际使用中比较多 适合 状态 的切换 因为我们经常会使用If else if else 进行状态切换 如果针对状态的这样判断切换反复出现 我们就要联想到是否可以采取Stat
  • Android相机-架构3

    目录 引言 1 Android相机的整体架构 2 相机 HAL 2 1 AIDL相机HAL 2 2 相机 HAL3 功能 3 HAL子系统 3 1 请求 3 2 HAL和相机子系统 3 2 1 相机的管道 3 2 2 使用 Android
  • TCP的拥塞控制(详解)

    在某段时间 若对网络中某一资源的需求超过了该资源所能提供的可用部分 网络性能就要变坏 这种情况就叫做网络拥塞 在计算机网络中数位链路容量 即带宽 交换结点中的缓存和处理机等 都是网络的资源 若出现拥塞而不进行控制 整个网络的吞吐量将随输入负
  • 【6 GoldenEye渗透笔记】

    1 前言 本文仅用于技术讨论与研究 不做任何导向 对于所有笔记中复现的这些终端 服务器或者实验环境 均为自行搭建的公开靶场 请勿在现实环境中模仿 操作 本文涉及到的工具仅就用到的方面做简要描述 如果想了解更详细的信息 请自行参阅其他技术资料
  • Java的多态性

    Java的多态性多态性严格来讲有两种描述形式 一 方法的多态性 1 方法的重载 同一个方法名称 会根据传入参数的类型及个数不同执行不同的方法体 2 方法的覆写 同一个方法名称 会根据子类的不同 实现不同的功能 二 对象的多态性 指的是发生在
  • 51Nod 2094 前缀和

    题目链接 https www 51nod com Challenge Problem html problemId 2094 include
  • unity3D简答题2

    游戏对象运动的本质 游戏对象运动的本质是对象Transform属性的变化 position决定位置 rotation决定旋转角度 请用三种方法以上方法 实现物体的抛物线运动 如 修改Transform属性 使用向量Vector3的方法 第一
  • canvas drawbitmap不出现_用Flutter做桌上弹球?聊聊绘图(Canvas&CustomPaint)API

    本文是Flutter中Canvas和CustomPaint API的使用实例 首先看一下列出最终目标 在程序运行后 显示一个小球 每次程序启动后 小球的样式均发生随机性变化 体现在大小 颜色和位置三点 小球运行的规律参考桌球或三维弹球游戏