الودجات المخصصة والرسم المخصص

الشرائح والعروض المخصصة للتمرير

16 دقيقة الدرس 11 من 12

الشرائح والعروض المخصصة للتمرير

ودجات التمرير القياسية في Flutter — ListView وGridView وSingleChildScrollView — سهلة الاستخدام، لكنها توفر قدرًا محدودًا من التحكم في كيفية تصرف أجزاء منطقة التمرير. الشرائح (Slivers) هي اللبنات الأساسية ذات المستوى الأدنى التي تُشغّل جميع عروض التمرير في Flutter. الشريحة هي جزء من منطقة قابلة للتمرير يمكنه تغيير هندسته (الحجم والموضع والتأثير البصري) استجابةً لإزاحة التمرير. بتركيب الشرائح داخل CustomScrollView، يمكنك إنشاء تخطيطات متطورة كأشرطة تطبيق تنهار، ورؤوس أقسام لاصقة، وعروض تجمع بين القوائم والشبكات.

ملاحظة: كل ListView وGridView مُنفَّذ داخليًا باستخدام الشرائح. عندما تحتاج إلى أكثر من نوع واحد من مناطق التمرير — مثل لافتة كبيرة يتبعها شبكة — يجب النزول إلى طبقة الشرائح واستخدام CustomScrollView مباشرةً.

CustomScrollView — الحاوية

يقبل CustomScrollView قائمة من الشرائح في خاصية slivers الخاصة به ويُمرِّرها كنافذة عرض موحدة واحدة. تشترك جميع العناصر الفرعية في نفس ScrollController وفيزياء التمرير، وهذا هو سبب انهيار شريط التطبيق القابل للتوسعة والقائمة أسفله بتزامن تام.

هيكل CustomScrollView البسيط

CustomScrollView(
  slivers: [
    // 1. شريط تطبيق منهار
    SliverAppBar(
      expandedHeight: 200.0,
      floating: false,
      pinned: true,
      flexibleSpace: FlexibleSpaceBar(
        title: const Text('تغذيتي'),
        background: Image.network(
          'https://picsum.photos/800/400',
          fit: BoxFit.cover,
        ),
      ),
    ),

    // 2. قسم من عناصر القائمة
    SliverList(
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return ListTile(title: Text('عنصر $index'));
        },
        childCount: 20,
      ),
    ),

    // 3. قسم من عناصر الشبكة
    SliverGrid(
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        mainAxisSpacing: 8.0,
        crossAxisSpacing: 8.0,
        childAspectRatio: 1.0,
      ),
      delegate: SliverChildBuilderDelegate(
        (BuildContext context, int index) {
          return Container(color: Colors.teal, child: Center(child: Text('$index')));
        },
        childCount: 12,
      ),
    ),
  ],
)

ودجات الشرائح الأساسية

SliverAppBar

يتكامل SliverAppBar مع إزاحة التمرير لتوسيع منطقة الرأس وطيّها. الخصائص الرئيسية:

  • expandedHeight — الارتفاع الإجمالي عند التوسعة الكاملة.
  • pinned: true — يُبقي شريط الأدوات مرئيًا بعد الطي.
  • floating: true — يُعيد التوسعة بمجرد أن يمرر المستخدم للأعلى، حتى في منتصف القائمة.
  • snap: true — يُستخدم مع floating؛ يُلتقط الشريط ليُفتح أو يُغلق بالكامل.
  • flexibleSpace: FlexibleSpaceBar — يعرض المنطقة القابلة للتوسعة (صورة خلفية، عنوان يتلاشى أو يتحرك).

SliverList

يعرض SliverList العناصر الفرعية بشكل كسول في قائمة خطية. استخدم SliverChildBuilderDelegate للقوائم الكبيرة أو اللانهائية، أو SliverChildListDelegate لمجموعة صغيرة وثابتة من الودجات.

SliverGrid

يعرض SliverGrid العناصر الفرعية في شبكة ثنائية الأبعاد. gridDelegate هو إما SliverGridDelegateWithFixedCrossAxisCount (عدد أعمدة ثابت) أو SliverGridDelegateWithMaxCrossAxisExtent (عدد أعمدة تلقائي بناءً على أقصى عرض للعنصر).

SliverToBoxAdapter

يُغلّف ودجت عادي (صندوقي) واحدًا حتى يمكن وضعه داخل CustomScrollView. استخدمه للأقسام المستقلة كاللافتات وأشرطة البحث وصفوف العناوين التي لا تنتمي إلى قائمة أو شبكة.

SliverPadding

يُطبّق حشوًا حول شريحة أخرى. يعادل تغليف ودجت صندوقي في Padding، لكنه يعمل في هندسة الشرائح.

