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

ConstrainedBox و FittedBox و AspectRatio

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

فهم قيود التخطيط

نظام التخطيط في Flutter مبني على مفهوم القيود. كل عنصر واجهة يتلقى قيودًا من العنصر الأب (الحد الأدنى والأقصى للعرض/الارتفاع) ويجب عليه اختيار حجم ضمن تلك الحدود. في هذا الدرس، نستكشف العناصر التي تتيح لك التحكم وتجاوز وتكييف القيود لبناء تخطيطات دقيقة ومتجاوبة.

مبدأ أساسي: في Flutter، القيود تنزل للأسفل، والأحجام تصعد للأعلى، والعناصر الأب تحدد المواضع. لا يمكن للعنصر اختيار موضعه بنفسه—يمكنه فقط تحديد حجمه ضمن القيود المعطاة من العنصر الأب.

ConstrainedBox

يفرض ConstrainedBox قيودًا إضافية على العنصر الفرعي. يستخدم كائن BoxConstraints الذي يحدد minWidth وmaxWidth وminHeight وmaxHeight. القيود النهائية المطبقة على العنصر الفرعي هي تقاطع قيود الأب والقيود التي تحددها أنت.

استخدام ConstrainedBox الأساسي

ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 100,
    maxWidth: 300,
    minHeight: 50,
    maxHeight: 200,
  ),
  child: Container(
    color: Colors.blue,
    child: const Text('أنا مُقيّد!'),
  ),
)

مُنشئات المصنع الشائعة لـ BoxConstraints تُبسّط الأنماط الشائعة:

مُنشئات مصنع BoxConstraints

// فرض حجم محدد
BoxConstraints.tight(const Size(200, 100))

// السماح بأي حجم حتى الأبعاد المعطاة
BoxConstraints.loose(const Size(300, 200))

// التوسع لملء كل المساحة المتاحة
const BoxConstraints.expand()

// عرض ثابت، ارتفاع مرن
const BoxConstraints.tightFor(width: 250)

// تقييد محور واحد فقط
const BoxConstraints(
  maxWidth: 400,
  // minWidth الافتراضي 0.0
  // الارتفاع غير مقيد
)
نصيحة: يمكن لـ ConstrainedBox فقط تشديد القيود—لا يمكنه جعلها أكثر مرونة. إذا كان الأب يقول بالفعل “يجب أن يكون العرض 100 بالضبط”، فإن ConstrainedBox بـ maxWidth: 300 لن يكون له أي تأثير.

UnconstrainedBox

يزيل UnconstrainedBox قيود الأب بالكامل، مما يسمح للعنصر الفرعي بالعرض بحجمه الطبيعي (الجوهري). هذا مفيد عندما يجبر الأب عنصرًا على التوسع لكنك تريد أن يكون الفرعي أصغر. كن حذرًا—إذا تجاوز الفرعي الحدود، سيعرض Flutter تحذير تجاوز.

مثال UnconstrainedBox

// داخل Row يمد العناصر الفرعية
Row(
  children: [
    Expanded(
      child: UnconstrainedBox(
        child: Container(
          width: 80,
          height: 40,
          color: Colors.green,
          child: const Center(
            child: Text('حر!'),
          ),
        ),
      ),
    ),
  ],
)

LimitedBox

يحد LimitedBox حجم العنصر الفرعي فقط عندما تكون القيود الواردة غير محدودة. هذا مفيد بشكل خاص داخل العروض القابلة للتمرير مثل ListView حيث يكون الارتفاع غير مقيد.

LimitedBox في ListView

ListView(
  children: [
    LimitedBox(
      maxHeight: 200,
      child: Container(
        color: Colors.orange,
        child: const Center(
          child: Text('أقصى ارتفاع 200 بكسل في القائمة'),
        ),
      ),
    ),
  ],
)

OverflowBox

يمرر OverflowBox قيودًا مختلفة لعنصره الفرعي عن تلك التي تلقاها من عنصره الأب. على عكس ConstrainedBox، يمكنه فعلاً تخفيف القيود. قد يرسم العنصر الفرعي خارج حدود OverflowBox.

مثال OverflowBox

SizedBox(
  width: 100,
  height: 100,
  child: OverflowBox(
    maxWidth: 200,
    maxHeight: 200,
    child: Container(
      width: 200,
      height: 200,
      color: Colors.red.withOpacity(0.5),
      child: const Center(
        child: Text('أنا أتجاوز!'),
      ),
    ),
  ),
)

FittedBox

يقوم FittedBox بتحجيم وتموضع عنصره الفرعي داخله وفقًا لوضع BoxFit. وهو مفيد بشكل خاص لتحجيم النصوص والصور أو أشجار العناصر بأكملها لتناسب مساحة معينة.

FittedBox مع أوضاع BoxFit

// تحجيم النص لملء العرض
SizedBox(
  width: 300,
  height: 50,
  child: FittedBox(
    fit: BoxFit.scaleDown,
    child: Text(
      'هذا النص يتقلص إذا كان عريضًا جدًا',
      style: const TextStyle(fontSize: 40),
    ),
  ),
)

