flutter_spring_animation 0.1.0 copy "flutter_spring_animation: ^0.1.0" to clipboard
flutter_spring_animation: ^0.1.0 copied to clipboard

A Flutter package for creating smooth spring-based animations with customizable damping, stiffness, and velocity parameters.

example/lib/main.dart

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

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

/// The main application widget demonstrating spring animations.
class MyApp extends StatelessWidget {
  /// Creates the main app widget.
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Spring Animation Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const SpringAnimationDemo(),
    );
  }
}

/// Main demo page showcasing different spring animation features.
class SpringAnimationDemo extends StatefulWidget {
  /// Creates the demo page.
  const SpringAnimationDemo({super.key});

  @override
  State<SpringAnimationDemo> createState() => _SpringAnimationDemoState();
}

class _SpringAnimationDemoState extends State<SpringAnimationDemo> {
  late SpringController _scaleController;
  late SpringController _rotationController;
  late SpringController _slideController;
  late SpringController _chainController;
  late SpringController _toggleController;
  late SpringController _bounceController;

  bool _isVisible = false;
  bool _isToggled = false;
  SpringConfig _currentConfig = SpringConfig.bouncy;
  double _dynamicTarget = 1.0;

  @override
  void initState() {
    super.initState();
    _updateControllers();
  }

  void _updateControllers() {
    // Save current values if controllers are already initialized
    double scaleValue = 0.0;
    double rotationValue = 0.0;
    double slideValue = 0.0;
    double chainValue = 0.0;
    bool toggleValue = false;

    try {
      scaleValue = _scaleController.value;
      rotationValue = _rotationController.value;
      slideValue = _slideController.value;
      chainValue = _chainController.value;
      toggleValue = _toggleController.value > 0.5;
    } catch (_) {
      // Controllers not initialized yet, use defaults (already set above)
    }

    // Dispose existing controllers if they exist
    try {
      _scaleController.dispose();
      _rotationController.dispose();
      _slideController.dispose();
      _chainController.dispose();
      _toggleController.dispose();
      _bounceController.dispose();
    } catch (_) {
      // Controllers not initialized yet, nothing to dispose
    }

    // Create new controllers with current config
    _scaleController = SpringController(
      config: _currentConfig,
      initialValue: scaleValue,
    );
    _rotationController = SpringController(
      config: _currentConfig,
      initialValue: rotationValue,
    );
    _slideController = SpringController(
      config: _currentConfig,
      initialValue: slideValue,
    );
    _chainController = SpringController(
      config: _currentConfig,
      initialValue: chainValue,
    );
    _toggleController = SpringController.toggle(
      config: _currentConfig,
      initialValue: toggleValue,
    );
    _bounceController = SpringController.bounce(
      config: _currentConfig,
      min: 0.0,
      max: 1.0,
      duration: const Duration(milliseconds: 800),
    );
  }

