Mi informacion de contacto
Correo[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
La implementación de efectos de animación física en las aplicaciones Flutter puede mejorar enormemente la experiencia del usuario.Este artículo presentará en detalle cómo crear una interfaz de bola animada que simule una colisión física en Flutter. La implementación del código principal se basa en la integración.sensors_plus
Complemento para obtener los datos del sensor de aceleración del dispositivo.
Antes de comenzar, asegúrese depubspec.yaml
Agregar al archivosensors_plus
Enchufar:
dependencies:
flutter:
sdk: flutter
sensors_plus: 4.0.2
entonces correflutter pub get
comando para obtener dependencias.
Implementaremos un programa llamadoPhysicsBallWidget
El widget personalizado incluye principalmente las siguientes partes:
Primero defineBall
Clase utilizada para representar información básica sobre cada bola, como el nombre:
class Ball {
final String name;
Ball({required this.name});
}
BadgeBallConfig
La clase se utiliza para gestionar el estado y el comportamiento de cada bola, incluida la aceleración, la velocidad, la posición y otra información:
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
La clase es el componente principal y es responsable de manejar la lógica y animación de la pelota:
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 bOffset
X = 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
Las clases se utilizan para mostrar específicamente cada bola y manejar sus animaciones y eventos:
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
La clase es una página de prueba que se utiliza para mostrar el efecto de animación de la pelota física:
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,
),
],
),
);
}
}
A través de este blog, mostramos cómo implementar un efecto de animación de bola física en Flutter e integrarlo.sensors_plus
Complemento para obtener los datos del sensor de aceleración del dispositivo. Espero que este blog pueda ayudarte a lograr efectos similares en el desarrollo de Flutter.
Para más detalles, consulte: github.com/yixiaolunhui/flutter_xy