flutter_phase_animator 0.0.1
flutter_phase_animator: ^0.0.1 copied to clipboard
A Flutter animation package inspired by SwiftUI's PhaseAnimator. Chain steps sequentially or loop through phases declaratively
import 'package:flutter/material.dart';
import 'package:flutter_phase_animator/flutter_phase_animator.dart';
void main() {
runApp(const PhaseAnimatorApp());
}
class PhaseAnimatorApp extends StatelessWidget {
const PhaseAnimatorApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Phase Animator Demo',
theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
home: const _PhaseAnimatorScreen(),
);
}
}
class _PhaseAnimatorScreen extends StatefulWidget {
const _PhaseAnimatorScreen();
@override
State<_PhaseAnimatorScreen> createState() => _PhaseAnimatorScreenState();
}
class _PhaseAnimatorScreenState extends State<_PhaseAnimatorScreen> {
final _sequencerKey = GlobalKey<PhaseAnimatorState>();
bool _trigger = false;
bool _loopPaused = false;
void _replayChain() {
setState(() => _trigger = false);
Future.microtask(() {
setState(() => _trigger = true);
_sequencerKey.currentState?.replay();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0D0D0D),
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('PhaseAnimator Demo'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Section 1: Chained ──────────────────────
_sectionLabel('PhaseAnimator (chained)'),
const SizedBox(height: 16),
Center(
child: PhaseAnimator(
key: _sequencerKey,
autoPlay: true,
steps: [
// Step 1: fade in
AnimationStep(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
builder: (ctx, t, child) =>
Opacity(opacity: t, child: child),
),
// Step 2: slide up
AnimationStep(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOut,
builder: (ctx, t, child) => Transform.translate(
offset: Offset(0, (1 - t) * 40),
child: child,
),
),
// Step 3: scale bounce
AnimationStep(
duration: const Duration(milliseconds: 300),
curve: Curves.elasticOut,
builder: (ctx, t, child) =>
Transform.scale(scale: 0.8 + t * 0.2, child: child),
),
],
child: Container(
width: 180,
height: 180,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6C63FF), Color(0xFFFF6584)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: const Color(0xFF6C63FF).withOpacity(0.5),
blurRadius: 32,
offset: const Offset(0, 8),
),
],
),
child: const Center(
child: Text('🚀', style: TextStyle(fontSize: 56)),
),
),
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: _replayChain,
icon: const Icon(Icons.replay),
label: const Text('Replay Sequence'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
),
),
),
const SizedBox(height: 48),
//── Section 2: Looping ──────────────────────
_sectionLabel('PhaseAnimatorLooping (phase-based)'),
const SizedBox(height: 16),
Center(
child: PhaseAnimatorLooping(
paused: _loopPaused,
phaseDuration: const Duration(milliseconds: 700),
curve: Curves.easeInOut,
phases: const [
Phase(opacity: 1.0, scale: 1.0, offset: Offset(0, 0)),
Phase(opacity: 0.6, scale: 1.3, offset: Offset(0, -20)),
Phase(opacity: 1.0, scale: 0.9, offset: Offset(0, 10)),
Phase(opacity: 0.8, scale: 1.1, offset: Offset(0, -5)),
Phase(opacity: 1.0, scale: 0.6, offset: Offset(0, 10)),
Phase(opacity: 0.3, scale: 1.5, offset: Offset(0, -5)),
],
builder: (ctx, phase, child) => Opacity(
opacity: phase.opacity,
child: Transform.translate(
offset: phase.offset,
child: Transform.scale(scale: phase.scale, child: child),
),
),
child: Container(
width: 140,
height: 140,
decoration: BoxDecoration(
color: const Color(0xFFFF6584),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFFFF6584).withOpacity(0.5),
blurRadius: 40,
spreadRadius: 4,
),
],
),
child: const Center(
child: Text('✨', style: TextStyle(fontSize: 48)),
),
),
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: () => setState(() => _loopPaused = !_loopPaused),
icon: Icon(_loopPaused ? Icons.play_arrow : Icons.pause),
label: Text(_loopPaused ? 'Resume' : 'Pause'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF6584),
foregroundColor: Colors.white,
),
),
),
const SizedBox(height: 48),
// ── Section 3: Combined ─────────────────────
_sectionLabel('Combined — enter sequence + looping idle'),
const SizedBox(height: 16),
const _CombinedDemo(),
const SizedBox(height: 32),
],
),
),
);
}
Widget _sectionLabel(String text) => Text(
text,
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
fontWeight: FontWeight.w600,
letterSpacing: 0.5,
),
);
}
/// Shows how to chain PhaseAnimator into PhaseAnimatorLooping:
/// entrance sequence plays once, then the looping idle starts.
class _CombinedDemo extends StatefulWidget {
const _CombinedDemo();
@override
State<_CombinedDemo> createState() => _CombinedDemoState();
}
class _CombinedDemoState extends State<_CombinedDemo> {
bool _entranceDone = false;
@override
Widget build(BuildContext context) {
return Center(
child: PhaseAnimator(
autoPlay: true,
onComplete: () => setState(() => _entranceDone = true),
steps: [
AnimationStep(
duration: const Duration(milliseconds: 600),
curve: Curves.easeOut,
builder: (ctx, t, child) => Opacity(opacity: t, child: child),
),
AnimationStep(
duration: const Duration(milliseconds: 2500),
curve: Curves.elasticOut,
builder: (ctx, t, child) => Transform.scale(scale: t, child: child),
),
],
child: PhaseAnimatorLooping(
paused: !_entranceDone,
phaseDuration: const Duration(milliseconds: 800),
phases: const [
Phase(scale: 1.0, rotation: 0.0),
Phase(scale: 1.05, rotation: 0.05),
Phase(scale: 1.0, rotation: -0.05),
Phase(scale: 0.97, rotation: 0.0),
],
builder: (ctx, phase, child) => Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..scale(phase.scale)
..rotateZ(phase.rotation),
child: child,
),
child: Container(
width: 160,
height: 160,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF43E97B), Color(0xFF38F9D7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(32),
boxShadow: [
BoxShadow(
color: const Color(0xFF43E97B).withOpacity(0.4),
blurRadius: 40,
spreadRadius: 4,
),
],
),
child: const Center(
child: Text('🎯', style: TextStyle(fontSize: 52)),
),
),
),
),
);
}
}