Flutter-实现物理小球碰撞效果

效果

在这里插入图片描述

引言

在Flutter应用中实现物理动画效果,可以大大提升用户体验。本文将详细介绍如何在Flutter中创建一个模拟物理碰撞的动画小球界面,主要代码实现基于集成sensors_plus插件来获取设备的加速度传感器数据。

准备工作

在开始之前,请确保在pubspec.yaml文件中添加sensors_plus插件:

dependencies:flutter:sdk: fluttersensors_plus: 4.0.2

然后运行flutter pub get命令来获取依赖。

代码结构

我们将实现一个名为PhysicsBallWidget的自定义小部件,主要包含以下几部分:

  • Ball类:表示每个球的基本信息。
  • BadgeBallConfig类:管理每个球的状态和行为。
  • PhysicsBallWidget类:主部件,包含球的逻辑和动画。
  • BallItemWidget类:具体显示每个球的小部件。
  • BallListPage类:测试页面,展示物理球动画效果。

Ball类

首先定义Ball类,用于表示每个球的基本信息,例如名称:

class Ball {final String name;Ball({required this.name});
}

BadgeBallConfig类

BadgeBallConfig类用于管理每个球的状态和行为,包括加速度、速度、位置等信息:

class BadgeBallConfig {final Acceleration _acceleration = Acceleration(0, 0);final double time = 0.02;late Function(Offset) collusionCallback;Size size = const Size(100, 100);Speed _speed = Speed(0, 0);late Offset _position;late String name;double oppositeAccelerationCoefficient = 0.7;void setPosition(Offset offset) {_position = offset;}void setInitSpeed(Speed speed) {_speed = speed;}void setOppositeSpeed(bool x, bool y) {if (x) {_speed.x = -_speed.x * oppositeAccelerationCoefficient;if (_speed.x.abs() < 5) _speed.x = 0;}if (y) {_speed.y = -_speed.y * oppositeAccelerationCoefficient;if (_speed.y.abs() < 5) _speed.y = 0;}}void setAcceleration(double x, double y) {_acceleration.x = x * oppositeAccelerationCoefficient;_acceleration.y = y * oppositeAccelerationCoefficient;}Speed getCurrentSpeed() => _speed;Offset getCurrentCenter() => Offset(_position.dx + size.width / 2,_position.dy + size.height / 2,);Offset getCurrentPosition() => _position;void inertiaStart(double x, double y) {if (x.abs() > _acceleration.x.abs()) _speed.x += x;if (y.abs() > _acceleration.y.abs()) _speed.y += y;}void afterCollusion(Offset offset, Speed speed) {_speed = Speed(speed.x * oppositeAccelerationCoefficient,speed.y * oppositeAccelerationCoefficient,);_position = offset;collusionCallback(offset);}Offset getOffset() {var offsetX = (_acceleration.x.abs() < 5 && _speed.x.abs() < 3) ? 0.0 : _speed.x * time + (_acceleration.x * time * time) / 2;var offsetY = (_acceleration.y.abs() < 5 && _speed.y.abs() < 6) ? 0.0 : _speed.y * time + (_acceleration.y * time * time) / 2;_position = Offset(_position.dx + offsetX, _position.dy + offsetY);_speed = Speed(_speed.x + _acceleration.x * time,_speed.y + _acceleration.y * time,);return _position;}
}class Speed {double x;double y;Speed(this.x, this.y);
}class Acceleration {double x;double y;Acceleration(this.x, this.y);
}

PhysicsBallWidget类

