le mie informazioni di contatto
Posta[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
L'implementazione di effetti di animazione fisica nelle applicazioni Flutter può migliorare notevolmente l'esperienza dell'utente.Questo articolo introdurrà in dettaglio come creare un'interfaccia di palla animata che simula la collisione fisica in Flutter. L'implementazione del codice principale si basa sull'integrazionesensors_plus
Plug-in per ottenere i dati del sensore di accelerazione del dispositivo.
Prima di iniziare, assicurati dipubspec.yaml
Aggiungi al filesensors_plus
Collegare:
dependencies:
flutter:
sdk: flutter
sensors_plus: 4.0.2
poi corriflutter pub get
comando per ottenere le dipendenze.
Implementeremo un programma chiamatoPhysicsBallWidget
Il widget personalizzato include principalmente le seguenti parti:
Innanzitutto definireBall
Classe utilizzata per rappresentare le informazioni di base su ciascuna palla, come il nome:
class Ball {
final String name;
Ball({required this.name});
}
BadgeBallConfig
La classe viene utilizzata per gestire lo stato e il comportamento di ciascuna palla, inclusa l'accelerazione, la velocità, la posizione e altre informazioni:
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 è il componente principale ed è responsabile della gestione della logica e dell'animazione della palla:
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
Le classi vengono utilizzate per visualizzare in modo specifico ciascuna palla e gestirne le animazioni e gli eventi:
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 è una pagina di test, utilizzata per visualizzare l'effetto di animazione fisica della palla:
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,
),
],
),
);
}
}
Attraverso questo blog, mostriamo come implementare un effetto di animazione fisica della palla in Flutter e integrarlosensors_plus
Plug-in per ottenere i dati del sensore di accelerazione del dispositivo. Spero che questo blog possa aiutarti a ottenere effetti simili nello sviluppo di Flutter.
Per i dettagli, vedere: github.com/yixiaolunhui/flutter_xy