الرسوم المتحركة والتفاعلات الدقيقة المصقولة
الرسوم المتحركة والتفاعلات الدقيقة المصقولة
التطبيق الرائع أكثر من مجرد شاشات وظيفية — إنه يبدو حياً. انتقالات الصفحات السلسة، وحركات Hero التي تربط الشاشات بصرياً، والتفاعلات الدقيقة التي تستجيب لإيماءات المستخدم — هذه هي اللمسات الأخيرة التي تفصل المنتج المصقول عن النموذج الأولي الخام. في هذا الدرس ستتعلم توصيل انتقالات صفحات GoRouter، وتنفيذ حركات Hero، واستخدام AnimatedSwitcher لتغييرات حالة القائمة، وتطبيق ودجات الحركة الضمنية لصنع تفاعلات دقيقة سلسة في جميع أنحاء تطبيق المشروع النهائي.
حركات انتقال الصفحات مع GoRouter
يكشف GoRouter عن استدعاء pageBuilder على كل GoRoute يتيح لك إرجاع Page مخصصة بدلاً من MaterialPage الافتراضية. بإرجاع CustomTransitionPage تتحكم في كل بكسل من حركة الدخول والخروج.
// router/app_router.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../screens/home_screen.dart';
import '../screens/detail_screen.dart';
final GoRouter appRouter = GoRouter(
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const HomeScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// تلاشٍ + انزلاق طفيف للأعلى
final tween = Tween(begin: const Offset(0, 0.05), end: Offset.zero)
.chain(CurveTween(curve: Curves.easeOutCubic));
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(tween),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 300),
),
),
GoRoute(
path: '/detail/:id',
pageBuilder: (context, state) {
final id = state.pathParameters['id']!;
return CustomTransitionPage(
key: state.pageKey,
child: DetailScreen(id: id),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// انزلاق أفقي على المحور المشترك
final enterTween = Tween(
begin: const Offset(1.0, 0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeInOutCubic));
final exitTween = Tween(
begin: Offset.zero,
end: const Offset(-0.3, 0),
).chain(CurveTween(curve: Curves.easeInOutCubic));
return Stack(
children: [
SlideTransition(
position: secondaryAnimation.drive(exitTween),
child: Container(),
),
SlideTransition(
position: animation.drive(enterTween),
child: child,
),
],
);
},
transitionDuration: const Duration(milliseconds: 350),
);
},
),
],
);
buildFadeSlide() تُرجع CustomTransitionPage حتى تشترك كل المسارات في نفس أسلوب الانتقال وتعدّل فقط المنحنى أو الاتجاه لكل مسار.حركات Hero
ودجت Hero يُحرّك عنصراً مرئياً مشتركاً بين مسارين — يُحرّك الإطار تلقائياً موضع الودجت وحجمه خلال التنقل. يجب أن تشترك ودجات المصدر والوجهة في نفس tag.
// في عنصر القائمة (المصدر)
Hero(
tag: 'product-image-${product.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
product.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
),
),
)
// في شاشة التفاصيل (الوجهة) — نفس الـ tag، حجم أكبر
Hero(
tag: 'product-image-${product.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
product.imageUrl,
width: double.infinity,
height: 260,
fit: BoxFit.cover,
),
),
)
tag فريداً عبر شجرة الودجات بالكامل في أي لحظة. استخدام المعرّف الفريد للكيان (مثل 'product-image-\${product.id}') يمنع تعارض العلامات عند وجود عناصر متعددة على الشاشة في آنٍ واحد.AnimatedSwitcher لتغييرات حالة القائمة
يُشغّل AnimatedSwitcher تلقائياً انتقالاً عند استبدال ودجت child بودجت آخر ذي key مختلفة. هذا مثالي للتبديل بين رسم توضيحي للحالة الفارغة وقائمة مملوءة، أو بين مؤشر التحميل والمحتوى المحمّل.
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionsBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(scale: animation, child: child),
);
},
child: items.isEmpty
? const EmptyStateWidget(key: ValueKey('empty'))
: ProductListView(key: ValueKey('list'), items: items),
)
AnimatedSwitcher تغيير الـ child فقط عندما يملك الـ child الجديد key مختلفة عن السابقة. إذا استبدلت الـ children دون تغيير المفتاح، لن تُشغَّل أي حركة. دائماً عيّن ValueKey (أو UniqueKey) لكل child مميز.ودجات الحركة الضمنية للتفاعلات الدقيقة
يشحن Flutter بعائلة من ودجات Animated* الضمنية التي تُحرّك تلقائياً عند تغيير الخاصية — لا حاجة لـ AnimationController. تراقب حجج المُنشئ وتُحرّك أي تغيير على مدى duration المحدد.
AnimatedContainer— اللون، الحجم، نصف قطر الحافة، الحشو، الزخرفةAnimatedOpacity— التلاشي للداخل/الخارج بناءً على علامة منطقيةAnimatedScale/AnimatedRotation— تكبير/تدوير الـ childAnimatedDefaultTextStyle— تغيير سلس لحجم الخط أو الوزن أو اللونAnimatedPositioned— انزلاق الـ children داخلStackTweenAnimationBuilder— تحريك أي قيمة غير مشمولة بما سبق
// تفاعل دقيق: زر المفضلة مع تكبير + وميض اللون
class FavouriteButton extends StatefulWidget {
final bool isFavourite;
final VoidCallback onToggle;
const FavouriteButton({
super.key,
required this.isFavourite,
required this.onToggle,
});
@override
State<FavouriteButton> createState() => _FavouriteButtonState();
}
class _FavouriteButtonState extends State<FavouriteButton> {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onToggle,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.elasticOut,
padding: EdgeInsets.all(widget.isFavourite ? 12 : 8),
decoration: BoxDecoration(
color: widget.isFavourite
? Colors.red.shade50
: Colors.grey.shade100,
shape: BoxShape.circle,
),
child: AnimatedScale(
scale: widget.isFavourite ? 1.25 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutBack,
child: Icon(
widget.isFavourite ? Icons.favorite : Icons.favorite_border,
color: widget.isFavourite ? Colors.red : Colors.grey,
size: 24,
),
),
),
);
}
}
الجمع بين التقنيات: عنصر قائمة مصقول
الصقل الحقيقي يأتي من تطبيق عدة تقنيات معاً. يمكن لبطاقة القائمة استخدام Hero على الصورة المصغرة، وAnimatedContainer لتمييز الضغط، وAnimatedOpacity للتلاشي عند التثبيت الأول.
TweenAnimationBuilder<double> مع begin: 0, end: 1 يُطلق في initState لتتدرج حركات ظهور عناصر القائمة. يؤخر كل عنصر بدء حركته بمقدار index * 50ms لتأثير ظهور متتالٍ.إرشادات الأداء
- فضّل الحركات الضمنية (ودجات Animated*) على
AnimationControllerالصريح للتحريك البسيط للخصائص — فهي أقل كوداً وآمنة من التسريب. - اجعل مدد الحركة بين 150ms و400ms لردود فعل واجهة المستخدم؛ المدد الأطول تبدو بطيئة.
- تجنب التحريك داخل
ListView.builderعلى المحور الرئيسي — يسبب تمريرات تخطيط مفرطة. حرّك الخصائص الزخرفية فقط (الشفافية، الحجم، اللون). - استخدم
RepaintBoundaryحول الشجرات الفرعية المتحركة الثقيلة لعزلها في طبقتها الخاصة، مما يقلل مساحة إعادة الرسم.
ملخص
أصبح لديك الآن مجموعة أدوات حركة كاملة لتطبيق المشروع النهائي. CustomTransitionPage في GoRouter يتحكم في الحركة على مستوى الصفحة. Hero يخلق استمرارية بصرية سلسة بين الشاشات. AnimatedSwitcher يتعامل بأناقة مع تبادل الـ child المدفوع بالحالة. وعائلة Animated* الضمنية تُتيح تفاعلات دقيقة سلسة — كل ذلك دون كتابة AnimationController واحد. طبّق هذه الأنماط بحكمة: الحركة يجب أن توضّح واجهة المستخدم، لا أن تشتت الانتباه عنها.