// قيم BoxFit الشائعة:
// BoxFit.contain   - التحجيم للملاءمة مع الحفاظ على نسبة العرض إلى الارتفاع
// BoxFit.cover     - التحجيم للملء مع الحفاظ على النسبة وقد يُقص
// BoxFit.fill      - التمدد للملء وقد يُشوّه
// BoxFit.fitWidth  - تحجيم العرض للملاءمة وقد يتجاوز الارتفاع
// BoxFit.fitHeight - تحجيم الارتفاع للملاءمة وقد يتجاوز العرض
// BoxFit.scaleDown - مثل contain لكن يُصغّر فقط
// BoxFit.none      - بدون تحجيم مع توسيط العنصر الفرعي
تحذير: يتطلب FittedBox أن يكون لعنصره الفرعي حجم محدود. إذا حاول الفرعي أن يكون عريضًا بلا حدود (مثل Row غير مقيد)، سيطرح Flutter خطأ. تأكد دائمًا من أن العنصر الفرعي له أبعاد محدودة.

مثال عملي: صورة متجاوبة

FittedBox للصور المتجاوبة

class ResponsiveImage extends StatelessWidget {
  final String imageUrl;

  const ResponsiveImage({super.key, required this.imageUrl});

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(
        maxWidth: 600,
        maxHeight: 400,
      ),
      child: FittedBox(
        fit: BoxFit.contain,
        child: Image.network(imageUrl),
      ),
    );
  }
}

AspectRatio

يقوم عنصر AspectRatio بتحجيم عنصره الفرعي وفقًا لنسبة عرض إلى ارتفاع محددة. يحاول أولاً مطابقة أكبر عرض تسمح به القيود، ثم يحدد الارتفاع وفقًا للنسبة. إذا وفّر الأب قيودًا ضيقة، قد لا تكون نسبة العرض إلى الارتفاع قابلة للتحقيق.

عنصر AspectRatio

// حاوية مشغل فيديو 16:9
AspectRatio(
  aspectRatio: 16 / 9,
  child: Container(
    color: Colors.black,
    child: const Center(
      child: Icon(
        Icons.play_circle_outline,
        color: Colors.white,
        size: 64,
      ),
    ),
  ),
)

// حاوية مربعة
AspectRatio(
  aspectRatio: 1.0,
  child: Container(
    decoration: BoxDecoration(
      color: Colors.purple,
      borderRadius: BorderRadius.circular(12),
    ),
    child: const Center(
      child: Text(
        '1:1',
        style: TextStyle(color: Colors.white, fontSize: 24),
      ),
    ),
  ),
)

IntrinsicHeight و IntrinsicWidth

يقوم IntrinsicHeight بتحجيم عنصره الفرعي وفقًا لارتفاعه الجوهري، وIntrinsicWidth يفعل نفس الشيء للعرض. هذه مفيدة عندما تحتاج أن تتطابق العناصر المتجاورة في الحجم لكن نظام التخطيط لا يفعل ذلك تلقائيًا.

IntrinsicHeight لبطاقات متساوية الارتفاع

IntrinsicHeight(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      Expanded(
        child: Card(
          color: Colors.blue[50],
          child: const Padding(
            padding: EdgeInsets.all(16),
            child: Text('نص قصير'),
          ),
        ),
      ),
      Expanded(
        child: Card(
          color: Colors.green[50],
          child: const Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              'هذه البطاقة تحتوي على محتوى أكثر بكثير '
              'مما يجعلها أطول. البطاقة الأخرى ستتمدد '
              'لتطابق هذا الارتفاع.',
            ),
          ),
        ),
      ),
    ],
  ),
)
تحذير أداء: يقوم IntrinsicHeight وIntrinsicWidth بإجراء تمرير تخطيط استكشافي، يقيس العنصر الفرعي مرتين. استخدمهما باعتدال—يمكن أن يسببا أداء O(n²) عند التداخل. فضّل استراتيجيات تخطيط أخرى (مثل Table أو CrossAxisAlignment.stretch) عندما يكون ذلك ممكنًا.

مثال عملي: حقل إدخال مُقيّد

حقل نص مُقيّد

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 200,
          maxWidth: 500,
        ),
        child: TextField(
          decoration: InputDecoration(
            labelText: 'عنوان البريد الإلكتروني',
            hintText: 'أدخل بريدك الإلكتروني',
            border: OutlineInputBorder(
              borderRadius: BorderRadius.circular(8),
            ),
            prefixIcon: const Icon(Icons.email),
          ),
        ),
      ),
    );
  }
}

مثال عملي: مشغل فيديو بنسبة عرض إلى ارتفاع

مشغل فيديو مع AspectRatio

class VideoPlayerCard extends StatelessWidget {
  final String title;
  final String thumbnailUrl;

  const VideoPlayerCard({
    super.key,
    required this.title,
    required this.thumbnailUrl,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          AspectRatio(
            aspectRatio: 16 / 9,
            child: Stack(
              fit: StackFit.expand,
              children: [
                Image.network(
                  thumbnailUrl,
                  fit: BoxFit.cover,
                ),
                Container(
                  color: Colors.black26,
                  child: const Center(
                    child: Icon(
                      Icons.play_circle_fill,
                      color: Colors.white,
                      size: 56,
                    ),
                  ),
                ),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(12),
            child: Text(
              title,
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ],
      ),
    );
  }
}
ملخص: استخدم ConstrainedBox لإضافة حدود دنيا/قصوى، وFittedBox لتحجيم المحتوى في مساحة، وAspectRatio للحفاظ على التحجيم النسبي، وIntrinsicHeight/Width فقط عندما لا يحقق أي عنصر تخطيط آخر النتيجة المطلوبة. معًا، تمنحك هذه العناصر تحكمًا دقيقًا في كيفية تحجيم العناصر ضمن نظام التخطيط القائم على القيود في Flutter.