افهم الآلية الداخلية للرسوم المتحركة الضمنية، وتحكّم بدقة في AnimationController الخاص بك.
افتح هذا الدرس في Kodokonينتمي AnimatedContainer إلى عائلة الرسوم المتحركة الضمنية: فأنت تغيّر قيمة هدف ويتولّى إطار العمل تحريك الانتقال. من الداخل، يحتفظ كل ImplicitlyAnimatedWidget بـ AnimationController مخفيّ في حالته. وعندما يصل تكوين جديد، يمرّ didUpdateWidget عبر الخصائص من خلال forEachTween ويعيد توجيه كل تدرّج (tween): فيصبح الحدّ begin هو القيمة المُستوفَاة (interpolated) الحالية، والحدّ end هو الهدف الجديد، ثم يعيد المتحكّم الانطلاق من الصفر. ونتيجة قيّمة لذلك: تغيير الهدف في منتصف الحركة لا يُسبّب أيّ قفزة بصرية - فتستأنف الحركة من حيث كانت تمامًا.
import 'package:flutter/material.dart';
class SelectableCard extends StatefulWidget {
const SelectableCard({super.key});
@override
State<SelectableCard> createState() =>
_SelectableCardState();
}
class _SelectableCardState extends State<SelectableCard> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _selected ? 220 : 160,
height: 96,
decoration: BoxDecoration(
color: _selected
? Colors.indigo
: Colors.indigo.shade200,
borderRadius: BorderRadius.circular(
_selected ? 28 : 12,
),
),
),
);
}
}تغطّي الرسوم الضمنية الخصائص التي يتوقّعها إطار العمل، ويُعمِّم TweenAnimationBuilder العملية لتشمل أيّ قيمة قابلة للاستيفاء (interpolation). وحالما تحتاج إلى قيادة الحركة - إيقاف مؤقت، أو عكس، أو تكرار، أو تفاعل مع الحالة - انتقل إلى AnimationController الصريح. يتوقّع الوسيط vsync قيمة من نوع TickerProvider: فيُنشئ الـ mixin كائن Ticker، وهو ردّ نداء يستدعيه SchedulerBinding في كل إطار. وثمة دقّة قليلة الشيوع: TickerMode يُخرِس مؤقّتات (tickers) شجرة فرعية مخفية (تحت مسار مغطّى مثلًا). فيستمر الزمن في التدفق، لكن لا يعمل أيّ ردّ نداء بعد ذلك - أيْ صفر إهدار لوحدة المعالجة المركزية على رسوم متحركة غير مرئية.
import 'package:flutter/material.dart';
class RevealPanel extends StatefulWidget {
const RevealPanel({super.key, required this.child});
final Widget child;
@override
State<RevealPanel> createState() => _RevealPanelState();
}
class _RevealPanelState extends State<RevealPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 200),
);
late final Animation<double> _opacity = _controller
.drive(CurveTween(curve: Curves.easeOutCubic));
void toggle() {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
_controller.reverse();
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggle,
child: FadeTransition(
opacity: _opacity,
child: widget.child,
),
);
}
}المتحكّم ما هو إلا Animation<double> يسير من 0 إلى 1: ركّبه بدلًا من استهلاكه خامًا. فـ controller.drive(CurveTween(...)) يُنتِج حركة مشتقّة دون أيّ حالة إضافية. ولتطبيق القيمة على الشجرة، فضِّل ودجات Transition (FadeTransition، RotationTransition): فهي تُنصِت إلى الحركة مباشرةً على مستوى كائن العرض (render object)، دون إعادة بناء أيّ ودجت في كل إطار. وعندما يكون التحويل اعتباطيًا، يؤدّي AnimatedBuilder المهمّة - شريطة أن تمرّر الشجرة الفرعية الثابتة عبر child: إذ تُبنى مرة واحدة فقط ثم يُعاد حقنها في builder في كل إطار. وهذا هو التحسين الأعلى مردودًا للرسوم المتحركة الصريحة.
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Orbit extends StatefulWidget {
const Orbit({super.key});
@override
State<Orbit> createState() => _OrbitState();
}
class _OrbitState extends State<Orbit>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: const Icon(Icons.satellite_alt, size: 32),
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Transform.translate(
offset: Offset(
math.cos(angle) * 80,
math.sin(angle) * 80,
),
child: child,
);
},
);
}
}