🐧

【Flutter入門】ぶつりシュミレーション的なものを使う

に公開

こんにちは、今回はflutter/cookbookに記載されている「Animate a widget using a physics simulation」を体感してみましょう!

https://docs.flutter.dev/cookbook/animation/physics-simulation

カードをドラッグして離したら、バネ(スプリング)の力で元に戻る」っていう楽しい物理アニメーションのデモですね〜

こんな感じに楽しい↓

楽しいし、色々なアイデアが浮かんできそう!
とりあえず楽しもうくらいの温度感!

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(child: FlutterLogo(size: 128)),
    );
  }
}

class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, super.key});
  final Widget child;

  
  State<DraggableCard> createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Alignment> _animation;
  Alignment _dragAlignment = Alignment.center;

  //スプリングシュミレー=ション
  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      AlignmentTween(begin: _dragAlignment, end: Alignment.center),
    );

    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1);

    final Simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

    _controller.animateWith(Simulation);
  }

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1),
    );
    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    return GestureDetector(
      onPanDown: (details) {
        _controller.stop();
      },
      onPanUpdate: (details) {
        setState(() {
          _dragAlignment += Alignment(
            details.delta.dx / (size.width / 2),
            details.delta.dy / (size.height / 2),
          );
        });
      },
      onPanEnd: (details) {
        _runAnimation(details.velocity.pixelsPerSecond, size);
      },
      child: Align(alignment: _dragAlignment, child: Card(child: widget.child)),
    );
  }
}

【実装時に気をつけるポイント】

1.AnimationController の管理
-dispose() で解放を忘れずに。
-unbounded コントローラーは無限範囲を扱えるので暴走しないよう stop() を適切に呼ぶこと。

2.Velocity の取り扱い
-details.velocity.pixelsPerSecond から速度を計算しますが、極端に大きい値が入ることもあるので、
本番では安全のために clamp() で上限を設けても良いです。

3.スプリングパラメータの調整
-mass, stiffness, damping のバランスで自然さが決まります。
4.パフォーマンス
-毎フレーム setState() が走るので、重たいUIを載せる場合はビルドツリーを最適化する必要があるらしい

Discussion