الرسوم المتحركة وتصميم الحركة

انتقالات Hero: عناصر مشتركة بين الشاشات

15 دقيقة الدرس 9 من 13

انتقالات Hero: عناصر مشتركة بين الشاشات

انتقال Hero هو رسم متحرك العنصر المشترك المدمج في Flutter: يتحرك الودجت بصرياً من موضعه في مسار (Route) إلى موضعه في المسار الوجهة أثناء الدفع (push) أو الرجوع (pop) في التنقل. يجعل هذا التأثير العلاقة بين الشاشات مفهومة على الفور — صورة مصغرة تتمدد إلى عرض تفصيلي، أو بطاقة منتج تتحول إلى صفحة منتج كاملة، أو صورة رمزية تنزلق من قائمة إلى رأس صفحة ملف شخصي.

يدير Flutter الرحلة بالكامل تلقائياً. كل ما تفعله هو تغليف نفس الودجت المنطقي في كلا المسارين داخل ودجت Hero وإعطاء كلا النسختين نفس الـ tag. يكتشف الإطار العلامات المتطابقة عند وقت التنقل ويُنسّق الرسوم المتحركة بينها.

ملاحظة: يجب أن يكون الـ tag فريداً على أي مسار معين. إذا شارك ودجتا Hero على نفس الشاشة نفس العلامة، فسيُطلق Flutter خطأ تأكيد في وضع التصحيح. بالنسبة لتدفقات القائمة-إلى-التفصيل، استخدم المعرف الفريد للعنصر كعلامة (مثل 'hero-product-\${product.id}').

مثال Hero بسيط

تغليف ودجت بـ Hero يستغرق ثلاثة أسطر: الودجت نفسه، وtag، وعلامات متطابقة على كلا المسارين. الـ Hero في المسار المصدر يُسمى hero المصدر؛ والذي في الوجهة هو hero الوجهة.

مثال 1 — Hero أساسي: من القائمة إلى التفصيل

// ─── المسار المصدر: شبكة المنتجات ────────────────────────────────
GestureDetector(
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (_) => ProductDetailPage(product: product),
      ),
    );
  },
  child: Hero(
    tag: 'product-image-\${product.id}',  // فريد لكل عنصر
    child: Image.network(
      product.imageUrl,
      width: 120,
      height: 120,
      fit: BoxFit.cover,
    ),
  ),
)

// ─── المسار الوجهة: تفصيل المنتج ─────────────────────────────────
Hero(
  tag: 'product-image-\${product.id}',   // نفس العلامة
  child: Image.network(
    product.imageUrl,
    width: double.infinity,
    height: 300,
    fit: BoxFit.cover,
  ),
)

أثناء الدفع، يحسب Flutter حجم/موضع البداية (120×120، خلية الشبكة) وحجم/موضع النهاية (عرض كامل، 300 بكسل ارتفاعاً). ثم يرفع ودجت الـ hero من كلا المسارين ويُحركه في طبقة تراكب فوقهما، مغيراً حجمه وموضعه عبر الشاشة بينما يتلاشى كلا المسارين. عند الرجوع، تنعكس الرسوم المتحركة تلقائياً.

كيف ينسّق Flutter الرحلة

فهم الآلية يساعدك على تجنب المشكلات الشائعة:

  • عند وقت التنقل، يبحث Flutter عن كل Hero بعلامة متطابقة في المسار القديم والجديد.
  • يضع عنصراً بديلاً (placeholder) في كلا المسارين (حتى لا يتغير التخطيط) ويعرض الـ hero نفسه في طبقة تراكب تملأ الشاشة بالكامل فوق كلا المسارين.
  • يُشغّل AnimationController المدفوع بانتقال المسار الـ hero من مستطيله المصدر إلى مستطيله الوجهة.
  • يتم بناء ودجت التراكب باستخدام child الـ hero الوجهة بشكل افتراضي (يمكنك تجاوز ذلك بـ flightShuttleBuilder).
  • عند الانتهاء، يُهبَط الـ hero في المسار الوجهة وتُزال طبقة التراكب.
نصيحة: يُعاد بناء child الـ hero في التراكب، لذا يجب ألا يعتمد على قيم InheritedWidget (مثل Theme أو MediaQuery) التي تختلف بين المسارات. إذا استخدم hero الخاص بك Theme.of(context)، فقم بتغليفه في ودجت Theme صريح حتى تنتقل السمة الصحيحة معه.

