Implementing Physics-Based Animations with Flutter Physics

Flutter provides a rich set of tools and widgets for creating beautiful and engaging user interfaces. One of the more advanced and visually appealing techniques is implementing physics-based animations. These animations mimic real-world physics, resulting in natural-looking and immersive user experiences. This comprehensive guide explores how to implement physics-based animations in Flutter using the flutter_physics package.

What are Physics-Based Animations?

Physics-based animations use mathematical models to simulate physical properties like velocity, acceleration, friction, and gravity. This approach leads to animations that feel more organic and realistic, making them more attractive to users.

Why Use Physics-Based Animations?

  • Realistic Motion: Creates more natural and believable animations.
  • Enhanced User Experience: Provides a more immersive and engaging interface.
  • Flexibility: Easily adjust physical parameters to fine-tune animation behavior.

Implementing Physics-Based Animations with flutter_physics

To get started with physics-based animations, you can use the flutter_physics package, which simplifies the implementation of physics simulations in Flutter.

Step 1: Add Dependency

Add the flutter_physics package to your pubspec.yaml file:

dependencies:
  flutter_physics: ^latest_version

Replace ^latest_version with the newest version of the package available on pub.dev.

Run flutter pub get to install the package.

Step 2: Basic Usage

Let’s start with a basic example of a draggable ball that bounces when it hits the edges of the screen.

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: PhysicsAnimationExample(),
    );
  }
}

class PhysicsAnimationExample extends StatefulWidget {
  @override
  _PhysicsAnimationExampleState createState() => _PhysicsAnimationExampleState();
}

class _PhysicsAnimationExampleState extends State<PhysicsAnimationExample> {
  late Physics simulation;
  Offset position = Offset(100, 100);
  double velocityX = 200.0;
  double velocityY = 300.0;
  double radius = 50.0;