PhysicsBallWidget类是主部件,负责处理球的逻辑和动画:

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_xy/application.dart';
import 'package:flutter_xy/xydemo/ball/ball_model.dart';
import 'package:sensors_plus/sensors_plus.dart';
//https://github.com/yixiaolunhui/flutter_xy
class PhysicsBallWidget extends StatefulWidget {final List<Ball> ballList;final double height;final double width;const PhysicsBallWidget({required this.ballList,required this.width,required this.height,Key? key,}) : super(key: key);State<StatefulWidget> createState() => _PhysicsBallState();
}class _PhysicsBallState extends State<PhysicsBallWidget> {List<Widget> badgeBallList = [];List<ValueKey<BadgeBallConfig>> keyList = [];late Size ballSize;void initState() {super.initState();fillKeyList();WidgetsBinding.instance.addPostFrameCallback((timeStamp) {App.get().addPersistentFrameCallback(travelHitMap);});}void dispose() {App.get().removePersistentFrameCallback(travelHitMap);super.dispose();}Widget build(BuildContext context) {fillWidgetList();return Stack(children: badgeBallList,);}void fillKeyList() {var badgeSize = (widget.width - 20) / 6;badgeSize = (badgeSize >= 84.0 || badgeSize <= 0.0 || !badgeSize.isFinite)? 84.0: badgeSize;var maxCount = ((widget.height - badgeSize) ~/ badgeSize) *(widget.width ~/ badgeSize);if (widget.ballList.length >= maxCount) {badgeSize = 50.0;}ballSize = Size(badgeSize, badgeSize);var initOffsetX = 0.0;var initOffsetY = widget.height - badgeSize;for (var element in widget.ballList) {keyList.add(ValueKey<BadgeBallConfig>(BadgeBallConfig()..size = ballSize..name = element.name..setPosition(Offset(initOffsetX, initOffsetY)),));initOffsetX += badgeSize;if (initOffsetX + badgeSize > widget.width - 20) {initOffsetX = 0;initOffsetY -= badgeSize;}}}void fillWidgetList() {badgeBallList.clear();for (var e in keyList) {badgeBallList.add(BallItemWidget(key: e,limitWidth: widget.width,limitHeight: widget.height,onTap: () {},),);}}void travelHitMap(Duration timeStamp) {for (var i = 0; i < keyList.length - 1; i++) {for (var j = i + 1; j < keyList.length; j++) {hit(keyList[i].value, keyList[j].value);}}}void hit(BadgeBallConfig a, BadgeBallConfig b) {final distance = a.size.height / 2 + b.size.height / 2;final w = b.getCurrentCenter().dx - a.getCurrentCenter().dx;final h = b.getCurrentCenter().dy - a.getCurrentCenter().dy;if (sqrt(w * w + h * h) <= distance) {var aOriginSpeed = a.getCurrentSpeed();var bOriginSpeed = b.getCurrentSpeed();var aOffset = a.getCurrentPosition();var angle = atan2(h, w);var sinNum = sin(angle);var cosNum = cos(angle);var aCenter = [0.0, 0.0];var bCenter = coordinateTranslate(w, h, sinNum, cosNum, true);var aSpeed = coordinateTranslate(aOriginSpeed.x, aOriginSpeed.y, sinNum, cosNum, true);var bSpeed = coordinateTranslate(bOriginSpeed.x, bOriginSpeed.y, sinNum, cosNum, true);var vxTotal = aSpeed[0] - bSpeed[0];aSpeed[0] = (2 * 10 * bSpeed[0]) / 20;bSpeed[0] = vxTotal + aSpeed[0];var overlap = distance - (aCenter[0] - bCenter[0]).abs();aCenter[0] -= overlap;bCenter[0] += overlap;var aRotatePos =coordinateTranslate(aCenter[0], aCenter[1], sinNum, cosNum, false);var bRotatePos =coordinateTranslate(bCenter[0], bCenter[1], sinNum, cosNum, false);var bOffsetX = aOffset.dx + bRotatePos[0];var bOffsetY = aOffset.dy + bRotatePos[1];var aOffsetX = aOffset.dx + aRotatePos[0];var aOffsetY = aOffset.dy + aRotatePos[1];var aSpeedF =coordinateTranslate(aSpeed[0], aSpeed[1], sinNum, cosNum, false);var bSpeedF =coordinateTranslate(bSpeed[0], bSpeed[1], sinNum, cosNum, false);a.afterCollusion(Offset(aOffsetX, aOffsetY), Speed(aSpeedF[0], aSpeedF[1]));b.afterCollusion(Offset(bOffsetX, bOffsetY), Speed(bSpeedF[0], bSpeedF[1]));}}List<double> coordinateTranslate(double x, double y, double sin, double cos, bool reverse) {return reverse? [x * cos + y * sin, y * cos - x * sin]: [x * cos - y * sin, y * cos + x * sin];}
}