نصيحة: امزج هذه الشرائح وتناسبها بحرية. وصفة شائعة: SliverAppBarSliverToBoxAdapter (لافتة ترويجية) ← SliverList (عناصر مميزة) ← SliverGrid (جميع العناصر). تتمرر كل قسم كتجربة واحدة سلسة.

SliverPersistentHeader والمفوَّضون المخصصون

للرؤوس المنهارة المخصصة تمامًا — تلك التي تُغيّر التخطيط أو تُحرّك العناصر الفرعية أو تُطبّق مزجًا مخصصًا أثناء الانهيار — استخدم SliverPersistentHeader مع SliverPersistentHeaderDelegate مُنجَز يدويًا.

يجب عليك توسيع SliverPersistentHeaderDelegate وتنفيذ ثلاثة أعضاء:

  • double get minExtent — الحد الأدنى لارتفاع الرأس (منهار بالكامل).
  • double get maxExtent — الحد الأقصى لارتفاع الرأس (موسَّع بالكامل).
  • Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) — يُستدعى في كل إطار؛ shrinkOffset يتراوح من 0 (موسَّع) إلى maxExtent - minExtent (منهار بالكامل).
  • bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) — أعد true عندما تتغير تهيئة المفوَّض.

رأس ملف شخصي منهار مخصص

class ProfileHeaderDelegate extends SliverPersistentHeaderDelegate {
  final String username;
  final String avatarUrl;

  const ProfileHeaderDelegate({
    required this.username,
    required this.avatarUrl,
  });

  @override
  double get minExtent => 60.0;

  @override
  double get maxExtent => 200.0;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    // احسب مدى تقدم الانهيار (0.0 = مفتوح، 1.0 = مغلق)
    final double t = (shrinkOffset / (maxExtent - minExtent)).clamp(0.0, 1.0);
    final double avatarSize = lerpDouble(80.0, 32.0, t)!;

    return Container(
      color: Colors.indigo,
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          CircleAvatar(
            radius: avatarSize / 2,
            backgroundImage: NetworkImage(avatarUrl),
          ),
          const SizedBox(width: 12.0),
          Text(
            username,
            style: TextStyle(
              color: Colors.white,
              fontSize: lerpDouble(22.0, 16.0, t)!,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    );
  }

  @override
  bool shouldRebuild(covariant ProfileHeaderDelegate oldDelegate) {
    return oldDelegate.username != username ||
        oldDelegate.avatarUrl != avatarUrl;
  }
}

// الاستخدام داخل CustomScrollView:
SliverPersistentHeader(
  pinned: true,
  delegate: ProfileHeaderDelegate(
    username: 'إدريس صالح',
    avatarUrl: 'https://picsum.photos/200',
  ),
),
تحذير: لا تستخدم أبدًا ارتفاعًا ثابتًا داخل طريقة build الخاصة بـ SliverPersistentHeaderDelegate. يُقيّد الإطار الرأسَ في النطاق [minExtent, maxExtent]، لذا فإن أي ودجت داخلي يطلب ارتفاعًا خارج هذا النطاق سيتجاوز الحدود أو يُقطع بشكل غير متوقع.

اعتبارات الأداء

تعرض الشرائح فقط ما هو مرئي حاليًا في نافذة العرض (التقييم الكسول)، مما يجعلها مناسبة للقوائم الطويلة أو اللانهائية. ضع هذه القواعد في الاعتبار:

  • فضِّل SliverChildBuilderDelegate على SliverChildListDelegate للقوائم التي تزيد عن ~20 عنصرًا — نسخة المُنشئ تبني العناصر عند الطلب.
  • تجنب تضمين ListView (أو أي عرض تمرير متقلص آخر) داخل شريحة CustomScrollView — استخدم SliverList بدلاً منه للإبقاء على كل شيء في نفس سياق التمرير.
  • تُستدعى طريقة build الخاصة بـ SliverPersistentHeaderDelegate في كل إطار تمرير؛ احرص على إبقائها خفيفة (لا حسابات ثقيلة، ولا إدخال/إخراج متزامن).

ملخص

تمنحك الشرائح تحكمًا دقيقًا في كل مقطع من واجهة المستخدم القابلة للتمرير. ركِّب CustomScrollView من SliverAppBar وSliverList وSliverGrid وSliverToBoxAdapter وSliverPadding للاحتياجات القياسية. للرؤوس المنهارة المخصصة التي تُحرَّك أو تُغيّر التخطيط أثناء تقلصها، نفِّذ SliverPersistentHeaderDelegate واحسب هندسة ودجاتك باستخدام معامل shrinkOffset. يُتيح لك هذا التوليف إنشاء تجارب تمرير غنية وعالية الأداء يستحيل تحقيقها بودجات التخطيط الصندوقي العادية.