تسلسل الرسوم المتحركة: الفترات الزمنية والتسلسل
تسلسل الرسوم المتحركة: الفترات الزمنية والتسلسل
عند بناء واجهات Flutter متقنة، كثيرًا ما تحتاج إلى تحريك عدة خصائص بصرية — الشفافية، والموضع، والحجم، واللون — بتسلسل محكم أو بتداخل مدروس. قيادة كل منها بـ AnimationController مستقل أمر مُكلف وصعب المزامنة. الحل الاحترافي هو استخدام متحكم واحد وتقسيم الجدول الزمني 0.0 → 1.0 إلى نطاقات فرعية مسمّاة باستخدام CurvedAnimations مقيّدة بـ Interval.
Interval هو صنف فرعي من Curve. عند تغليفه داخل CurvedAnimation، فإنه يُعيّن شريحةً من الجدول الزمني للمتحكم إلى المدخل 0.0–1.0 لأي Tween. خارج تلك الشريحة تُثبَّت القيمة عند 0.0 أو 1.0، فتبقى الخاصية ساكنة بينما تعمل رسوم متحركة أخرى.كيف يعمل Interval
Interval(begin, end, curve: ...) يقبل موضعين معيارَيْن على الجدول الزمني للمتحكم الأب (كلاهما بين 0.0 و1.0). داخل هذه النافذة يُطبَّق المنحنى الداخلي (الافتراضي: Curves.linear). خارجها تُثبَّت القيمة:
- قبل
begin← الخرج 0.0 - بعد
end← الخرج 1.0 - بين
beginوend← يُعيّن المنحنى الداخلي التقدم المحلي
مثال 1 — تلاشٍ + انزلاق متدرج من متحكم واحد
class StaggeredCard extends StatefulWidget {
const StaggeredCard({super.key});
@override
State<StaggeredCard> createState() => _StaggeredCardState();
}
class _StaggeredCardState extends State<StaggeredCard>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
// الرسم المتحرك الفرعي 1: تلاشٍ خلال أول 40 % من الجدول الزمني
late final Animation<double> _opacity;
// الرسم المتحرك الفرعي 2: انزلاق للأعلى خلال 40–90 % من الجدول الزمني
late final Animation<Offset> _slide;
// الرسم المتحرك الفرعي 3: تكبير خلال 60–100 % من الجدول الزمني
late final Animation<double> _scale;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_opacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.4, curve: Curves.easeIn),
),
);
_slide = Tween<Offset>(
begin: const Offset(0.0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.4, 0.9, curve: Curves.easeOut),
),
);
_scale = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.6, 1.0, curve: Curves.elasticOut),
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return FadeTransition(
opacity: _opacity,
child: SlideTransition(
position: _slide,
child: ScaleTransition(
scale: _scale,
child: child,
),
),
);
},
child: const Card(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text('مرحبًا بعالم الرسوم المتدرجة!'),
),
),
);
}
}
child إلى AnimatedBuilder. هذه الشجرة الفرعية تُبنى مرةً واحدة وتُعاد استخدامها بين الإطارات، مما يتجنب إعادة بناء المحتوى الثابت. فقط أغلفة الانتقال تُعاد رسمها في كل إطار.الفترات المتتالية مقابل المتداخلة
يمكن ترتيب الفترات بثلاث طرق لتحقيق تأثيرات توجيه مختلفة:
- متتالية (بلا تداخل):
Interval(0.0, 0.5)ثمInterval(0.5, 1.0)— تنتهي إحداهن قبل أن تبدأ الأخرى. - متداخلة:
Interval(0.0, 0.6)وInterval(0.4, 1.0)— تتشاركان نافذة تداخل 20 % لتأثير انتقال أكثر سلاسة. - بداية متأخرة:
Interval(0.3, 1.0)— يظل الرسم المتحرك خاملًا طوال أول 30 % من الجدول الزمني، ثم ينطلق بأقصى سرعة.
حزمة SequenceAnimation
للأنماط المتدرجة المعقدة، توفر حزمة المجتمع sequence_animation واجهة برمجية بنّاءة سلسة مبنية على نفس مبدأ Interval. تدير النطاقات الزمنية نيابةً عنك وتُعيد AnimationMap مفاتيحها تسميات نصية. وإن لم تكن جزءًا من SDK الرسمي، فإن فهمها يُعزز إدراك سبب قوة آلية Interval الأساسية.
مثال 2 — تسلسل ألوان ثلاثي المراحل (SDK فقط، بلا حزمة)
class ColorSequence extends StatefulWidget {
const ColorSequence({super.key});
@override
State<ColorSequence> createState() => _ColorSequenceState();
}
class _ColorSequenceState extends State<ColorSequence>
with SingleTickerProviderStateMixin {
late final AnimationController _ctrl;
late final Animation<Color?> _phase1; // أزرق → أخضر (0–33 %)
late final Animation<Color?> _phase2; // أخضر → برتقالي (33–66 %)
late final Animation<Color?> _phase3; // برتقالي → أحمر (66–100 %)
Color get _color =>
_phase3.value ?? _phase2.value ?? _phase1.value ?? Colors.blue;
@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat(reverse: true);
_phase1 = ColorTween(begin: Colors.blue, end: Colors.green).animate(
CurvedAnimation(
parent: _ctrl,
curve: const Interval(0.0, 0.33, curve: Curves.linear),
),
);
_phase2 = ColorTween(begin: Colors.green, end: Colors.orange).animate(
CurvedAnimation(
parent: _ctrl,
curve: const Interval(0.33, 0.66, curve: Curves.linear),
),
);
_phase3 = ColorTween(begin: Colors.orange, end: Colors.red).animate(
CurvedAnimation(
parent: _ctrl,
curve: const Interval(0.66, 1.0, curve: Curves.linear),
),
);
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _ctrl,
builder: (_, __) => Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: _color,
shape: BoxShape.circle,
),
),
);
}
}
ColorTween يُعيد Color? قابلًا للقيمة الخالية. احرص دائمًا على توفير قيمة احتياطية (كما هو موضح في سلسلة null-coalescing أعلاه) أو استخدم ! فقط حين تكون واثقًا أن الرسم المتحرك قد بدأ. إغفال هذا مصدر شائع لأعطال null-dereference عند الإطار الأول.اعتبارات الأداء
الرسوم المتحركة المتسلسلة بالفترات فعّالة لأنها تتشارك ticker واحدة. ضع هذه الممارسات الفضلى في الاعتبار:
- استخدم
AnimatedBuilderمعchildثابت لتجنب إعادة بناء الأشجار الفرعية غير المتغيرة. - فضّل
FadeTransitionوSlideTransitionوScaleTransitionعلى أغلفةOpacityوTransform— فودجات الانتقال مدركة لحدود إعادة الرسم. - قِسْ باستخدام لوحة Performance في Flutter DevTools لرصد الإطارات المتقطعة إذا كانت الفترات تُطلق عملًا بنائيًا ثقيلًا في كل إطار.
الخلاصة
تنظيم رسوم متحركة متعددة من AnimationController واحد هو النمط القياسي في Flutter للحركة المتدرجة المنسّقة. الخطوات الأساسية هي: (1) إنشاء متحكم واحد بمدة إجمالية، (2) تغليفه في CurvedAnimations متعددة مقيّدة كل منها بـ Interval، (3) تغذية كل CurvedAnimation إلى Tween الخاص به، و(4) العرض باستخدام AnimatedBuilder مع ودجات الانتقال. يمكن أن تكون الفترات متتالية أو متداخلة أو متأخرة البداية لتحقيق أي توجيه مطلوب دون الحاجة أبدًا إلى متحكم ثانٍ.