fly_to_target 1.0.0
fly_to_target: ^1.0.0 copied to clipboard
A Flutter package for animating multiple widgets flying to target positions simultaneously with customizable paths and effects.
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fly_to_target/fly_to_target.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fly To Target Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.amber),
useMaterial3: true,
),
home: const DemoPage(),
);
}
}
class DemoPage extends StatelessWidget {
const DemoPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fly To Target Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: ListView(
children: [
_DemoTile(
title: 'Basic Animation',
subtitle: 'Simple linear animation',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const BasicExample()),
),
),
_DemoTile(
title: 'Coin Animation',
subtitle: 'Multiple coins with effects',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CoinExample()),
),
),
_DemoTile(
title: 'Cart Animation',
subtitle: 'Add to cart with stagger effect',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CartExample()),
),
),
_DemoTile(
title: 'Multiple Targets',
subtitle: 'Items fly to different destinations',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const MultipleTargetsExample()),
),
),
const Divider(),
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Custom Animations',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
),
_DemoTile(
title: 'Custom Path',
subtitle: 'Spiral and wave trajectories',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CustomPathExample()),
),
),
_DemoTile(
title: 'Decoration Effects',
subtitle: 'Feathers, particles, and sparkles',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const DecorationExample()),
),
),
_DemoTile(
title: 'Full Effects',
subtitle: 'Rotation, scale, fade combined',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullEffectsExample()),
),
),
_DemoTile(
title: 'Heart Burst',
subtitle: 'Hearts flying with love',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const HeartBurstExample()),
),
),
_DemoTile(
title: 'Game Rewards',
subtitle: 'Stars and gems collection',
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const GameRewardsExample()),
),
),
],
),
);
}
}
class _DemoTile extends StatelessWidget {
final String title;
final String subtitle;
final VoidCallback onTap;
const _DemoTile({
required this.title,
required this.subtitle,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(title),
subtitle: Text(subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: onTap,
);
}
}
/// 基本的なアニメーション例
class BasicExample extends StatefulWidget {
const BasicExample({super.key});
@override
State<BasicExample> createState() => _BasicExampleState();
}
class _BasicExampleState extends State<BasicExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _targetKey = GlobalKey();
final List<GlobalKey> _itemKeys = List.generate(5, (_) => GlobalKey());
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _flyAll() async {
final items = _itemKeys.map((key) {
return FlyItem.fromKey(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: const Icon(Icons.star, color: Colors.white, size: 24),
),
key: key,
);
}).toList();
await _controller.flyAll(
items: items,
target: FlyTargetFromKey(_targetKey),
config: const FlyAnimationConfig(
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
staggerDelay: Duration(milliseconds: 100),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic Animation'),
actions: [
Container(
key: _targetKey,
padding: const EdgeInsets.all(8),
child: const Icon(Icons.star_border, size: 32),
),
],
),
body: Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: List.generate(5, (index) {
return Container(
key: _itemKeys[index],
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.star, color: Colors.blue),
);
}),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _flyAll,
icon: const Icon(Icons.play_arrow),
label: const Text('Fly All'),
),
);
}
}
/// コインアニメーション例
class CoinExample extends StatefulWidget {
const CoinExample({super.key});
@override
State<CoinExample> createState() => _CoinExampleState();
}
class _CoinExampleState extends State<CoinExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _walletKey = GlobalKey();
int _coinCount = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _collectCoins() async {
final random = Random();
final screenSize = MediaQuery.of(context).size;
// ランダムな位置からコインを生成
final coinPositions = List.generate(8, (_) {
return Offset(
50 + random.nextDouble() * (screenSize.width - 100),
200 + random.nextDouble() * (screenSize.height - 400),
);
});
final items = coinPositions.map((offset) {
return FlyItem.fromOffset(
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.amber.shade300, Colors.orange.shade600],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.orange.withValues(alpha: 0.5),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Center(
child: Text(
'\$',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
offset: offset,
size: const Size(40, 40),
);
}).toList();
await _controller.flyAll(
items: items,
target: FlyTargetFromKey(_walletKey),
config: FlyAnimationConfig.coin(
duration: const Duration(milliseconds: 1000),
staggerDelay: const Duration(milliseconds: 60),
),
);
setState(() {
_coinCount += items.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Coin Animation'),
actions: [
Container(
key: _walletKey,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
const Icon(Icons.wallet, size: 28),
const SizedBox(width: 8),
Text(
'$_coinCount',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
body: const Center(
child: Text(
'Tap the button to collect coins!',
style: TextStyle(fontSize: 18),
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _collectCoins,
icon: const Icon(Icons.monetization_on),
label: const Text('Collect Coins'),
),
);
}
}
/// カートへ追加アニメーション例
class CartExample extends StatefulWidget {
const CartExample({super.key});
@override
State<CartExample> createState() => _CartExampleState();
}
class _CartExampleState extends State<CartExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _cartKey = GlobalKey();
final Map<int, GlobalKey> _productKeys = {};
int _cartCount = 0;
@override
void initState() {
super.initState();
for (var i = 0; i < 6; i++) {
_productKeys[i] = GlobalKey();
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _addToCart(int index) async {
final key = _productKeys[index]!;
await _controller.fly(
item: FlyItem.fromKey(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.shopping_bag, color: Colors.white),
),
key: key,
),
target: FlyTargetFromKey(_cartKey),
config: const FlyAnimationConfig(
duration: Duration(milliseconds: 500),
curve: Curves.easeOut,
pathConfig: ParabolicPathConfig(height: -80),
effects: FlyEffects(
scale: ScaleEffect(endScale: 0.5),
fade: FadeEffect(startAt: 0.7),
),
),
);
setState(() {
_cartCount++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Cart Animation'),
actions: [
Stack(
children: [
Container(
key: _cartKey,
padding: const EdgeInsets.all(12),
child: const Icon(Icons.shopping_cart, size: 28),
),
if (_cartCount > 0)
Positioned(
right: 4,
top: 4,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Text(
'$_cartCount',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: 6,
itemBuilder: (context, index) {
return Card(
child: InkWell(
onTap: () => _addToCart(index),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
key: _productKeys[index],
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors
.primaries[index % Colors.primaries.length].shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.shopping_bag,
color: Colors.primaries[index % Colors.primaries.length],
size: 32,
),
),
const SizedBox(height: 8),
Text('Product ${index + 1}'),
const SizedBox(height: 4),
const Text(
'Tap to add',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
),
);
},
),
);
}
}
/// 複数目的地への同時アニメーション例
class MultipleTargetsExample extends StatefulWidget {
const MultipleTargetsExample({super.key});
@override
State<MultipleTargetsExample> createState() => _MultipleTargetsExampleState();
}
class _MultipleTargetsExampleState extends State<MultipleTargetsExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _target1Key = GlobalKey();
final _target2Key = GlobalKey();
final _target3Key = GlobalKey();
final List<GlobalKey> _itemKeys = List.generate(9, (_) => GlobalKey());
final List<int> _targetCounts = [0, 0, 0];
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _distributeItems() async {
final targets = [_target1Key, _target2Key, _target3Key];
final colors = [Colors.red, Colors.green, Colors.blue];
final itemsWithTargets = _itemKeys.asMap().entries.map((entry) {
final index = entry.key;
final key = entry.value;
final targetIndex = index % 3;
return FlyItemWithTarget(
item: FlyItem.fromKey(
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: colors[targetIndex],
shape: BoxShape.circle,
),
),
key: key,
id: 'item_$index',
),
target: FlyTargetFromKey(targets[targetIndex]),
);
}).toList();
await _controller.flyToTargets(
itemsWithTargets: itemsWithTargets,
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 800),
staggerDelay: const Duration(milliseconds: 50),
pathConfig: BezierPathConfig.auto(
curvature: 0.4,
randomness: 0.1,
),
effects: const FlyEffects(
scale: ScaleEffect(endScale: 0.5, startAt: 0.6),
fade: FadeEffect(startAt: 0.8),
),
),
);
setState(() {
_targetCounts[0] += 3;
_targetCounts[1] += 3;
_targetCounts[2] += 3;
});
}
Widget _buildTarget(GlobalKey key, Color color, int count) {
return Container(
key: key,
width: 60,
height: 60,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.2),
shape: BoxShape.circle,
border: Border.all(color: color, width: 2),
),
child: Center(
child: Text(
'$count',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: color,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multiple Targets'),
),
body: Column(
children: [
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildTarget(_target1Key, Colors.red, _targetCounts[0]),
_buildTarget(_target2Key, Colors.green, _targetCounts[1]),
_buildTarget(_target3Key, Colors.blue, _targetCounts[2]),
],
),
const SizedBox(height: 40),
Expanded(
child: Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: List.generate(9, (index) {
final colors = [Colors.red, Colors.green, Colors.blue];
return Container(
key: _itemKeys[index],
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors[index % 3].shade200,
shape: BoxShape.circle,
),
);
}),
),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _distributeItems,
icon: const Icon(Icons.send),
label: const Text('Distribute'),
),
);
}
}
/// カスタム軌道アニメーション例
class CustomPathExample extends StatefulWidget {
const CustomPathExample({super.key});
@override
State<CustomPathExample> createState() => _CustomPathExampleState();
}
class _CustomPathExampleState extends State<CustomPathExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _targetKey = GlobalKey();
String _selectedPath = 'spiral';
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
PathConfig _getPathConfig() {
return switch (_selectedPath) {
'spiral' => CustomPathConfig(
pathFunction: (t, start, end) {
// 螺旋軌道
final angle = t * 4 * pi;
final radius = (1 - t) * 100;
final linearX = start.dx + (end.dx - start.dx) * t;
final linearY = start.dy + (end.dy - start.dy) * t;
return Offset(
linearX + cos(angle) * radius,
linearY + sin(angle) * radius,
);
},
),
'wave' => CustomPathConfig(
pathFunction: (t, start, end) {
// 波形軌道
final linearX = start.dx + (end.dx - start.dx) * t;
final linearY = start.dy + (end.dy - start.dy) * t;
final wave = sin(t * 6 * pi) * 50 * (1 - t);
return Offset(linearX, linearY + wave);
},
),
'zigzag' => CustomPathConfig(
pathFunction: (t, start, end) {
// ジグザグ軌道
final linearX = start.dx + (end.dx - start.dx) * t;
final linearY = start.dy + (end.dy - start.dy) * t;
final segments = 5;
final segmentT = (t * segments) % 1.0;
final zigzag = (segmentT < 0.5 ? segmentT * 2 : 2 - segmentT * 2) *
80 *
(1 - t);
return Offset(linearX + zigzag, linearY);
},
),
'bounce' => CustomPathConfig(
pathFunction: (t, start, end) {
// バウンス軌道
final linearX = start.dx + (end.dx - start.dx) * t;
final linearY = start.dy + (end.dy - start.dy) * t;
final bounceHeight = sin(t * pi) * 150 * pow(1 - t, 0.5);
return Offset(linearX, linearY - bounceHeight);
},
),
_ => const LinearPathConfig(),
};
}
Future<void> _flyWithCustomPath() async {
final screenSize = MediaQuery.of(context).size;
final random = Random();
final items = List.generate(5, (i) {
return FlyItem.fromOffset(
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [
Colors.purple.shade300,
Colors.purple.shade700,
],
),
shape: BoxShape.circle,
),
child: const Icon(Icons.auto_awesome, color: Colors.white, size: 20),
),
offset: Offset(
50 + random.nextDouble() * (screenSize.width - 100),
screenSize.height * 0.6 + random.nextDouble() * 100,
),
size: const Size(36, 36),
);
});
await _controller.flyAll(
items: items,
target: FlyTargetFromKey(_targetKey),
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 1500),
curve: Curves.easeInOut,
staggerDelay: const Duration(milliseconds: 100),
pathConfig: _getPathConfig(),
effects: const FlyEffects(
rotation: RotationEffect(rotations: 2 * pi),
scale: ScaleEffect(endScale: 0.6, startAt: 0.5),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Path'),
actions: [
Container(
key: _targetKey,
padding: const EdgeInsets.all(12),
child: const Icon(Icons.flag, size: 32),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
children: [
_PathChip(
label: 'Spiral',
selected: _selectedPath == 'spiral',
onTap: () => setState(() => _selectedPath = 'spiral'),
),
_PathChip(
label: 'Wave',
selected: _selectedPath == 'wave',
onTap: () => setState(() => _selectedPath = 'wave'),
),
_PathChip(
label: 'Zigzag',
selected: _selectedPath == 'zigzag',
onTap: () => setState(() => _selectedPath = 'zigzag'),
),
_PathChip(
label: 'Bounce',
selected: _selectedPath == 'bounce',
onTap: () => setState(() => _selectedPath = 'bounce'),
),
],
),
),
const Expanded(
child: Center(
child: Text('Select a path type and tap Play'),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _flyWithCustomPath,
icon: const Icon(Icons.play_arrow),
label: const Text('Play'),
),
);
}
}
class _PathChip extends StatelessWidget {
final String label;
final bool selected;
final VoidCallback onTap;
const _PathChip({
required this.label,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return FilterChip(
label: Text(label),
selected: selected,
onSelected: (_) => onTap(),
);
}
}
/// 装飾エフェクトアニメーション例
class DecorationExample extends StatefulWidget {
const DecorationExample({super.key});
@override
State<DecorationExample> createState() => _DecorationExampleState();
}
class _DecorationExampleState extends State<DecorationExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _targetKey = GlobalKey();
String _selectedDecoration = 'feather';
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
List<DecorationConfig> _getDecorations() {
return switch (_selectedDecoration) {
'feather' => [
FeatherDecorationConfig(
count: 6,
colors: [
Colors.white,
Colors.pink.shade100,
Colors.purple.shade100,
],
size: const Size(12, 24),
spread: 40,
flutter: 1.5,
),
],
'particle' => [
ParticleDecorationConfig(
count: 20,
colors: [
Colors.orange,
Colors.yellow,
Colors.red,
],
minSize: 3,
maxSize: 8,
speed: 1.5,
),
],
'sparkle' => [
const SparkleDecorationConfig(
count: 8,
size: 12,
color: Colors.amber,
intensity: 1.0,
blinkSpeed: 3.0,
),
],
'all' => [
FeatherDecorationConfig(
count: 4,
colors: [Colors.white, Colors.pink.shade100],
size: const Size(10, 20),
),
ParticleDecorationConfig(
count: 15,
colors: [Colors.orange, Colors.yellow],
minSize: 2,
maxSize: 6,
),
const SparkleDecorationConfig(
count: 6,
size: 10,
color: Colors.amber,
),
],
_ => [],
};
}
Future<void> _flyWithDecoration() async {
final screenSize = MediaQuery.of(context).size;
final item = FlyItem.fromOffset(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.pink.shade300, Colors.purple.shade500],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.purple.withValues(alpha: 0.5),
blurRadius: 12,
),
],
),
child: const Icon(Icons.favorite, color: Colors.white, size: 28),
),
offset: Offset(screenSize.width / 2, screenSize.height * 0.7),
size: const Size(50, 50),
);
await _controller.fly(
item: item,
target: FlyTargetFromKey(_targetKey),
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 1200),
curve: Curves.easeOut,
pathConfig: const ParabolicPathConfig(height: -120),
effects: const FlyEffects(
rotation: RotationEffect(rotations: pi),
),
decorations: _getDecorations(),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Decoration Effects'),
actions: [
Container(
key: _targetKey,
padding: const EdgeInsets.all(12),
child: const Icon(Icons.catching_pokemon, size: 32),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8,
children: [
_PathChip(
label: '🪶 Feather',
selected: _selectedDecoration == 'feather',
onTap: () => setState(() => _selectedDecoration = 'feather'),
),
_PathChip(
label: '✨ Particle',
selected: _selectedDecoration == 'particle',
onTap: () => setState(() => _selectedDecoration = 'particle'),
),
_PathChip(
label: '⭐ Sparkle',
selected: _selectedDecoration == 'sparkle',
onTap: () => setState(() => _selectedDecoration = 'sparkle'),
),
_PathChip(
label: '🎆 All',
selected: _selectedDecoration == 'all',
onTap: () => setState(() => _selectedDecoration = 'all'),
),
],
),
),
const Expanded(
child: Center(
child: Text('Select decoration type and tap Play'),
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _flyWithDecoration,
icon: const Icon(Icons.play_arrow),
label: const Text('Play'),
),
);
}
}
/// フルエフェクトアニメーション例
class FullEffectsExample extends StatefulWidget {
const FullEffectsExample({super.key});
@override
State<FullEffectsExample> createState() => _FullEffectsExampleState();
}
class _FullEffectsExampleState extends State<FullEffectsExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _targetKey = GlobalKey();
double _rotationSpeed = 2.0;
double _endScale = 0.3;
double _fadeStart = 0.7;
bool _clockwise = true;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _flyWithEffects() async {
final screenSize = MediaQuery.of(context).size;
final items = List.generate(6, (i) {
final hue = (i / 6 * 360).toDouble();
return FlyItem.fromOffset(
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: HSVColor.fromAHSV(1, hue, 0.7, 0.9).toColor(),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: HSVColor.fromAHSV(0.5, hue, 0.7, 0.9).toColor(),
blurRadius: 8,
),
],
),
child: Center(
child: Text(
'${i + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
),
offset: Offset(
40 + (i % 3) * (screenSize.width - 80) / 2,
screenSize.height * 0.5 + (i ~/ 3) * 80,
),
size: const Size(44, 44),
);
});
await _controller.flyAll(
items: items,
target: FlyTargetFromKey(_targetKey),
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 1000),
curve: Curves.easeInOut,
staggerDelay: const Duration(milliseconds: 80),
pathConfig: BezierPathConfig.auto(curvature: 0.5, randomness: 0.2),
effects: FlyEffects(
rotation: RotationEffect(
rotations: _rotationSpeed * pi,
direction: _clockwise
? RotationDirection.clockwise
: RotationDirection.counterClockwise,
),
scale: ScaleEffect(
startScale: 1.0,
endScale: _endScale,
startAt: 0.4,
),
fade: FadeEffect(
startOpacity: 1.0,
endOpacity: 0.0,
startAt: _fadeStart,
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Full Effects'),
actions: [
Container(
key: _targetKey,
padding: const EdgeInsets.all(12),
child: const Icon(Icons.blur_on, size: 32),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Rotation Speed',
style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _rotationSpeed,
min: 0,
max: 6,
divisions: 12,
label: '${_rotationSpeed.toStringAsFixed(1)}π',
onChanged: (v) => setState(() => _rotationSpeed = v),
),
Row(
children: [
const Text('Direction: '),
ChoiceChip(
label: const Text('Clockwise'),
selected: _clockwise,
onSelected: (_) => setState(() => _clockwise = true),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('Counter'),
selected: !_clockwise,
onSelected: (_) => setState(() => _clockwise = false),
),
],
),
const SizedBox(height: 16),
const Text('End Scale',
style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _endScale,
min: 0.1,
max: 1.0,
divisions: 9,
label: _endScale.toStringAsFixed(1),
onChanged: (v) => setState(() => _endScale = v),
),
const SizedBox(height: 16),
const Text('Fade Start',
style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _fadeStart,
min: 0,
max: 1,
divisions: 10,
label: _fadeStart.toStringAsFixed(1),
onChanged: (v) => setState(() => _fadeStart = v),
),
const SizedBox(height: 32),
Center(
child: ElevatedButton.icon(
onPressed: _flyWithEffects,
icon: const Icon(Icons.play_arrow),
label: const Text('Play Animation'),
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
),
),
],
),
),
);
}
}
/// ハートバーストアニメーション例
class HeartBurstExample extends StatefulWidget {
const HeartBurstExample({super.key});
@override
State<HeartBurstExample> createState() => _HeartBurstExampleState();
}
class _HeartBurstExampleState extends State<HeartBurstExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _targetKey = GlobalKey();
final _sourceKey = GlobalKey();
int _heartCount = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _burstHearts() async {
// ソース位置を取得
final sourceBox =
_sourceKey.currentContext?.findRenderObject() as RenderBox?;
if (sourceBox == null) return;
final sourcePosition = sourceBox.localToGlobal(
Offset(sourceBox.size.width / 2, sourceBox.size.height / 2),
);
final random = Random();
final heartColors = [
Colors.red,
Colors.pink,
Colors.purple,
Colors.deepPurple,
];
final items = List.generate(12, (i) {
final color = heartColors[random.nextInt(heartColors.length)];
final size = 24.0 + random.nextDouble() * 20;
return FlyItem.fromOffset(
child: Icon(
Icons.favorite,
color: color,
size: size,
shadows: [
Shadow(
color: color.withValues(alpha: 0.5),
blurRadius: 8,
),
],
),
offset: Offset(
sourcePosition.dx + (random.nextDouble() - 0.5) * 60,
sourcePosition.dy + (random.nextDouble() - 0.5) * 40,
),
size: Size(size, size),
);
});
await _controller.flyAll(
items: items,
target: FlyTargetFromKey(_targetKey),
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 1200),
curve: Curves.easeOutCubic,
staggerDelay: const Duration(milliseconds: 40),
pathConfig: BezierPathConfig.auto(curvature: 0.6, randomness: 0.3),
effects: const FlyEffects(
rotation: RotationEffect(
rotations: pi / 2,
direction: RotationDirection.counterClockwise,
),
scale: ScaleEffect(
startScale: 1.2,
endScale: 0.4,
startAt: 0.3,
),
fade: FadeEffect(startAt: 0.75),
),
decorations: [
ParticleDecorationConfig(
count: 8,
colors: [Colors.pink.shade200, Colors.red.shade200],
minSize: 2,
maxSize: 5,
lifetime: 0.6,
),
],
),
);
setState(() {
_heartCount += items.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Heart Burst'),
backgroundColor: Colors.pink.shade100,
actions: [
Container(
key: _targetKey,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
const Icon(Icons.favorite, color: Colors.red),
const SizedBox(width: 4),
Text(
'$_heartCount',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
),
],
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.pink.shade50, Colors.white],
),
),
child: Center(
child: Column(
key: _sourceKey,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.favorite_border, size: 80, color: Colors.pink),
SizedBox(height: 16),
Text(
'Tap to send love!',
style: TextStyle(fontSize: 20, color: Colors.pink),
),
],
),
),
),
floatingActionButton: FloatingActionButton.large(
onPressed: _burstHearts,
backgroundColor: Colors.pink,
child: const Icon(Icons.favorite, size: 36),
),
);
}
}
/// ゲーム報酬アニメーション例
class GameRewardsExample extends StatefulWidget {
const GameRewardsExample({super.key});
@override
State<GameRewardsExample> createState() => _GameRewardsExampleState();
}
class _GameRewardsExampleState extends State<GameRewardsExample>
with TickerProviderStateMixin {
final _controller = FlyToTargetController();
final _starTargetKey = GlobalKey();
final _gemTargetKey = GlobalKey();
int _starCount = 0;
int _gemCount = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_controller.isAttached) {
_controller.attach(context, this);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _collectRewards() async {
final screenSize = MediaQuery.of(context).size;
final random = Random();
// スター
final stars = List.generate(6, (i) {
return FlyItemWithTarget(
item: FlyItem.fromOffset(
child: const Icon(Icons.star, color: Colors.amber, size: 32),
offset: Offset(
50 + random.nextDouble() * (screenSize.width - 100),
screenSize.height * 0.4 + random.nextDouble() * 150,
),
size: const Size(32, 32),
id: 'star_$i',
),
target: FlyTargetFromKey(_starTargetKey),
);
});
// ジェム
final gems = List.generate(4, (i) {
final gemColors = [Colors.cyan, Colors.purple, Colors.green, Colors.red];
return FlyItemWithTarget(
item: FlyItem.fromOffset(
child: Icon(
Icons.diamond,
color: gemColors[i],
size: 28,
shadows: [
Shadow(
color: gemColors[i].withValues(alpha: 0.5),
blurRadius: 8,
),
],
),
offset: Offset(
80 + random.nextDouble() * (screenSize.width - 160),
screenSize.height * 0.5 + random.nextDouble() * 100,
),
size: const Size(28, 28),
id: 'gem_$i',
),
target: FlyTargetFromKey(_gemTargetKey),
);
});
await _controller.flyToTargets(
itemsWithTargets: [...stars, ...gems],
config: FlyAnimationConfig(
duration: const Duration(milliseconds: 900),
curve: Curves.easeOutBack,
staggerDelay: const Duration(milliseconds: 50),
pathConfig: BezierPathConfig.auto(curvature: 0.5),
effects: const FlyEffects(
rotation: RotationEffect(rotations: 2 * pi),
scale: ScaleEffect(endScale: 0.5, startAt: 0.5),
fade: FadeEffect(startAt: 0.8),
),
decorations: [
const SparkleDecorationConfig(
count: 5,
size: 8,
color: Colors.white,
blinkSpeed: 4,
),
],
),
);
setState(() {
_starCount += stars.length;
_gemCount += gems.length;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Game Rewards'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.indigo.shade900, Colors.indigo.shade700],
),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_RewardCounter(
key: _starTargetKey,
icon: Icons.star,
color: Colors.amber,
count: _starCount,
),
_RewardCounter(
key: _gemTargetKey,
icon: Icons.diamond,
color: Colors.cyan,
count: _gemCount,
),
],
),
),
const Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.celebration,
size: 80,
color: Colors.white54,
),
SizedBox(height: 16),
Text(
'Collect your rewards!',
style: TextStyle(
fontSize: 24,
color: Colors.white70,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: _collectRewards,
backgroundColor: Colors.amber,
icon: const Icon(Icons.card_giftcard),
label: const Text('Collect!'),
),
);
}
}
class _RewardCounter extends StatelessWidget {
final IconData icon;
final Color color;
final int count;
const _RewardCounter({
super.key,
required this.icon,
required this.color,
required this.count,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
border: Border.all(color: color.withValues(alpha: 0.5)),
),
child: Row(
children: [
Icon(icon, color: color, size: 28),
const SizedBox(width: 8),
Text(
'$count',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
}