تخطيطات Flutter والتصميم المتجاوب

Wrap و Flow

45 دقيقة الدرس 8 من 16

عنصر Wrap

عنصر Wrap يرتب عناصره الفرعية في سطر أفقي أو عمودي. عندما لا تكون هناك مساحة كافية في السطر الحالي، ينتقل إلى السطر التالي. على عكس Row الذي يتجاوز عندما تتخطى العناصر الفرعية المساحة المتاحة، يتعامل Wrap مع التجاوز بأناقة بإنشاء أسطر جديدة.

الفرق الرئيسي: Row يضع جميع العناصر في سطر واحد ويتجاوز إذا لم تتسع. Wrap ينقل العناصر المتجاوزة تلقائياً إلى سطر جديد. استخدم Wrap كلما كان لديك عدد ديناميكي من العناصر قد لا تتسع في صف واحد.

Wrap مقابل Row: مشكلة التجاوز

تجاوز Row مقابل Wrap

// ROW - يسبب خطأ تجاوز عندما لا تتسع الرقائق!
Row(
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Material Design')),
    Chip(label: Text('Responsive')),
    Chip(label: Text('Animation')),  // تجاوز!
  ],
)

// WRAP - ينتقل بأناقة إلى السطر التالي
Wrap(
  spacing: 8.0,      // الفجوة الأفقية بين الرقائق
  runSpacing: 4.0,   // الفجوة العمودية بين الأسطر
  children: [
    Chip(label: Text('Flutter')),
    Chip(label: Text('Dart')),
    Chip(label: Text('Firebase')),
    Chip(label: Text('Material Design')),
    Chip(label: Text('Responsive')),
    Chip(label: Text('Animation')),  // ينتقل إلى السطر التالي
  ],
)

خصائص Wrap

يوفر Wrap عدة خصائص للتحكم في سلوك التخطيط:

إعدادات Wrap

Wrap(
  // الاتجاه: أفقي (افتراضي) أو عمودي
  direction: Axis.horizontal,

  // محاذاة العناصر داخل السطر
  alignment: WrapAlignment.start,  // start, end, center, spaceBetween, spaceAround, spaceEvenly

  // التباعد بين العناصر في نفس السطر
  spacing: 8.0,

  // التباعد بين الأسطر
  runSpacing: 12.0,

  // محاذاة الأسطر داخل Wrap نفسه
  runAlignment: WrapAlignment.start,

  // محاذاة المحور العرضي للعناصر داخل السطر
  crossAxisAlignment: WrapCrossAlignment.center,

  // اتجاه النص (يؤثر على ترتيب التخطيط لـ RTL)
  textDirection: TextDirection.ltr,

  children: [
    // عناصر فرعية...
  ],
)

خيارات محاذاة Wrap

أمثلة المحاذاة

// سحابة وسوم بمحاذاة مركزية
Wrap(
  alignment: WrapAlignment.center,
  spacing: 8.0,
  runSpacing: 8.0,
  children: tags.map((tag) {
    return Chip(
      label: Text(tag),
      backgroundColor: Colors.blue.shade50,
    );
  }).toList(),
)

// أزرار موزعة بالتساوي
Wrap(
  alignment: WrapAlignment.spaceEvenly,
  spacing: 8.0,
  runSpacing: 12.0,
  children: [
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.share),
      label: const Text('مشاركة'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.bookmark),
      label: const Text('حفظ'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.download),
      label: const Text('تحميل'),
    ),
    ElevatedButton.icon(
      onPressed: () {},
      icon: const Icon(Icons.print),
      label: const Text('طباعة'),
    ),
  ],
)

// التفاف عمودي (من أعلى لأسفل، ثم العمود التالي)
Wrap(
  direction: Axis.vertical,
  spacing: 8.0,
  runSpacing: 16.0,
  children: items.map((item) => Text(item)).toList(),
)
نصيحة: استخدم WrapAlignment.spaceBetween لسحابات الوسوم حيث تريد توزيع العناصر على العرض الكامل. استخدم WrapAlignment.center عندما تريد تخطيطاً متوازناً ومتمركزاً بصرياً.

مثال عملي: سحابة الوسوم

سحابة وسوم تفاعلية

class TagCloudWidget extends StatefulWidget {
  final List<String> allTags;

  const TagCloudWidget({super.key, required this.allTags});

  @override
  State<TagCloudWidget> createState() => _TagCloudWidgetState();
}

class _TagCloudWidgetState extends State<TagCloudWidget> {
  final Set<String> _selectedTags = {};

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8.0,
      runSpacing: 8.0,
      children: widget.allTags.map((tag) {
        final isSelected = _selectedTags.contains(tag);
        return FilterChip(
          label: Text(tag),
          selected: isSelected,
          onSelected: (selected) {
            setState(() {
              if (selected) {
                _selectedTags.add(tag);
              } else {
                _selectedTags.remove(tag);
              }
            });
          },
          selectedColor: Colors.blue.shade100,
          checkmarkColor: Colors.blue,
        );
      }).toList(),
    );
  }
}

مثال عملي: شريط أزرار متجاوب

شريط إجراءات متجاوب

class ResponsiveActionBar extends StatelessWidget {
  const ResponsiveActionBar({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Wrap(
        alignment: WrapAlignment.end,
        spacing: 12.0,
        runSpacing: 8.0,
        children: [
          OutlinedButton(
            onPressed: () {},
            child: const Text('إلغاء'),
          ),
          OutlinedButton(
            onPressed: () {},
            child: const Text('حفظ مسودة'),
          ),
          FilledButton(
            onPressed: () {},
            child: const Text('نشر'),
          ),
        ],
      ),
    );
  }
}