تخصيص الرحلة باستخدام flightShuttleBuilder

بشكل افتراضي، الودجت المعروض أثناء الرحلة هو child الـ hero الوجهة. تتيح لك flightShuttleBuilder استبدال ودجت مختلف تماماً لمدة الرحلة — مفيد لتحويل الأشكال، أو إظهار حالة تحميل، أو مزج المرئيات المصدر والوجهة.

مثال 2 — ودجت رحلة مخصص باستخدام flightShuttleBuilder

Hero(
  tag: 'avatar-\${user.id}',
  // الودجت المعروض أثناء الرحلة (ليس child المصدر أو الوجهة)
  flightShuttleBuilder: (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) {
    // تلاشٍ متقاطع بين child المصدر والوجهة
    return AnimatedBuilder(
      animation: animation,
      builder: (context, _) {
        final Widget fromChild =
            (fromHeroContext.widget as Hero).child;
        final Widget toChild =
            (toHeroContext.widget as Hero).child;
        return Stack(
          fit: StackFit.expand,
          children: [
            Opacity(opacity: 1.0 - animation.value, child: fromChild),
            Opacity(opacity: animation.value, child: toChild),
          ],
        );
      },
    );
  },
  child: CircleAvatar(
    backgroundImage: NetworkImage(user.avatarUrl),
    radius: 24,
  ),
)

المعاملات الخمسة لـ flightShuttleBuilder تمنحك كل ما تحتاجه: قيمة الرسوم المتحركة (0.0 → 1.0 عند الدفع، 1.0 → 0.0 عند الرجوع)، واتجاه الرحلة، وكلا سياقَي الـ hero حتى تتمكن من استخراج ومزج children المصدر والوجهة.

التحكم في سلوك العنصر البديل

بينما الـ hero في الرحلة، يشغل عنصر بديل (placeholder) موضعه في تخطيط كل مسار. بشكل افتراضي هذا العنصر البديل هو صندوق شفاف بنفس حجم الـ hero. يمكنك تخصيصه باستخدام placeholderBuilder:

  • placeholderBuilder: (context, heroSize, child) => SizedBox.fromSize(size: heroSize) — صندوق غير مرئي (مشابه للافتراضي).
  • قدّم تأثير بريق أو نسخة ضبابية من الصورة لمنع الإزاحات المفاجئة في التخطيط على الصفحة المصدر أثناء الرحلة.
تحذير: لا تضع أبداً Hero داخل ودجت يقصّ عناصره الفرعية (مثل ClipRRect يلف الـ Hero). القص يمنع الـ hero من العرض في طبقة التراكب. بدلاً من ذلك، طبّق أي قص داخل child الـ hero، أو استخدم flightShuttleBuilder الخاص بـ Hero لتحريك نصف قطر الحدود أثناء الرحلة.

تحريك نصف قطر الحدود أثناء الرحلة

تأثير شائع هو التحريك من صورة رمزية دائرية (مسار صغير) إلى صورة مستطيلة (مسار التفصيل). لأن حجم الـ hero يُستَكمل تلقائياً، تحتاج فقط إلى تحريك نصف قطر الحدود داخل flightShuttleBuilder باستخدام قيمة الرسوم المتحركة:

  • استخدم BorderRadiusTween داخل flightShuttleBuilder للاستكمال بين BorderRadius.circular(50) وBorderRadius.zero.
  • غلّف النتيجة في ClipRRect بنصف القطر المستكمَل.

الخلاصة

يجعل ودجت Hero انتقالات العنصر المشترك سهلة: طابق العلامات على كلا المسارين وسيتولى Flutter إدارة الرحلة. للمرئيات المخصصة أثناء الرحلة، قدّم flightShuttleBuilder؛ وللعناصر البديلة المخصصة، استخدم placeholderBuilder. تأكد دائماً من أن العلامات فريدة لكل مسار وتجنب تغليف Hero داخل ودجات التقطيع. بهذه الأدوات يمكنك إنشاء الرسوم المتحركة الاحترافية بين الشاشات التي تميز تطبيقات Flutter عالية الجودة.