flutter_spring_animation 0.1.0
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.
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),
],
);
}
}