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.mzph.cn/news/870675.shtml

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

相关文章

一文详解DDL同步及其应用场景

目录 一、什么是DDL&#xff1f; 二、什么是DDL同步&#xff1f; 三、DDL同步的痛点 1、缺少自动DDL同步机制 2、缺少DDL变更监测预警 四、解决方案 五、应用场景及案例 案例一 案例二 案例三 在现代数据管理中&#xff0c;数据库的结构变更频繁且不可避免&#xff0c;特别是在…

Kubelet 认证

当我们执行kubectl exec -it pod [podName] sh命令时&#xff0c;apiserver会向kubelet发起API请求。也就是说&#xff0c;kubelet会提供HTTP服务&#xff0c;而为了安全&#xff0c;kubelet必须提供HTTPS服务&#xff0c;且还要提供一定的认证与授权机制&#xff0c;防止任何知…

C语言 | Leecode C语言题解之第229题多数元素II

题目&#xff1a; 题解&#xff1a; /*** Note: The returned array must be malloced, assume caller calls free().*//*假定 num1&#xff0c;num2 为出现次数大于 nums.length / 3 的两个数。&#xff08;最多出现两个&#xff09;遍历 nums&#xff0c; 若出现 num1、num2…

《C语言程序设计 第4版》笔记和代码 第十一章 指针和数组

第十一章 指针和数组 11.1 指针和一维数组间的关系 1 由于数组名代表数组元素的连续存储空间的首地址&#xff0c;因此&#xff0c;数组元素既可以用下标法也可以用指针来引用。 例11.1见文末 2 p1与p在本质上是两个不同的操作&#xff0c;前者不改变当前指针的指向&#xf…

无人机之遥控器保养

一、使用存放 1、避免让遥控器受到强烈的震动或从高处跌落&#xff0c;以免影响内部结构的精度&#xff1b; 2、遥控器在使用完后&#xff0c;需要将天线收拢&#xff0c;避免折断&#xff0c;养成定期检查天线的习惯&#xff1b; 3、定期检查遥控器按键有无裂纹、畸变、松旷…

ISO/OIS的七层模型②

OSI模型是一个分层的模型&#xff0c;每一个部分称为一层&#xff0c;每一层扮演固定的角色&#xff0c;互不干扰。OSI有7层&#xff0c;从上到下分别是&#xff1a; 一&#xff0c;每层功能 7.应用层&#xff08;Application layer &#xff09;&#xff1a;应用层功能&#x…

如何从gitlab删除仓库

嗨&#xff0c;我是兰若姐姐。今天发现gitlab上有些仓库的代码没有用&#xff0c;是个多余的仓库&#xff0c;想要删掉&#xff0c;经过一番操作之后&#xff0c;成功的删除了&#xff0c;git上没有 多余的仓库&#xff0c;看着干净舒服很多&#xff0c;现在把删除的过程分享出…

基于ssm的图书管理系统的设计与实现

摘 要 在当今信息技术日新月异的时代背景下&#xff0c;图书管理领域正经历着深刻的变革&#xff0c;传统的管理模式已难以适应现代社会的快节奏和高要求&#xff0c;逐渐向数字化、智能化的方向演进。本论文聚焦于这一转变趋势&#xff0c;致力于设计并成功实现一个基于 SSM&…

U-net和U²-Net网络详解

目录 U-Net: Convolutional Networks for Biomedical Image Segmentation摘要U-net网络结构pixel-wise loss weight U-Net: Going Deeper with Nested U-Structure for Salient Object Detection摘要网络结构详解整体结构RSU-n结构RSU-4F结构saliency map fusion module -- 显著…

JavaFx+MySql学生管理系统

前言: 上个月学习了javafx和mysql数据库,于是写了一个学生管理系统,因为上个月在复习并且有一些事情,比较忙,所以没有更新博客了,这个项目页面虽然看着有点简陋了,但是大致内容还是比较简单的,于是现在跟大家分享一下我的学生管理系统,希望对这方面有兴趣的同学提供一些帮助 &a…

