Flutter Animasyonları: Fizik, Örtülü ve Özel Ressamlar
Flutter'daki animasyonlar üç katmanlı bir sistem üzerine kurulmuştur: örtülü animasyonlar (AnimatedContainer, AnimatedOpacity) için sıfır kurulumla basit geçişler, müstehcen animasyonlar Tam zamanlama kontrolü için (AnimationController, Tween, AnimatedBuilder), eğri ve yön, e fiziksel simülasyonlar (Bahar Simülasyonu, SürtünmeSimülasyonu) yasalara uyduğu için gerçek görünen hareketler için fizik. Buna eklenen Özel Ressam çizmek özel grafikler doğrudan Canvas üzerinde.
Doğru seviyeyi seçmek, bakımı yapılabilir kod ile kod arasındaki farkı yaratır sebepsiz yere karmaşık. Bu kılavuz örneklerle dört seviyenin tamamını kapsar Doğrudan projelerinizde kullanabileceğiniz pratik.
Ne Öğreneceksiniz
- Örtük animasyonlar: Yeterli olduklarında ve sınırladıkları zaman
- AnimationController + Tween: TickerProvider ile ayrıntılı kontrol
- Animasyon eğrileri: Curves.easeInOut, sıçramaOut, elasticIn ve özel
- SpringSimulation: Doğal hareketler için fiziksel yaylar
- FrictionSimulation: kaydırma ve kaydırma için yavaşlama simülasyonu
- CustomPainter: Path, Paint, DrawArc ile Tuval üzerine çizim yapın
- Kahraman animasyonu: Ekranlar arasında paylaşılan geçişler
Örtülü Animasyonlar: Basit Yol
Le animasyonlu widget Flutter bunu otomatik olarak halleder
bir özellik değiştiğinde animasyon: denetleyici yok, kayan yazı yok,
sadece bir tane duration ve bir curve. Şunun için mükemmel:
Günlük kullanıcı arayüzü geçişlerinin %90'ı.
// Catalog delle animated widgets piu utili
// AnimatedContainer: anima width, height, color, border, padding
class ExpandableCard extends StatefulWidget {
@override
State<ExpandableCard> createState() => _ExpandableCardState();
}
class _ExpandableCardState extends State<ExpandableCard> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: _expanded ? 200 : 80,
decoration: BoxDecoration(
color: _expanded ? Colors.blue : Colors.grey.shade200,
borderRadius: BorderRadius.circular(_expanded ? 16 : 8),
boxShadow: _expanded
? [BoxShadow(blurRadius: 12, color: Colors.blue.withOpacity(0.3))]
: [],
),
padding: EdgeInsets.all(_expanded ? 24 : 12),
child: const Text('Tap to expand'),
),
);
}
}
// AnimatedOpacity: fade in/out
AnimatedOpacity(
opacity: _isVisible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: const SomeWidget(),
)
// AnimatedSwitcher: anima il cambio di widget (crossfade)
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: _showFirst
? const Text('First', key: ValueKey('first'))
: const Text('Second', key: ValueKey('second')),
)
// TweenAnimationBuilder: anima un valore personalizzato
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: _progress),
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
builder: (context, value, child) => LinearProgressIndicator(value: value),
)
AnimationController: Açık Kontrol
// AnimationController: timing e direzione manuale
class PulseButton extends StatefulWidget {
final VoidCallback onPressed;
const PulseButton({required this.onPressed, super.key});
@override
State<PulseButton> createState() => _PulseButtonState();
}
class _PulseButtonState extends State<PulseButton>
with SingleTickerProviderStateMixin {
// SingleTickerProviderStateMixin: fornisce il vsync al controller
late final AnimationController _controller;
late final Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, // collegato al VSync per 60fps
duration: const Duration(milliseconds: 150),
);
// Tween: mappa 0.0 - 1.0 del controller a 1.0 - 0.9 (shrink on press)
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.9).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
),
);
}
@override
void dispose() {
// CRITICO: disponi sempre il controller per evitare memory leak
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => _controller.forward(),
onTapUp: (_) async {
await _controller.reverse();
widget.onPressed();
},
onTapCancel: () => _controller.reverse(),
child: AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) => Transform.scale(
scale: _scaleAnimation.value,
child: child,
),
child: ElevatedButton(
onPressed: null, // Gestito da GestureDetector
child: const Text('Press Me'),
),
),
);
}
}
// Animazione in loop con repeat()
class LoadingDots extends StatefulWidget {
@override
State<LoadingDots> createState() => _LoadingDotsState();
}
class _LoadingDotsState extends State<LoadingDots>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat(); // Loop infinito
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
// Ogni punto ha un delay diverso
final delay = index * 0.2;
final value = (_controller.value - delay).clamp(0.0, 1.0);
final scale = Tween(begin: 0.5, end: 1.0)
.evaluate(CurvedAnimation(
parent: AlwaysStoppedAnimation(
(math.sin(value * math.pi * 2) + 1) / 2,
),
curve: Curves.easeInOut,
));
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Transform.scale(
scale: scale,
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
),
);
}),
);
},
);
}
}
Bahar Simülasyonu: Gerçek Fizik
// SpringSimulation: molla fisica per movimenti naturali
class SpringCard extends StatefulWidget {
@override
State<SpringCard> createState() => _SpringCardState();
}
class _SpringCardState extends State<SpringCard>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
double _dragOffset = 0;
late double _startOffset;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1), // massima durata della simulazione
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onPanStart(DragStartDetails details) {
_controller.stop();
_startOffset = _dragOffset;
}
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
_dragOffset += details.delta.dx;
});
}
void _onPanEnd(DragEndDetails details) {
// Simula una molla che riporta la card alla posizione originale
final spring = SpringSimulation(
SpringDescription(
mass: 1.0, // massa della card (kg)
stiffness: 200.0, // rigidita della molla (N/m) - piu alto = piu rigida
damping: 20.0, // smorzamento - piu alto = meno rimbalzi
),
_dragOffset, // posizione iniziale
0.0, // posizione target (0 = centro)
details.velocity.pixelsPerSecond.dx, // velocita iniziale
);
_controller.animateWith(spring).then((_) {
setState(() => _dragOffset = 0.0);
});
_controller.addListener(() {
setState(() {
_dragOffset = spring.x(_controller.value);
});
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: Transform.translate(
offset: Offset(_dragOffset, 0),
child: Card(
child: const Padding(
padding: EdgeInsets.all(24),
child: Text('Drag me - I spring back!'),
),
),
),
);
}
}
CustomPainter: Tuval Üzerine Çizim
// CustomPainter: grafico a ciambella animato
class DonutChart extends StatelessWidget {
final double progress; // 0.0 - 1.0
final Color color;
final String label;
const DonutChart({
required this.progress,
required this.color,
required this.label,
super.key,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
size: const Size(120, 120),
painter: _DonutPainter(progress: progress, color: color),
child: Center(
child: Text(
'${(progress * 100).round()}%',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
),
);
}
}
class _DonutPainter extends CustomPainter {
final double progress;
final Color color;
const _DonutPainter({required this.progress, required this.color});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 10;
const strokeWidth = 12.0;
// Paint per il background (grigio)
final bgPaint = Paint()
..color = Colors.grey.shade200
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
// Paint per il progresso
final progressPaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
// Disegna il cerchio background
canvas.drawCircle(center, radius, bgPaint);
// Disegna l'arco di progresso
final sweepAngle = 2 * math.pi * progress;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2, // start: ore 12 (top)
sweepAngle, // angolo spazzato
false, // non collegare al centro (false = arco solo)
progressPaint,
);
}
@override
bool shouldRepaint(_DonutPainter oldDelegate) {
// Ridisegna solo se i dati sono cambiati
return oldDelegate.progress != progress || oldDelegate.color != color;
}
}
// Animazione del grafico con TweenAnimationBuilder
class AnimatedDonutChart extends StatelessWidget {
final double targetProgress;
final Color color;
const AnimatedDonutChart({
required this.targetProgress,
required this.color,
super.key,
});
@override
Widget build(BuildContext context) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: targetProgress),
duration: const Duration(milliseconds: 1200),
curve: Curves.easeInOut,
builder: (context, value, _) => DonutChart(
progress: value,
color: color,
label: '${(value * 100).round()}%',
),
);
}
}
Hangi Animasyon Türünün Kullanılacağı
- Örtülü (Animasyonlu*): basit sahiplik geçişleri (vakaların %80'i)
- Açık (AnimationController): Birden çok mülk arasında senkronizasyonla, etkinliklerle tetiklenen döngüsel animasyonlar
- Fizik (BaharSimülasyonu): sürükle ve bırak, geri dönüşle kaydırma, fiziksel görünmesi gereken hareketler
- Özel Boyacı: grafikler, özel ilerleme göstergeleri, standart widget'larla elde edilemeyen görsel efektler
- Kahraman: Öne çıkan görsel öğeler için ekranlar arasında paylaşılan geçişler
Sonuçlar
Flutter animasyon sistemi en güçlü ve esnek olanlardan biridir. mobil çerçeve: 5 satır kod gerektiren örtülü widget'tan gerçeklikten ayırt edilemeyen hareketler üreten fiziksel simülasyon Canvas API'sinin tamamını Dart'ın kullanımına sunan CustomPainter'a. Önemli olan her kullanım durumu için doğru soyutlama düzeyini seçmektir. AnimatedContainer yeterli olduğunda aşırı mühendislik gerektirmeden.