BallItemWidget类

BallItemWidget类用于具体显示每个球,并处理其动画和事件:

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_xy/application.dart';
import 'package:flutter_xy/xydemo/ball/ball_model.dart';
import 'package:sensors_plus/sensors_plus.dart';class BallItemWidget extends StatefulWidget {final double limitWidth;final double limitHeight;final Function onTap;const BallItemWidget({required this.limitWidth,required this.limitHeight,required this.onTap,Key? key,}) : super(key: key);State<StatefulWidget> createState() => BallItemState();
}class BallItemState extends State<BallItemWidget> {final List<StreamSubscription<dynamic>> _streamSubscriptions = [];late BadgeBallConfig config;Duration sensorInterval = SensorInterval.normalInterval;var color = Color.fromARGB(255,Random().nextInt(256),Random().nextInt(256),Random().nextInt(256),);Timer? timer;double x = 0;double y = 0;double limitY = 0;double limitX = 0;void initState() {super.initState();initData();_streamSubscriptions.add(accelerometerEvents.listen((AccelerometerEvent event) {config.setAcceleration(-double.parse(event.x.toStringAsFixed(1)) * 50,double.parse(event.y.toStringAsFixed(1)) * 50,);},),);_streamSubscriptions.add(userAccelerometerEvents.listen((UserAccelerometerEvent event) {config.inertiaStart(double.parse(event.x.toStringAsFixed(1)) * 50,-double.parse(event.y.toStringAsFixed(1)) * 20,);},),);timer = Timer.periodic(const Duration(milliseconds: 20), (timer) {if (!SchedulerBinding.instance.hasScheduledFrame) {SchedulerBinding.instance.scheduleFrame();}});WidgetsBinding.instance.addPostFrameCallback((timeStamp) {App.get().addPersistentFrameCallback(updatePosition);});}void dispose() {super.dispose();for (var subscription in _streamSubscriptions) {subscription.cancel();}App.get().removePersistentFrameCallback(updatePosition);timer?.cancel();timer = null;}Widget build(BuildContext context) {return AnimatedPositioned(left: x,top: y,duration: const Duration(milliseconds: 16),child: GestureDetector(onTap: () {widget.onTap.call();},child: Container(width: config.size.width,alignment: Alignment.center,height: config.size.height,decoration: BoxDecoration(shape: BoxShape.circle,border: Border.all(color: color, width: 2.w),),child: Text(config.name,style: TextStyle(fontSize: 16.w, color: Colors.red),),),),);}void initData() {limitX = widget.limitWidth;limitY = widget.limitHeight;config = (widget.key as ValueKey<BadgeBallConfig>).value;config.collusionCallback = (offset) {setState(() {x = offset.dx;y = offset.dy;config.setPosition(offset);});};x = config.getCurrentPosition().dx;y = config.getCurrentPosition().dy;}void updatePosition(Duration timeStamp) {setState(() {var tempX = config.getOffset().dx;var tempY = config.getOffset().dy;if (tempX < 0) {tempX = 0;config.setOppositeSpeed(true, false);}if (tempX > limitX - config.size.width) {tempX = limitX - config.size.width;config.setOppositeSpeed(true, false);}if (tempY < 0) {tempY = 0;config.setOppositeSpeed(false, true);}if (tempY > limitY - config.size.height) {tempY = limitY - config.size.height;config.setOppositeSpeed(false, true);}x = tempX;y = tempY;config.setPosition(Offset(x, y));});}
}

BallListPage类

BallListPage类是测试页面,用于展示物理球动画效果:

