تخطَّ إلى المحتوى

العناصر المتراكبة في Flutter: Stack و Positioned و Align

الدرس 14 من 33 · Flutter

Row يرتب العناصر بجانب بعضها، و Column يرتبها تحت بعضها. لكن ماذا عن دائرة حمراء صغيرة فوق أيقونة الجرس؟ أو عنوان مكتوب فوق صورة؟ أو زر مثبت في زاوية بطاقة؟ هنا يأتي دور Stack: المحور الثالث في التصميم، من الخلف إلى الأمام.

Stack يضع أبناءه فوق بعضهم كطبقات. تخيّل أوراقاً شفافة تضعها واحدة فوق الأخرى على المكتب: أول ورقة في الأسفل، وكل ورقة جديدة تغطي ما تحتها.

Stack(
children: [
Container(width: 160, height: 160, color: Colors.indigo),
Container(width: 110, height: 110, color: Colors.amber),
Container(width: 60, height: 60, color: Colors.red),
],
)

الترتيب في الكود هو ترتيب الطبقات: أول ابن في الخلف، وآخر ابن في الأمام. وافتراضياً تتجمع العناصر كلها عند زاوية البداية العلوية (اليمين العلوي في التطبيق العربي).

لتغيير مكان تجمّع الأبناء:

Stack(
alignment: Alignment.center,
children: [
Container(width: 160, height: 160, color: Colors.indigo),
const Text('في المنتصف', style: TextStyle(color: Colors.white)),
],
)

Alignment.center تضع كل الأبناء في منتصف الـ Stack. ومن القيم الأخرى: topCenter و bottomCenter و centerLeft و bottomRight.

تحديد مكان ابن واحد: Positioned

Section titled “تحديد مكان ابن واحد: Positioned”

alignment تحرّك كل الأبناء معاً. لتحديد مكان ابن بعينه، لفّه بـ Positioned واكتب بُعده عن الحواف:

Stack(
children: [
Container(width: 200, height: 120, color: Colors.indigo.shade100),
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
color: Colors.red,
child: const Text('خصم 20%', style: TextStyle(color: Colors.white)),
),
),
],
)
  • top و bottom و left و right: المسافة بين الابن وحافة الـ Stack.
  • إذا كتبت left و right معاً، يتمدد الابن بينهما.
  • Positioned.fill: اختصار يجعل الابن يملأ الـ Stack كله.
  • PositionedDirectional: نسخة تستخدم start و end بدلاً من left و right، لتحترم اتجاه اللغة.

تخيّل Positioned مثل دبوس تثبّت به ورقة صغيرة على لوحة: «على بعد 8 من الأعلى و 8 من اليسار».

Align بديل أبسط عندما تريد وضع ابن في زاوية أو في المنتصف بدون أرقام:

Stack(
children: [
Container(width: 200, height: 120, color: Colors.teal.shade100),
const Align(
alignment: Alignment.bottomCenter,
child: Text('أسفل المنتصف'),
),
],
)

استخدم Align للأماكن المعروفة (زاوية، منتصف)، و Positioned عندما تحتاج مسافة دقيقة بالأرقام.

الخروج عن الحدود: clipBehavior

Section titled “الخروج عن الحدود: clipBehavior”

افتراضياً يقص الـ Stack أي جزء يخرج عن حدوده. لكن شارة الإشعارات مثلاً تخرج قليلاً عن حدود الأيقونة عمداً. للسماح بذلك:

Stack(
clipBehavior: Clip.none,
children: [
const Icon(Icons.notifications, size: 36),
Positioned(
top: -4,
right: -4,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text(
'3',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
],
)

القيم السالبة في top و right تُخرج الدائرة عن حدود الأيقونة، و Clip.none تمنع قصها.

النص الأبيض فوق صورة فاتحة لا يُقرأ. الحل الشائع: طبقة سوداء شبه شفافة بين الصورة والنص.

SizedBox(
height: 180,
child: Stack(
fit: StackFit.expand,
children: [
Image.asset('assets/images/cover.png', fit: BoxFit.cover),
Container(color: Colors.black.withValues(alpha: 0.4)),
const Center(
child: Text(
'عروض الصيف',
style: TextStyle(color: Colors.white, fontSize: 26),
),
),
],
),
)

fit: StackFit.expand تجعل كل الأبناء العاديين يتمددون بحجم الـ Stack، فتملأ الصورة وطبقة التعتيم المساحة كلها بدون أن تكتب أبعادهما.

المشروع: بطاقة منتج بشارات

Section titled “المشروع: بطاقة منتج بشارات”
lib/product_card.dart
class ProductCard extends StatelessWidget {
const ProductCard({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('منتج اليوم')),
body: Center(
child: SizedBox(
width: 240,
height: 240,
child: Stack(
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.asset('assets/images/cover.png', fit: BoxFit.cover),
),
Positioned(
top: 12,
left: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
child: const Text(
'خصم 20%',
style: TextStyle(color: Colors.white),
),
),
),
Positioned(
top: 4,
right: 4,
child: IconButton(
onPressed: () {},
icon: const Icon(Icons.favorite_border, color: Colors.white),
tooltip: 'أضف للمفضلة',
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(16),
),
),
child: const Text(
'سماعة لاسلكية',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
],
),
),
),
);
}
}

البطاقة أربع طبقات من الخلف إلى الأمام:

  1. الصورة تملأ البطاقة، مقصوصة بزوايا دائرية.
  2. شارة الخصم الحمراء، مثبتة في الزاوية العلوية اليسرى.
  3. زر القلب في الزاوية العلوية اليمنى.
  4. شريط سفلي شبه شفاف عليه اسم المنتج. كتبنا left: 0 و right: 0 معاً ليتمدد بعرض البطاقة كله.
  1. شغّل بطاقة المنتج وغيّر أماكن الشارات.
  2. حوّل البطاقة إلى StatefulWidget، واجعل القلب يتحول إلى Icons.favorite بلون أحمر عند الضغط.
  3. أضف شارة عدد فوق أيقونة سلة التسوق باستخدام Clip.none والقيم السالبة.
  4. تحدٍّ: اصنع صورة شخصية دائرية في زاويتها السفلية نقطة خضراء صغيرة تعني «متصل الآن».
  • Stack كل أبنائه Positioned: لا يوجد ابن يحدد الحجم، فيأخذ الـ Stack أكبر مساحة متاحة أو ينكمش إلى صفر. ضع ابناً عادياً أو لف الـ Stack بـ SizedBox.
  • استخدام Positioned خارج Stack: خطأ «Incorrect use of ParentDataWidget». Positioned يعمل فقط كابن مباشر لـ Stack.
  • استخدام Stack لكل شيء: ترتيب عناصر متجاورة بالأرقام هش وينكسر مع تغيّر حجم الشاشة. استخدم Row و Column للترتيب، و Stack للتراكب فقط.

سؤال

في Stack فيه ثلاثة أبناء، أي ابن يظهر في الأمام فوق الجميع؟

سؤال

كيف تجعل ابناً داخل Stack يتمدد بعرض الـ Stack كله في أسفله؟

  • Stack يرتب أبناءه كطبقات: الأول في الخلف والأخير في الأمام.
  • alignment تحاذي كل الأبناء، و Positioned و Align لابن واحد.
  • حجم الـ Stack من أكبر ابن غير مثبَّت، و StackFit.expand تمدد الأبناء العاديين.
  • Clip.none تسمح بخروج عنصر عن الحدود، مثل شارة الإشعارات.
  • طبقة سوداء شبه شفافة بين الصورة والنص تجعل النص مقروءاً.