  @override
  void initState() {
    super.initState();
    simulation = Physics(
      position: Vector2(position.dx, position.dy),
      velocity: Vector2(velocityX, velocityY),
      // drag: 0.1,  // You can uncomment and set these properties according to requirements.
      // mass: 0.5,
      // stiffness: 200.0,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Physics Animation Example'),
      ),
      body: AnimatedBuilder(
        animation: simulation,
        builder: (context, child) {
          final size = MediaQuery.of(context).size;

          // Bounce off the edges
          if (simulation.position.x + radius > size.width || simulation.position.x - radius < 0) {
            simulation.velocity = Vector2(-simulation.velocity.x, simulation.velocity.y);
          }
          if (simulation.position.y + radius > size.height || simulation.position.y - radius < 0) {
            simulation.velocity = Vector2(simulation.velocity.x, -simulation.velocity.y);
          }

          return GestureDetector(
            onPanUpdate: (details) {
              setState(() {
                simulation.position = Vector2(
                  details.localPosition.dx,
                  details.localPosition.dy,
                );
                simulation.velocity = Vector2(0, 0); // Stop movement while dragging
              });
            },
            onPanEnd: (details) {
              setState(() {
                simulation.velocity = Vector2(details.velocity.pixelsPerSecond.dx, details.velocity.pixelsPerSecond.dy);
              });
            },
            child: Transform.translate(
              offset: Offset(simulation.position.x - radius, simulation.position.y - radius),
              child: Container(
                width: 2 * radius,
                height: 2 * radius,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  shape: BoxShape.circle,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Explanation:

  • The Physics object manages the simulation.
  • We update the position of the ball based on the simulation’s position value.
  • Bouncing off the screen edges is achieved by inverting the appropriate velocity component when the ball hits an edge.
  • The GestureDetector is used for dragging behavior which sets the velocity of the simulation according to the pan velocity.

Step 3: Advanced Physics Simulations

You can customize the behavior by adjusting the physics properties. Here’s how you can implement more advanced simulations:

1. Spring Animation

import 'package:flutter/material.dart';
import 'package:flutter_physics/flutter_physics.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SpringAnimationExample(),
    );
  }
}

class SpringAnimationExample extends StatefulWidget {
  @override
  _SpringAnimationExampleState createState() => _SpringAnimationExampleState();
}

class _SpringAnimationExampleState extends State<SpringAnimationExample> {
  late Physics simulation;
  Offset position = Offset(200, 200);
  double radius = 50.0;

  @override
  void initState() {
    super.initState();
    simulation = Physics(
      position: Vector2(position.dx, position.dy),
      velocity: Vector2(0.0, 0.0),
      drag: 0.1,
      mass: 0.5,
      stiffness: 200.0,  // Stiffness determines spring strength
    );
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    Offset anchor = Offset(size.width / 2, size.height / 2);

    return Scaffold(
      appBar: AppBar(
        title: Text('Spring Animation Example'),
      ),
      body: GestureDetector(
        onPanUpdate: (details) {
          setState(() {
            position = details.localPosition;  // Set the anchor position
          });
        },
        onPanEnd: (details) {
          setState(() {
            position = Offset(size.width / 2, size.height / 2); // Set to middle after pan ends
          });
        },
        child: AnimatedBuilder(
          animation: simulation,
          builder: (context, child) {
            // Pull toward center: spring-like effect
            Vector2 force = Vector2(position.dx, position.dy) - simulation.position;
            simulation.applyForce(force);

            // Bounce off the edges
            if (simulation.position.x + radius > size.width || simulation.position.x - radius < 0) {
              simulation.velocity = Vector2(-simulation.velocity.x * 0.8, simulation.velocity.y * 0.8); // Reduce velocity on impact
            }
            if (simulation.position.y + radius > size.height || simulation.position.y - radius < 0) {
              simulation.velocity = Vector2(simulation.velocity.x * 0.8, -simulation.velocity.y * 0.8); // Reduce velocity on impact
            }
          
            return Transform.translate(
              offset: Offset(simulation.position.x - radius, simulation.position.y - radius),
              child: Container(
                width: 2 * radius,
                height: 2 * radius,
                decoration: BoxDecoration(
                  color: Colors.green,
                  shape: BoxShape.circle,
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

Explanation:

  • The ball is attracted to the center. This creates a spring-like motion.
  • Stiffness is set when initializing Physics, and a custom `applyForce()` function computes force to pull the ball toward a target position.

2. Adding Friction and Gravity

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FrictionAndGravityExample(),
    );
  }
}

class FrictionAndGravityExample extends StatefulWidget {
  @override
  _FrictionAndGravityExampleState createState() => _FrictionAndGravityExampleState();
}

class _FrictionAndGravityExampleState extends State<FrictionAndGravityExample> {
  late Physics simulation;
  Offset position = Offset(100, 100);
  double velocityX = 200.0;
  double velocityY = 300.0;
  double radius = 50.0;

  @override
  void initState() {
    super.initState();
    simulation = Physics(
      position: Vector2(position.dx, position.dy),
      velocity: Vector2(velocityX, velocityY),
      drag: 0.1, // Represents friction
      mass: 0.5,
    );
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    double gravity = 9.81 * 20;

    return Scaffold(
      appBar: AppBar(
        title: Text('Friction and Gravity Example'),
      ),
      body: AnimatedBuilder(
        animation: simulation,
        builder: (context, child) {
          // Apply gravity
          simulation.applyForce(Vector2(0, gravity * simulation.mass));

          // Bounce off the edges
          if (simulation.position.x + radius > size.width || simulation.position.x - radius < 0) {
            simulation.velocity = Vector2(-simulation.velocity.x * 0.8, simulation.velocity.y * 0.8); // Reduce velocity on impact
          }
          if (simulation.position.y + radius > size.height || simulation.position.y - radius < 0) {
            simulation.velocity = Vector2(simulation.velocity.x * 0.8, -simulation.velocity.y * 0.8); // Reduce velocity on impact
          }

          return GestureDetector(
            onPanUpdate: (details) {
              setState(() {
                simulation.position = Vector2(
                  details.localPosition.dx,
                  details.localPosition.dy,
                );
                simulation.velocity = Vector2(0, 0); // Stop movement while dragging
              });
            },
            onPanEnd: (details) {
              setState(() {
                simulation.velocity = Vector2(details.velocity.pixelsPerSecond.dx, details.velocity.pixelsPerSecond.dy);
              });
            },
            child: Transform.translate(
              offset: Offset(simulation.position.x - radius, simulation.position.y - radius),
              child: Container(
                width: 2 * radius,
                height: 2 * radius,
                decoration: BoxDecoration(
                  color: Colors.red,
                  shape: BoxShape.circle,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

Explanation:

  • The ball now experiences a gravitational force pulling it downwards and friction that slows it down.
  • The mass also plays a role: the higher the mass, the harder it is for the object to change its state of motion.

Conclusion

Physics-based animations enhance user experiences by providing natural and realistic motion in Flutter applications. The flutter_physics package offers an easy way to integrate these animations, allowing you to simulate complex physical behaviors. By adjusting properties like velocity, drag, stiffness, and mass, you can fine-tune your animations for optimal visual appeal and interactivity. Experiment with different values and configurations to achieve unique and compelling effects.