import 'package:flutter/material.dart';
import 'package:flutter_xy/xydemo/ball/ball_model.dart';
import 'package:flutter_xy/xydemo/ball/ball_widget.dart';class BallListPage extends StatefulWidget {const BallListPage({super.key});State<BallListPage> createState() => _BallListPageState();
}class _BallListPageState extends State<BallListPage> {final List<Ball> badgeList = [Ball(name: '北京'),Ball(name: '上海'),Ball(name: '天津'),Ball(name: '徐州'),Ball(name: '南京'),Ball(name: '苏州'),Ball(name: '杭州'),Ball(name: '合肥'),Ball(name: '武汉'),Ball(name: '常州'),Ball(name: '香港'),Ball(name: '澳门'),Ball(name: '新疆'),Ball(name: '成都'),Ball(name: '宿迁'),];Widget build(BuildContext context) {return Scaffold(body: Stack(children: [PhysicsBallWidget(ballList: badgeList,height: MediaQuery.of(context).size.height,width: MediaQuery.of(context).size.width,),],),);}
}

结论

通过这篇博客,我们展示了如何在Flutter中实现一个物理球动画效果,并且集成了sensors_plus插件来获取设备的加速度传感器数据。希望这篇博客能对您在Flutter开发中实现类似效果有所帮助。
详情见:github.com/yixiaolunhui/flutter_xy

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/374569.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

聊聊大模型如何为敏捷研发提效

这是鼎叔的第一百零二篇原创文章。行业大牛和刚毕业的小白&#xff0c;都可以进来聊聊。 欢迎关注本公众号《敏捷测试转型》&#xff0c;星标收藏&#xff0c;大量原创思考文章陆续推出。本人新书《无测试组织-测试团队的敏捷转型》已出版&#xff08;机械工业出版社&#xff…

红日靶场----(三)2.漏洞利用

上期的通过一句话木马实现对目标主机的持久后门 我使用的是蚁剑&#xff0c;蚁剑安装及使用参考&#xff1a; 下载地址&#xff1a; GitHub - AntSwordProject/AntSword-Loader: AntSword 加载器 安装即使用&#xff1a; 1. 快速入门 语雀 通过YXCMS的后台GETSHELL 利用…

设计模式探索:策略模式

1. 什么是策略模式&#xff08;Strategy Pattern&#xff09; 定义 策略模式&#xff08;Strategy Pattern&#xff09;的原始定义是&#xff1a;定义一系列算法&#xff0c;将每一个算法封装起来&#xff0c;并使它们可以相互替换。策略模式让算法可以独立于使用它的客户端而…

算法day03 桶排序 数据结构分类 时间复杂度 异或运算

学数据结构之前 必看_哔哩哔哩_bilibili 1.认识复杂度和简单排序算法_哔哩哔哩_bilibili 桶排序&#xff08;Bucket sort&#xff09;------时间复杂度为O(n)的排序方法&#xff08;一&#xff09;_多桶排序时间复杂度-CSDN博客 桶排序 测试场景&#xff1a;数组中有10000个随…

PyTorch SummaryWriter TensorBoard 中进行可视化

在 PyTorch 中&#xff0c;SummaryWriter 通常用于在训练过程中记录各种数据&#xff0c;以便在 TensorBoard 中进行可视化。 - 安装&#xff1a; pip install tensorboard -i https://mirrors.aliyun.com/pypi/simple/ from torch.utils.tensorboard import SummaryWriter…

MVC分页

