Condivisione della tecnologia

Il flutter realizza l'effetto fisico della collisione della palla

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Effetto

Inserisci qui la descrizione dell'immagine

introduzione

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_plusPlug-in per ottenere i dati del sensore di accelerazione del dispositivo.

Preparazione

Prima di iniziare, assicurati dipubspec.yamlAggiungi al filesensors_plusCollegare:

dependencies:
  flutter:
    sdk: flutter
  sensors_plus: 4.0.2
  • 1
  • 2
  • 3
  • 4

poi corriflutter pub getcomando per ottenere le dipendenze.

Struttura del codice

Implementeremo un programma chiamatoPhysicsBallWidgetIl widget personalizzato include principalmente le seguenti parti:

  • Classe palla: rappresenta le informazioni di base di ogni palla.
  • Classe BadgeBallConfig: gestisce lo stato e il comportamento di ogni pallina.
  • Classe PhysicsBallWidget: componente principale, inclusa la logica e l'animazione della palla.
  • Classe BallItemWidget: visualizza specificamente il widget per ogni palla.
  • Classe BallListPage: pagina di test, che mostra l'effetto di animazione fisica della palla.

Lezione di palla

Innanzitutto definireBallClasse utilizzata per rappresentare le informazioni di base su ciascuna palla, come il nome:

class Ball {
  final String name;

  Ball({required this.name});
}
  • 1
  • 2
  • 3
  • 4
  • 5

Classe BadgeBallConfig

BadgeBallConfigLa 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

Classe PhysicsBallWidget

PhysicsBallWidgetLa 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];
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163

BallItemWidgetClass

BallItemWidgetLe 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));
    });
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157

Classe BallListPage

BallListPageLa 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,
          ),
        ],
      ),
    );
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

Insomma

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