انتقالات الصفحات والرسوم المتحركة المخصصة للمسارات
انتقالات الصفحات والرسوم المتحركة المخصصة للمسارات
في كل مرة تنتقل فيها بين الشاشات في Flutter، تُشغَّل رسوم متحركة للمسار. بشكل افتراضي، يطبّق Flutter انتقالاً خاصاً بكل منصة: انزلاق من اليمين على Android وانزلاق من الأسفل على iOS. وبينما تبدو هذه الإعدادات الافتراضية أنيقة، كثيراً ما تحتاج التطبيقات الاحترافية إلى انتقالات مخصصة تعكس هوية علامتها التجارية — تلاشٍ متقاطع، أو تحجيم منبثق، أو انزلاق بأسلوب الأبطال، أو حتى تسلسل معقد للعناصر المشتركة.
مفتاح كل ذلك هو PageRouteBuilder — فئة مسار منخفضة المستوى تمنحك تحكماً كاملاً في الرسوم المتحركة عند دفع المسار أو إزالته.
MaterialPageRoute وCupertinoPageRoute) هي كلتيهما فئات فرعية من PageRoute. PageRouteBuilder هو أيضاً فئة فرعية من PageRoute — فهو يكشف فقط عن خطافات الرسوم المتحركة كاستدعاءات رد في المُنشئ بدلاً من إلزامك بإنشاء فئات فرعية بنفسك.كيف يعمل PageRouteBuilder
يقبل PageRouteBuilder استدعاءَي ردٍّ حاسمَين:
pageBuilder— يُرجع الودجت الوجهة (محتوى صفحتك). يستقبلBuildContext، والـAnimation<double>الأساسية (قيمها 0 → 1 عند دخول الصفحة)، والـsecondaryAnimation(قيمها 0 → 1 عند دفع صفحة جديدة فوق هذه).transitionsBuilder— يلفّ ودجت الصفحة في ودجات الرسوم المتحركة. يستقبل نفس الوسائط إضافةً إلى ودجتchildالذي أرجعهpageBuilder. هنا تُطبّقFadeTransitionأوSlideTransitionأوScaleTransitionأو أي تركيبة منها.
child المُقدَّم داخل transitionsBuilder بدلاً من استدعاء pageBuilder مجدداً. يُخزّن Flutter ودجت الصفحة مؤقتاً لتحسين الأداء — إعادة بنائه داخل الانتقال يُبطل هذا التحسين.المثال الأول — انتقال التلاشي
التلاشي المتقاطع البسيط هو أسهل انتقال مخصص للتنفيذ. تتلاشى الصفحة من الشفافية إلى العتامة عند الدخول، وتتلاشى للخلف عند الإزالة.
مسار صفحة بالتلاشي
Route<void> _fadeRoute(Widget page) {
return PageRouteBuilder<void>(
// مدة الانتقال
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 300),
pageBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return page;
},
transitionsBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
// CurvedAnimation يُشكّل التقدم الخام من 0 إلى 1
final curved = CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
);
return FadeTransition(opacity: curved, child: child);
},
);
}
// الاستخدام
Navigator.of(context).push(_fadeRoute(const DetailsPage()));
المثال الثاني — انزلاق مع تلاشٍ مدمج
الجمع بين انزلاق أفقي وتلاشٍ متزامن هو نمط شائع في التطبيقات الحديثة. نركّب ودجتَي انتقال — SlideTransition يلفّ FadeTransition:
مسار انزلاق مع تلاشٍ
Route<void> _slideFadeRoute(Widget page) {
return PageRouteBuilder<void>(
transitionDuration: const Duration(milliseconds: 350),
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// انزلاق من اليمين: إزاحة x من 1.0 إلى 0.0
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
final slideTween = Tween(begin: begin, end: end)
.chain(CurveTween(curve: Curves.easeOutCubic));
// تلاشٍ متزامن
final fadeTween = Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeIn));
return SlideTransition(
position: animation.drive(slideTween),
child: FadeTransition(
opacity: animation.drive(fadeTween),
child: child,
),
);
},
);
}
// الدفع عند الضغط على زر
onPressed: () => Navigator.of(context).push(
_slideFadeRoute(const ProfilePage()),
),
انتقال التحجيم (الانبثاق)
يجعل انتقال التحجيم الصفحة تبدو وكأنها تنمو من مركز الشاشة — تأثير "انبثاق" مُرضٍ يُستخدم غالباً للمسارات ذات النمط النافذي:
مسار الانبثاق بالتحجيم
Route<void> _scaleRoute(Widget page) {
return PageRouteBuilder<void>(
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, __) => page,
transitionsBuilder: (context, animation, __, child) {
final scaleTween = Tween<double>(begin: 0.85, end: 1.0)
.chain(CurveTween(curve: Curves.easeOutBack));
final fadeTween = Tween<double>(begin: 0.0, end: 1.0);
return ScaleTransition(
scale: animation.drive(scaleTween),
child: FadeTransition(
opacity: animation.drive(fadeTween),
child: child,
),
);
},
);
}
فئة مسار مخصصة قابلة لإعادة الاستخدام
للانتقالات المستخدمة في أرجاء تطبيقك، مدّد PageRouteBuilder إلى فئة مسمّاة حتى لا تكرر النمطي في كل مكان:
SlideUpRoute قابل لإعادة الاستخدام
class SlideUpRoute<T> extends PageRouteBuilder<T> {
final Widget page;
SlideUpRoute({required this.page})
: super(
transitionDuration: const Duration(milliseconds: 400),
reverseTransitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) => page,
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
final tween = Tween(
begin: const Offset(0.0, 1.0), // من الأسفل إلى الأعلى
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOutQuart));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}
// الاستخدام في أي مكان بالتطبيق:
Navigator.of(context).push(SlideUpRoute(page: const CheckoutPage()));
استخدام secondaryAnimation
تتيح الرسوم المتحركة الثانوية للصفحة الحالية أن تتفاعل حين يُدفع شيء جديد فوقها. مثلاً، يُزيح iOS صفحة الخلفية قليلاً إلى اليسار بينما تنزلق الصفحة الجديدة من اليمين. يمكنك تكرار هذا التأثير "المنظوري":
رسوم متحركة ثانوية بتأثير المنظور
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// الأساسية: الصفحة الجديدة تنزلق من اليمين
final enterTween = Tween(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).chain(CurveTween(curve: Curves.easeOut));
// الثانوية: هذه الصفحة تنزاح قليلاً إلى اليسار عند تغطيتها
final exitTween = Tween(
begin: Offset.zero,
end: const Offset(-0.25, 0.0),
).chain(CurveTween(curve: Curves.easeIn));
return SlideTransition(
position: animation.drive(enterTween),
child: SlideTransition(
position: secondaryAnimation.drive(exitTween),
child: child,
),
);
},
transitionDuration بين 200 و500 ميلي ثانية في معظم الحالات. تجنّب العمليات الحسابية الثقيلة أو إعادة بناء الودجات الضخمة داخل transitionsBuilder — فهو يعمل في كل إطار رسوم متحركة (عادةً 60 إلى 120 مرة في الثانية).انتقالات شاملة عبر onGenerateRoute
لتطبيق انتقال مخصص واحد على كل مسار في تطبيقك دون تعديل كل استدعاء Navigator.push()، تجاوز onGenerateRoute في MaterialApp:
انتقال مخصص على مستوى التطبيق
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
final Widget page = _resolvePageForRoute(settings);
return PageRouteBuilder<void>(
settings: settings, // مرّر الإعدادات لحفظ اسم المسار
pageBuilder: (_, __, ___) => page,
transitionsBuilder: (_, animation, __, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
),
child: child,
);
},
);
},
);
ملخص
PageRouteBuilder هو العمود الفقري لانتقالات الصفحات المخصصة في Flutter. يمنحك استدعاء الرد transitionsBuilder Animation<double> خاماً تُشكّله بـ Tween وCurveTween وCurvedAnimation، ثم تطبّقه عبر FadeTransition أو SlideTransition أو ScaleTransition أو أي تركيبة. تتيح secondaryAnimation للصفحة الخارجة أن تتحرك أثناء دفع مسار جديد فوقها. لاتساق على مستوى التطبيق، أنشئ فئة فرعية مسمّاة أو اربط منشئاً مشتركاً عبر onGenerateRoute.
Tween (يُعرّف قيم البداية والنهاية)، ومنحنى Curve (يُشكّل التوقيت)، وودجت الانتقال (FadeTransition أو SlideTransition أو ScaleTransition). الجمع بين هذه الثلاثة يمنحك تقريباً أي تأثير انتقال صفحة يمكنك تخيّله.