public ActionResult Index(int ? page){IPagedList<EF.ACCOUNT> userPagedList;using (EF.eMISENT content new EF.eMISENT()){第几页int pageNumber page ?? 1;每页数据条数&#xff0c;这个可以放在配置文件中int pageSize 10;//var infoslist.C660List.OrderBy(…

2.电容(常见元器件及电路基础知识)

一.电容种类 1.固态电容 这种一般价格贵一些&#xff0c;ESR,ESL比较低,之前项目400W电源用的就是这个&#xff0c;温升能够很好的控制 2.铝电解电容 这种一般很便宜&#xff0c;ESR,ESL相对大一些&#xff0c;一般发热量比较大&#xff0c;烫手。 这种一般比上一个贵一点&am…

【人工智能】-- 反向传播

个人主页&#xff1a;欢迎来到 Papicatch的博客 课设专栏 &#xff1a;学生成绩管理系统 专业知识专栏&#xff1a; 专业知识 文章目录 &#x1f349;引言 &#x1f349;反向传播 &#x1f348;定义 &#x1f348;反向传播的作用 &#x1f34d;参数优化 &#x1f34d;学…

docker也能提权??内网学习第6天 rsync未授权访问覆盖 sudo(cve-2021-3156)漏洞提权 polkit漏洞利用

现在我们来说说liunx提权的操作&#xff1a;前面我们说了环境变量&#xff0c;定时任务来进行提权的操作 rsync未授权访问覆盖 我们先来说说什么是rsync rsync是数据备份工具&#xff0c;默认是开启的873端口 我们在进行远程连接的时候&#xff0c;如果它没有让我们输入账号…

Python高级(三)_正则表达式

Python高级-正则表达式 第三章 正则表达式 在开发中会有大量的字符串处理工作,其中经常会涉及到字符串格式的校验。 1、正则表达式概述 正则表达式,又称正规表示式、正规表示法、正规表达式、规则表达式、常规表示法(英语:Regular Expression,在代码中常简写为regex、…

论文学习_Getafix: learning to fix bugs automatically

1. 引言 研究背景:现代生产代码库极其复杂并且不断更新。静态分析器可以帮助开发人员发现代码中的潜在问题(在本文的其余部分中称为错误),这对于在这些大型代码库中保持高代码质量是必要的。虽然通过静态分析尽早发现错误是有帮助的,但修复这些错误的问题在实践中仍然主要…

51单片机STC89C52RC——16.1 五线四相步进电机

目录 目的/效果 一&#xff0c;STC单片机模块 二&#xff0c;步进电机 2.2 什么是步进电机&#xff1f; 2.2.1 步进电机驱动板 静态参数 动态参数 2.2.2 五线四相 单相激励步进 双相激励步进 混合激励驱动 2.3 细分驱动 2.4 通过数字信号控制旋转位置和转速。 2…

第一次参加数学建模竞赛新手小白备赛经验贴

2024年暑假已经来临&#xff0c;下半年的数学建模竞赛非常多&#xff0c;许多同学可能是第一次参赛&#xff0c;对于如何准备感到迷茫和无从下手。在这种情况下&#xff0c;我们将分享一些备赛的小技巧&#xff0c;帮助大家在这个暑假更好的入门&#xff0c;即便是零基础的小白…

resistronic焊接机RMF10 RE120安装SSK10说明操作

resistronic焊接机RMF10 RE120安装SSK10说明操作

新零售起盘案例「半藏酱酒」布局路径,半藏总院分院招商模式

在当前白酒市场中&#xff0c;一款名为半藏酒的酒品以其独特的新零售模式引起了广泛关注。这种模式不同于传统销售方式&#xff0c;通过多种创新玩法&#xff0c;实现了销售与品牌推广的双重目标&#xff0c;让我们一起来看看细节。 半藏酒的分级代理制度将代理商分为两个层级&…

如何录制屏幕视频?4款软件,轻松录屏

在数字化飞速发展的时代&#xff0c;如何录制屏幕视频已经成为我们工作、学习和娱乐中不可省略的一个重要问题。无论是制作教学教程还是录制游戏视频等&#xff0c;屏幕视频录制都为我们提供了极大的便利。今天&#xff0c;就让我们一起探索如何录制屏幕视频的精彩方式&#xf…

Go 1.19.4 函数-Day 08

1. 函数概念和调用原理 1.1 基本介绍 函数是基本的代码块&#xff0c;用于执行一个任务。 Go 语言最少有个 main() 函数。 你可以通过函数来划分不同功能&#xff0c;逻辑上每个函数执行的是指定的任务。 函数声明告诉了编译器函数的名称&#xff0c;返回类型&#xff0c;和参…

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…

Java版Flink使用指南——将消息写入到RabbitMQ的队列中

大纲 新建工程新增依赖 编码自动产生数据写入RabbitMQ 测试工程代码 在 《Java版Flink使用指南——从RabbitMQ中队列中接入消息流》一文中&#xff0c;我们介绍了如何使用Java在Flink中读取RabbitMQ中的数据&#xff0c;并将其写入日志中。本文将通过代码产生一些数据&#xf…

未解之谜----macOS版fiddler everywhere 如何将当前会话保存成一个txt文件查看

如图&#xff0c;这是win版的保存方式&#xff0c;mac上面根本没有这个按钮&#xff0c;找的很崩溃