magic_animations 0.2.0
magic_animations: ^0.2.0 copied to clipboard
Beautiful, easy-to-use Flutter animations. Chainable entrance, interactive, and GLSL shader effects with a dead-simple API.
import 'package:magic_animations/magic_animations.dart';
import 'package:flutter/material.dart';
void main() => runApp(const GalleryApp());
class GalleryApp extends StatelessWidget {
const GalleryApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'magic_animations gallery',
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(
useMaterial3: true,
).copyWith(scaffoldBackgroundColor: const Color(0xFF11131A)),
home: const GalleryHome(),
);
}
}
class GalleryHome extends StatelessWidget {
const GalleryHome({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('magic_animations'), centerTitle: false),
body: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 40),
children: [
const _SectionHeader('Entrance'),
ReplayableDemo(
title: 'fadeIn()',
code: "Text('Hello').fadeIn()",
builder: (c) => const _Chip(
'Fade',
).fadeIn(trigger: AnimateTrigger.manual, controller: c),
),
ReplayableDemo(
title: 'slideUp()',
code: 'widget.slideUp()',
builder: (c) => const _Chip(
'Slide up',
).slideUp(trigger: AnimateTrigger.manual, controller: c),
),
ReplayableDemo(
title: 'scaleIn()',
code: 'widget.scaleIn()',
builder: (c) => const _Chip(
'Scale',
).scaleIn(trigger: AnimateTrigger.manual, controller: c),
),
ReplayableDemo(
title: 'blurIn()',
code: 'widget.blurIn()',
builder: (c) => const _Chip(
'Blur',
).blurIn(trigger: AnimateTrigger.manual, controller: c),
),
ReplayableDemo(
title: 'flipIn()',
code: 'widget.flipIn()',
builder: (c) => const _Chip(
'Flip',
).flipIn(trigger: AnimateTrigger.manual, controller: c),
),
ReplayableDemo(
title: 'printIn() — unroll through a slot',
code: 'receipt.printIn(showSlot: true)',
builder: (c) => const _Receipt().printIn(
showSlot: true,
trigger: AnimateTrigger.manual,
controller: c,
),
),
const _StaticDemo(
title: 'shimmer()',
code: 'skeleton.shimmer()',
child: Shimmer(child: _Chip('Loading…', color: Color(0xFFE0E0E0))),
),
ReplayableDemo(
title: 'CountUp',
code: 'CountUp(end: 1024, onComplete: ...)',
builder: (c) => CountUp(
end: 1024,
trigger: AnimateTrigger.manual,
controller: c,
style: const TextStyle(
fontSize: 34,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
),
ReplayableDemo(
title: 'Typewriter',
code: "Typewriter(text: '...', cursor: '|')",
builder: (c) => Typewriter(
text: 'Welcome to magic_animations!',
cursor: '|',
trigger: AnimateTrigger.manual,
controller: c,
style: const TextStyle(
fontSize: 18,
fontFamily: 'monospace',
color: Colors.white,
),
),
),
const _StaggerDemo(),
const _SectionHeader('Interactive'),
const _StaticDemo(
title: 'pressScale() — press me',
code: 'widget.pressScale(haptics: true)',
wrap: _Wrap.press,
child: _Chip('Press me', color: Color(0xFF4C6FFF)),
),
const _StaticDemo(
title: 'bouncyTap() — tap me',
code: 'widget.bouncyTap()',
wrap: _Wrap.bouncy,
child: _Chip('Tap me', color: Color(0xFFFF6CAB)),
),
const _StaticDemo(
title: 'rippleTap() — tap me',
code: 'widget.rippleTap()',
wrap: _Wrap.ripple,
child: _Chip('Ripple', color: Color(0xFF23A455)),
),
const _StaticDemo(
title: 'jiggle()',
code: 'icon.jiggle()',
wrap: _Wrap.jiggle,
child: _Chip('Jiggle', color: Color(0xFFFF9F43)),
),
const _SectionHeader('Shaders (GPU)'),
const _StaticDemo(
title: 'glow()',
code: 'Icon(Icons.star).glow()',
wrap: _Wrap.glow,
child: Padding(
padding: EdgeInsets.all(20),
child: Icon(Icons.star_rounded, size: 56, color: Colors.white),
),
),
const _StaticDemo(
title: 'gradientFlow()',
code: "Text('magic_animations').gradientFlow()",
wrap: _Wrap.gradient,
child: Text(
'magic_animations',
style: TextStyle(fontSize: 34, fontWeight: FontWeight.w800),
),
),
const _StaticDemo(
title: 'liquid()',
code: 'card.liquid()',
wrap: _Wrap.liquid,
child: SizedBox(
width: 220,
height: 90,
child: Center(
child: Text(
'Liquid',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
),
ReplayableDemo(
title: 'dissolve()',
code: 'widget.dissolve()',
builder: (c) => const _Chip(
'Dissolve',
color: Color(0xFF7C4DFF),
).dissolve(trigger: AnimateTrigger.manual, controller: c),
),
],
),
);
}
}
/// Chooses how a [_StaticDemo] wraps its child (for the interactive/shader
/// demos that don't use a replay controller).
enum _Wrap { none, press, bouncy, ripple, jiggle, glow, gradient, liquid }
class _SectionHeader extends StatelessWidget {
const _SectionHeader(this.label);
final String label;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 28, bottom: 8),
child: Text(
label.toUpperCase(),
style: const TextStyle(
fontSize: 12,
letterSpacing: 2,
fontWeight: FontWeight.w700,
color: Color(0xFF8A90A6),
),
),
);
}
}
/// A demo card with a Replay button, driven by an [AnimateController].
class ReplayableDemo extends StatefulWidget {
const ReplayableDemo({
super.key,
required this.title,
required this.code,
required this.builder,
});
final String title;
final String code;
final Widget Function(AnimateController controller) builder;
@override
State<ReplayableDemo> createState() => _ReplayableDemoState();
}
class _ReplayableDemoState extends State<ReplayableDemo> {
final AnimateController _controller = AnimateController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _controller.play());
}
void _replay() {
_controller.reset();
_controller.play();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _DemoCard(
title: widget.title,
code: widget.code,
trailing: IconButton(
icon: const Icon(Icons.replay_rounded),
tooltip: 'Replay',
onPressed: _replay,
),
child: widget.builder(_controller),
);
}
}
/// A demo card whose child animates on its own (loop / on tap / continuous).
class _StaticDemo extends StatelessWidget {
const _StaticDemo({
required this.title,
required this.code,
required this.child,
this.wrap = _Wrap.none,
});
final String title;
final String code;
final Widget child;
final _Wrap wrap;
Widget _wrapped() {
switch (wrap) {
case _Wrap.none:
return child;
case _Wrap.press:
return child.pressScale(haptics: true);
case _Wrap.bouncy:
return child.bouncyTap(haptics: true);
case _Wrap.ripple:
return ClipRRect(
borderRadius: BorderRadius.circular(14),
child: child.rippleTap(color: const Color(0x55FFFFFF)),
);
case _Wrap.jiggle:
return child.jiggle();
case _Wrap.glow:
return child.glow();
case _Wrap.gradient:
return child.gradientFlow();
case _Wrap.liquid:
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: child.liquid(),
);
}
}
@override
Widget build(BuildContext context) {
return _DemoCard(title: title, code: code, child: _wrapped());
}
}
/// Demos [Stagger] by remounting a fresh `Stagger.list` (with a new [Key])
/// on replay, so each item's `onAppear` cascade plays again from scratch.
class _StaggerDemo extends StatefulWidget {
const _StaggerDemo();
@override
State<_StaggerDemo> createState() => _StaggerDemoState();
}
class _StaggerDemoState extends State<_StaggerDemo> {
int _replayKey = 0;
@override
Widget build(BuildContext context) {
return _DemoCard(
title: 'Stagger — cascades a list',
code: "Stagger.list([chipA, chipB, chipC])",
trailing: IconButton(
icon: const Icon(Icons.replay_rounded),
tooltip: 'Replay',
onPressed: () => setState(() => _replayKey++),
),
child: Row(
key: ValueKey(_replayKey),
mainAxisAlignment: MainAxisAlignment.center,
children:
Stagger.list([
const _Chip('One', color: Color(0xFF4C6FFF)).scaleIn(),
const _Chip('Two', color: Color(0xFF23A455)).scaleIn(),
const _Chip('Three', color: Color(0xFFFF6CAB)).scaleIn(),
], interval: const Duration(milliseconds: 120))
.map(
(w) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: w,
),
)
.toList(),
),
);
}
}
class _DemoCard extends StatelessWidget {
const _DemoCard({
required this.title,
required this.code,
required this.child,
this.trailing,
});
final String title;
final String code;
final Widget child;
final Widget? trailing;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
color: const Color(0xFF1A1D28),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: const Color(0xFF262A38)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 0),
child: Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
),
?trailing,
],
),
),
Container(
constraints: const BoxConstraints(minHeight: 150),
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 12),
child: child,
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
code,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12.5,
color: Color(0xFF8A90A6),
),
),
),
),
],
),
);
}
}
/// A little skeuomorphic receipt used to show off [PrintIn].
class _Receipt extends StatelessWidget {
const _Receipt();
@override
Widget build(BuildContext context) {
return Container(
width: 200,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: const BoxDecoration(
color: Color(0xFFF7F5EF),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(6)),
),
child: const DefaultTextStyle(
style: TextStyle(color: Color(0xFF222222), fontFamily: 'monospace'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
'RUN RECEIPT',
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
SizedBox(height: 10),
_Row('Distance', '5.02 km'),
_Row('Time', '24:18'),
_Row('Pace', '4:50 /km'),
_Row('Calories', '312 kcal'),
SizedBox(height: 10),
Center(child: Text('— thank you —')),
],
),
),
);
}
}
class _Row extends StatelessWidget {
const _Row(this.label, this.value);
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text(label), Text(value)],
),
);
}
}
/// A simple pill used as demo content.
class _Chip extends StatelessWidget {
const _Chip(this.label, {this.color = const Color(0xFF2D3142)});
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 14),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(14),
),
child: Text(
label,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
);
}
}