الرسوم المتحركة الصريحة: AnimationController و Tween
الرسوم المتحركة الصريحة: AnimationController و Tween
ينقسم نظام الرسوم المتحركة في Flutter إلى عائلتين: الرسوم المتحركة الضمنية (مثل AnimatedContainer) التي تتولى إدارة المتحكم عنك، والرسوم المتحركة الصريحة حيث تمتلك أنت المتحكم وتقود كل إطار يدوياً. تمنحك الرسوم المتحركة الصريحة تحكماً دقيقاً في اتجاه التشغيل والسرعة والتكرار والتسلسل — وهو أمر ضروري لأي شيء يتجاوز انتقالات الخصائص البسيطة.
لماذا نستخدم الرسوم المتحركة الصريحة؟
الودجات الضمنية مريحة لكنها محدودة. تحتاج إلى رسوم متحركة صريحة عندما تريد:
- تشغيل رسم متحرك للأمام ثم عكسه عند أحداث مستخدم مختلفة
- تكرار رسم متحرك إلى ما لا نهاية أو عدداً محدداً من المرات
- مزامنة رسوم متحركة متعددة على خط زمني واحد
- التفاعل مع أحداث حالة الرسوم المتحركة (بدأ، اكتمل، عكس، رُفض)
- قيادة محاكاة فيزيائية أو منحنى قائم على السرعة
الـ TickerProviderStateMixin
يتطلب AnimationController وجود Ticker — وهو آلية تستدعي callback في كل تحديث للشاشة (عادةً 60 أو 120 مرة في الثانية). يجعل mixin الخاص بـ TickerProviderStateMixin فئة State الخاصة بك TickerProvider صالحاً. إذا كنت تنشئ متحكماً واحداً فقط، يمكنك استخدام SingleTickerProviderStateMixin بدلاً من ذلك لأنه أكثر كفاءة قليلاً.
SingleTickerProviderStateMixin عندما يكون لديك AnimationController واحد. استخدم TickerProviderStateMixin عندما تدير متحكمَين أو أكثر في نفس State.إنشاء AnimationController
AnimationController هو الساعة الرئيسية لرسم متحرك. تنشئه في initState، وتوفر له duration، وتمرر vsync: this لربطه بـ ticker الشاشة.
إعداد AnimationController البسيط
import 'package:flutter/material.dart';
class FadeBox extends StatefulWidget {
const FadeBox({super.key});
@override
State<FadeBox> createState() => _FadeBoxState();
}
class _FadeBoxState extends State<FadeBox>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this, // يربط المتحكم بـ Ticker الخاص بهذه الحالة
);
}
@override
void dispose() {
_controller.dispose(); // تخلص دائماً لمنع تسرب الذاكرة
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 100, height: 100, color: Colors.blue),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => _controller.forward(),
child: const Text('تشغيل'),
),
],
),
),
);
}
}
_controller.dispose() في dispose() يتسبب في تسرب ذاكرة — يستمر Ticker في العمل حتى بعد إزالة الودجت من الشجرة. سيؤدي ذلك في نهاية المطاف إلى تعطل تطبيقك في وضع debug برسالة "ticker was still active".إرفاق Tween
يُنتج AnimationController وحده قيماً بين 0.0 و1.0 فقط. يقوم Tween بتعيين هذا النطاق على أي فاصل مكتوب تحتاجه — double أو Color أو Offset أو Size وما إلى ذلك. تربطه باستخدام .animate() لإنتاج Animation<T> مكتوب:
Tween ينتج Animation مكتوباً
// ينتج Animation<double> يتراوح من 0.0 إلى 300.0
final Animation<double> _widthAnimation = Tween<double>(
begin: 0.0,
end: 300.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut, // تطبيق منحنى تخفيف
),
);
// ينتج Animation<Color?> ينتقل بين لونين
final Animation<Color?> _colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.orange,
).animate(_controller);
// استخدام القيمة المتحركة في build():
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _widthAnimation.value,
height: 80,
color: _colorAnimation.value,
child: child, // child لا يُعاد بناؤه في كل إطار
);
},
child: const Center(child: Text('حرّكني')),
);
}
قيادة الأمام والعكس والتكرار
بمجرد أن يكون لديك متحكم، تقوده بثلاث طرق رئيسية:
_controller.forward()— يشغل من القيمة الحالية نحو 1.0_controller.reverse()— يشغل من القيمة الحالية عائداً نحو 0.0_controller.repeat(reverse: true)— يكرر الرسم المتحرك مع تبديل الاتجاه في كل دورة_controller.reset()— يُعيد القيمة فوراً إلى 0.0 دون تحريك_controller.stop()— يجمد التشغيل عند القيمة الحالية
_controller.addStatusListener((status) { ... }). الحالات الأربع هي AnimationStatus.forward وdismissed وreverse وcompleted. هذه هي الطريقة الصحيحة لتسلسل الرسوم المتحركة أو تشغيل إجراءات عند انتهاء رسم متحرك.إعادة بناء واجهة المستخدم مع AnimatedBuilder
تغليف الشجرة الفرعية المتحركة في AnimatedBuilder هو النمط الموصى به. يستدعي builder في كل إطار، لكنه يقبل معامل child للأجزاء من الشجرة الفرعية التي لا تتغير — يتم بناؤها مرة واحدة وإعادة استخدامها، مما يحافظ على تكلفة منخفضة لكل إطار.
ملخص
تتطلب الرسوم المتحركة الصريحة في Flutter ثلاث خطوات: (1) خلط SingleTickerProviderStateMixin (أو TickerProviderStateMixin) في State الخاص بك؛ (2) إنشاء AnimationController في initState والتخلص منه في dispose؛ (3) إرفاق Tween عبر .animate() لتعيين النطاق 0–1 على القيمة المكتوبة المرغوبة. قد الرسم المتحرك بـ forward() أو reverse() أو repeat() استجابةً لأحداث المستخدم، وأعد بناء واجهة المستخدم مع AnimatedBuilder لأداء مثالي.