  @override
  void dispose() {
    _scaleController.dispose();
    _rotationController.dispose();
    _slideController.dispose();
    _chainController.dispose();
    _toggleController.dispose();
    _bounceController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Spring Animation Demo'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildConfigSelector(),
            const SizedBox(height: 24),
            _buildBasicAnimations(),
            const SizedBox(height: 24),
            _buildTransitionWidgets(),
            const SizedBox(height: 24),
            _buildInteractiveDemo(),
            const SizedBox(height: 24),
            _buildAdvancedControllers(),
          ],
        ),
      ),
    );
  }

  Widget _buildConfigSelector() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Spring Configuration',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Wrap(
              spacing: 8,
              children: [
                _configChip('Bouncy', SpringConfig.bouncy),
                _configChip('Gentle', SpringConfig.gentle),
                _configChip('Wobbly', SpringConfig.wobbly),
                _configChip('Stiff', SpringConfig.stiff),
                _configChip('Slow', SpringConfig.slow),
              ],
            ),
            const SizedBox(height: 8),
            Container(
              padding: const EdgeInsets.all(8),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                'Applied to all animations below\n'
                'Damping: ${_currentConfig.damping.toStringAsFixed(1)}, '
                'Stiffness: ${_currentConfig.stiffness.toStringAsFixed(1)}, '
                'Mass: ${_currentConfig.mass.toStringAsFixed(1)}',
                style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                textAlign: TextAlign.center,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _configChip(String label, SpringConfig config) {
    final isSelected = _currentConfig == config;
    return FilterChip(
      label: Text(label),
      selected: isSelected,
      onSelected: (selected) {
        if (selected) {
          setState(() {
            _currentConfig = config;
            _updateControllers();
          });
        }
      },
    );
  }

  Widget _buildBasicAnimations() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Basic Animations',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _buildAnimatedBox(
                  'Scale',
                  _scaleController,
                  (value) => Transform.scale(
                    scale: 0.5 + (value * 0.5),
                    child: Container(
                      width: 60,
                      height: 60,
                      decoration: BoxDecoration(
                        color: Colors.blue,
                        borderRadius: BorderRadius.circular(8),
                      ),
                    ),
                  ),
                ),
                _buildAnimatedBox(
                  'Rotation',
                  _rotationController,
                  (value) => Transform.rotate(
                    angle: value * 2 * 3.14159,
                    child: Container(
                      width: 60,
                      height: 60,
                      decoration: BoxDecoration(
                        color: Colors.red,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Icon(
                        Icons.star,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
                _buildAnimatedBox(
                  'Slide',
                  _slideController,
                  (value) => Transform.translate(
                    offset: Offset(0, (1 - value) * 50),
                    child: Container(
                      width: 60,
                      height: 60,
                      decoration: BoxDecoration(
                        color: Colors.green,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Icon(
                        Icons.arrow_upward,
                        color: Colors.white,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAnimatedBox(
    String label,
    SpringController controller,
    Widget Function(double) builder,
  ) {
    return Column(
      children: [
        SizedBox(
          height: 80,
          child: Center(
            child: AnimatedBuilder(
              animation: controller,
              builder: (context, child) => builder(controller.value),
            ),
          ),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: () {
            controller.toggleValue();
          },
          child: Text(label),
        ),
      ],
    );
  }

  Widget _buildTransitionWidgets() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Transition Widgets',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Text('Visible: '),
                Switch(
                  value: _isVisible,
                  onChanged: (value) {
                    setState(() {
                      _isVisible = value;
                    });
                  },
                ),
              ],
            ),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                SpringTransition(
                  visible: _isVisible,
                  config: _currentConfig,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.purple,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.favorite,
                      color: Colors.white,
                    ),
                  ),
                ),
                SpringSlideTransition(
                  visible: _isVisible,
                  direction: AxisDirection.up,
                  config: _currentConfig,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.orange,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.trending_up,
                      color: Colors.white,
                    ),
                  ),
                ),
                SpringRotationTransition(
                  visible: _isVisible,
                  turns: 0.25,
                  config: _currentConfig,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.teal,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.refresh,
                      color: Colors.white,
                    ),
                  ),
                ),
                SpringSizeTransition(
                  visible: _isVisible,
                  config: _currentConfig,
                  child: Container(
                    width: 50,
                    height: 50,
                    decoration: BoxDecoration(
                      color: Colors.pink,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: const Icon(
                      Icons.zoom_in,
                      color: Colors.white,
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInteractiveDemo() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Interactive Demo',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            Text(
              'Tap the button to toggle the spring animation:',
              style: Theme.of(context).textTheme.bodyMedium,
            ),
            const SizedBox(height: 16),
            Center(
              child: SpringAnimationBuilder(
                config: _currentConfig,
                target: _isToggled ? 1.0 : 0.0,
                builder: (context, value) {
                  // Clamp value to prevent assertion errors when spring overshoots
                  final clampedValue = value.clamp(0.0, 1.0);
                  return GestureDetector(
                    onTap: () {
                      setState(() {
                        _isToggled = !_isToggled;
                      });
                    },
                    child: Container(
                      width: 200,
                      height: 100,
                      decoration: BoxDecoration(
                        gradient: LinearGradient(
                          colors: [
                            Colors.blue,
                            Color.lerp(
                                Colors.blue, Colors.purple, clampedValue)!,
                          ],
                        ),
                        borderRadius: BorderRadius.circular(16),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black
                                .withValues(alpha: 0.2 * clampedValue),
                            blurRadius: 10 * clampedValue,
                            offset: Offset(0, 5 * clampedValue),
                          ),
                        ],
                      ),
                      child: Transform.scale(
                        scale: 0.9 + (0.1 * clampedValue),
                        child: Center(
                          child: Text(
                            _isToggled ? 'ON' : 'OFF',
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
            const SizedBox(height: 16),
            Text(
              'Animation Value: ${(_isToggled ? 1.0 : 0.0).toStringAsFixed(2)}',
              style: Theme.of(context).textTheme.bodySmall,
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildAdvancedControllers() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Controller Features',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: [
                ElevatedButton(
                  onPressed: () async {
                    // Chaining animations using await on animateTo (Future-based)
                    await _chainController.animateTo(1.0);
                    await _chainController.animateTo(0.5);
                    await _chainController.animateTo(0.0);
                  },
                  child: const Text('Run Chained Animation'),
                ),
                ElevatedButton(
                  onPressed: () {
                    _toggleController.toggleValue();
                    setState(() {});
                  },
                  child: Text(
                      'Toggle: ${_toggleController.value.toStringAsFixed(1)}'),
                ),
                ElevatedButton(
                  onPressed: () {
                    // Bounce controller is already auto-looping; reset for visibility.
                    _bounceController.reset();
                    _bounceController.animateTo(1.0);
                    setState(() {});
                  },
                  child: const Text('Kick Bounce'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            Text(
              'Update target mid-flight',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Slider(
              value: _dynamicTarget,
              min: 0.0,
              max: 1.0,
              divisions: 10,
              label: _dynamicTarget.toStringAsFixed(1),
              onChanged: (value) {
                setState(() {
                  _dynamicTarget = value;
                  _chainController.animateTo(value);
                });
              },
            ),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _buildControllerPreview(
                  label: 'Chain',
                  controller: _chainController,
                ),
                _buildControllerPreview(
                  label: 'Toggle',
                  controller: _toggleController,
                ),
                _buildControllerPreview(
                  label: 'Bounce',
                  controller: _bounceController,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControllerPreview({
    required String label,
    required SpringController controller,
  }) {
    return Column(
      children: [
        SizedBox(
          height: 60,
          width: 60,
          child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
              final value = controller.value.clamp(0.0, 1.0);
              return Transform.scale(
                scale: 0.5 + (value * 0.5),
                child: Container(
                  decoration: BoxDecoration(
                    color: Colors.blue.withValues(alpha: 0.3 + 0.4 * value),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Center(
                    child: Text(
                      value.toStringAsFixed(2),
                      style: const TextStyle(fontSize: 12),
                    ),
                  ),
                ),
              );
            },
          ),
        ),
        const SizedBox(height: 8),
        Text(label),
      ],
    );
  }
}
1
likes
160
points
117
downloads

Publisher

verified publisherbechattaoui.dev

Weekly Downloads

A Flutter package for creating smooth spring-based animations with customizable damping, stiffness, and velocity parameters.

Repository (GitHub)
View/report issues

Topics

#animation #spring #physics #ui #flutter

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

BSD-3-Clause (license)

Dependencies

flutter

More

Packages that depend on flutter_spring_animation