عنصر Flow

عنصر Flow يوفر طريقة عالية الكفاءة لإنشاء تخطيطات مخصصة. يستخدم FlowDelegate لتموضع العناصر الفرعية بدقة مطلقة. Flow عالي الأداء لأنه يستخدم مصفوفة تحويل لتموضع العناصر دون تفعيل إعادة التخطيط، مما يجعله مثالياً للرسوم المتحركة.

تحذير: Flow هو عنصر منخفض المستوى مخصص للتخطيطات المتحركة المخصصة. لمعظم سيناريوهات الالتفاف، Wrap أبسط وكافٍ. استخدم Flow فقط عندما تحتاج منطق تموضع مخصص أو انتقالات تخطيط متحركة.

Flow مع FlowDelegate

class SimpleFlowDelegate extends FlowDelegate {
  final Animation<double> animation;

  SimpleFlowDelegate({required this.animation}) : super(repaint: animation);

  @override
  void paintChildren(FlowPaintingContext context) {
    final childCount = context.childCount;
    for (int i = 0; i < childCount; i++) {
      final dx = i * 60.0;
      final dy = 0.0;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(dx, dy, 0),
      );
    }
  }

  @override
  bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
    return animation != oldDelegate.animation;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return Size(constraints.maxWidth, 60.0);
  }
}

// الاستخدام
Flow(
  delegate: SimpleFlowDelegate(animation: _animationController),
  children: List.generate(5, (index) {
    return Container(
      width: 50.0,
      height: 50.0,
      decoration: BoxDecoration(
        color: Colors.primaries[index % Colors.primaries.length],
        shape: BoxShape.circle,
      ),
      child: Center(
        child: Text(
          '\${index + 1}',
          style: const TextStyle(color: Colors.white),
        ),
      ),
    );
  }),
)

مثال عملي: قائمة دائرية متحركة

قائمة Flow دائرية

class CircularMenuDelegate extends FlowDelegate {
  final Animation<double> animation;
  final double radius;

  CircularMenuDelegate({
    required this.animation,
    this.radius = 100.0,
  }) : super(repaint: animation);

  @override
  void paintChildren(FlowPaintingContext context) {
    final childCount = context.childCount;
    final angleStep = (pi * 0.5) / (childCount - 1);

    for (int i = 0; i < childCount; i++) {
      if (i == childCount - 1) {
        // العنصر الأخير هو الزر الرئيسي (المركز)
        context.paintChild(i);
      } else {
        // نشر عناصر القائمة في ربع دائرة
        final angle = i * angleStep;
        final progress = animation.value;
        final dx = -cos(angle) * radius * progress;
        final dy = -sin(angle) * radius * progress;

        context.paintChild(
          i,
          transform: Matrix4.translationValues(dx, dy, 0)
            ..scale(progress, progress),
          opacity: progress,
        );
      }
    }
  }

  @override
  bool shouldRepaint(CircularMenuDelegate oldDelegate) {
    return animation != oldDelegate.animation;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return const Size(56.0, 56.0);
  }
}

// الاستخدام في StatefulWidget مع AnimationController
class CircularMenu extends StatefulWidget {
  const CircularMenu({super.key});

  @override
  State<CircularMenu> createState() => _CircularMenuState();
}

class _CircularMenuState extends State<CircularMenu>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  bool _isOpen = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  void _toggle() {
    setState(() {
      _isOpen = !_isOpen;
      if (_isOpen) {
        _controller.forward();
      } else {
        _controller.reverse();
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: CircularMenuDelegate(
        animation: _controller,
        radius: 120.0,
      ),
      children: [
        _buildMenuItem(Icons.camera_alt, Colors.red),
        _buildMenuItem(Icons.photo, Colors.green),
        _buildMenuItem(Icons.videocam, Colors.blue),
        // زر التبديل الرئيسي (العنصر الأخير)
        FloatingActionButton(
          onPressed: _toggle,
          child: AnimatedIcon(
            icon: AnimatedIcons.menu_close,
            progress: _controller,
          ),
        ),
      ],
    );
  }

  Widget _buildMenuItem(IconData icon, Color color) {
    return FloatingActionButton(
      mini: true,
      backgroundColor: color,
      onPressed: () {
        _toggle();
        // معالجة نقر عنصر القائمة
      },
      child: Icon(icon),
    );
  }
}
نصيحة: الميزة الرئيسية لـ Flow على عناصر التخطيط الأخرى هي أن paintChildren يستخدم مصفوفة تحويل لتموضع العناصر. هذا يعني أن إعادة تموضع العناصر لا يفعّل مرور تخطيط — فقط إعادة رسم — مما يجعل الرسوم المتحركة سلسة وفعالة للغاية.
ملخص: استخدم Wrap عندما تحتاج العناصر للانتقال تلقائياً إلى السطر التالي عند نفاد المساحة — مثالي لسحابات الوسوم ومجموعات الرقائق وأشرطة الأزرار المتجاوبة. استخدم Flow عندما تحتاج تحكماً كاملاً في تموضع العناصر مع رسوم متحركة عالية الأداء — مثالي للقوائم المخصصة وتأثيرات التخطيط المتحركة.