Vue 3 中创建一个动态的组件实例

本文将介绍如何在 Vue 3 中实现一个动态 Toast 组件实例。我们将创建一个简单的 Toast 组件&#xff0c;并使用一个动态创建实例的脚本来显示 Toast 消息。在 Vue 3 中创建动态组件实例有许多好处&#xff0c;这些好处主要体现在灵活性、性能、可维护性和用户体验等方面。 创建…

【JavaScript 算法】快速排序:高效的排序算法

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 一、算法原理二、算法实现三、应用场景四、优化与扩展五、总结 快速排序&#xff08;Quick Sort&#xff09;是一种高效的排序算法&#xff0c;通过分治法将数组分为较小的子数组&#xff0c;递归地排序子数组。快速排序通常…

分享一个 .NET 通过监听器拦截 EF 消息写日志的详细例子

前言 EF 开发效率确实很高也很便捷&#xff0c;但当它发生错误时&#xff0c;也挺让人头疼的&#xff0c;为什么&#xff1f;因为 EF 就像是一个黑盒子&#xff0c;一切全被封装起来&#xff0c;出错的时候很难定位原因&#xff0c;如果能够知道并打印 EF 生成的 SQL 语句&…

记录些Redis题集(1)

为什么Redis要有淘汰机制&#xff1f; 淘汰机制的存在是必要的&#xff0c;因为Redis是一种基于内存的数据库&#xff0c;所有数据都存储在内存中。然而&#xff0c;内存资源是有限的。在Redis的配置文件redis.conf中&#xff0c;有一个关键的配置项&#xff1a; # maxmemory…

Go语言入门之Map详解

Go语言入门之Map详解 1.基础定义 map是一个无序的&#xff0c;k-v键值对格式的集合 &#xff08;1&#xff09;特点 类型特点&#xff1a;map为引用类型&#xff0c;所以在函数中更新value值会永久改变顺序特点&#xff1a;map的遍历是无序的&#xff0c;因为底层是哈希表&am…

零基础学python(二)

1. 字典 字典的创建 最常见的创建方式&#xff1a; d {"k1": "v1", "k2": "v2"} 再向字典中添加一对键值&#xff1a; d["k3"] "v3" 字典还可以用dict()创建&#xff0c;这种方式中&#xff0c;键是不加引…

【Unity2D 2022:UI】制作主菜单

一、创建主菜单游戏场景 1. 在Scenes文件夹中新建一个游戏场景Main Menu 2. 为场景添加背景 &#xff08;1&#xff09;创建画布Canvas &#xff08;2&#xff09;在Canvas中创建新的空游戏物体Main Menu &#xff08;3&#xff09;在Main Menu中新建一个图像游戏物体Backgrou…

无人机之机身保养

一、外观检查 1、检查机器表面整洁无划痕、无针孔凹陷擦伤、畸变等损坏情况&#xff1b; 2、晃动机身&#xff0c;仔细听机身内部有无松动零件或者螺丝在机身内部。 二、桨叶检查 1、有无裂痕、磨损、变形等缺陷&#xff0c;如有明显缺陷建议更换&#xff1b; 2、卡扣、紧…

Animate软件基础:图层的基本用法

图层作为Animate软件中比较重要的功能&#xff0c;需要对其使用方法和作用理解充分&#xff0c;并熟练操作才可以更好的用来制作内容。 图层相关的功能和用法如下&#xff1a; 图层可以帮助在文档中组织插图。 可以在一个图层上绘制和编辑对象&#xff0c;而不会影响其他图层…

排座椅【详细代码题解】

[NOIP2008 普及组] 排座椅 题目描述 上课的时候总会有一些同学和前后左右的人交头接耳&#xff0c;这是令小学班主任十分头疼的一件事情。不过&#xff0c;班主任小雪发现了一些有趣的现象&#xff0c;当同学们的座次确定下来之后&#xff0c;只有有限的 D D D 对同学上课时…