2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
La mise en œuvre d'effets d'animation physique dans les applications Flutter peut considérablement améliorer l'expérience utilisateur.Cet article présentera en détail comment créer une interface de balle animée qui simule une collision physique dans Flutter. L'implémentation principale du code est basée sur l'intégration.sensors_plus
Plugin pour obtenir les données du capteur d'accélération de l'appareil.
Avant de commencer, assurez-vouspubspec.yaml
Ajouter au fichiersensors_plus
Brancher:
dependencies:
flutter:
sdk: flutter
sensors_plus: 4.0.2
puis coursflutter pub get
commande pour obtenir les dépendances.
Nous mettrons en œuvre un programme appeléPhysicsBallWidget
Le widget personnalisé comprend principalement les parties suivantes :
Définir d'abordBall
Classe utilisée pour représenter les informations de base sur chaque balle, telles que le nom :
class Ball {
final String name;
Ball({required this.name});
}
BadgeBallConfig
La classe est utilisée pour gérer l'état et le comportement de chaque balle, y compris l'accélération, la vitesse, la position et d'autres informations :
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 classe est l'élément principal et est responsable de la gestion de la logique et de l'animation du ballon :
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
Les classes sont utilisées pour afficher spécifiquement chaque balle et gérer ses animations et événements :
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 classe est une page de test, utilisée pour afficher l'effet d'animation physique du ballon :
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 travers ce blog, nous montrons comment implémenter un effet d'animation de balle physique dans Flutter et intégrersensors_plus
Plugin pour obtenir les données du capteur d'accélération de l'appareil. J'espère que ce blog pourra vous aider à obtenir des effets similaires dans le développement de Flutter.
Pour plus de détails, voir : github.com/yixiaolunhui/flutter_xy