Stack و Positioned
مقدمة إلى Stack
بينما يرتب Row و Column العناصر الفرعية بشكل خطي، يتيح لك عنصر Stack تراكب العناصر الفرعية فوق بعضها البعض. فكّر فيه كطبقات من العناصر مثل البطاقات على طاولة -- العنصر الأول في الأسفل، وكل عنصر لاحق يُرسم فوقه.
يُعدّ Stack أساسياً لبناء واجهات المستخدم التي تتطلب عناصر متراكبة: شارات على الأيقونات، نصوص متراكبة على الصور، أزرار عائمة، وتخطيطات بطاقات معقدة.
الاستخدام الأساسي لـ Stack
يحدد Stack حجمه ليحتوي جميع عناصره الفرعية غير المحددة الموضع ثم يضع كل عنصر فرعي في الزاوية العلوية اليسرى افتراضياً.
مثال Stack بسيط
Stack(
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.blue,
),
Container(
width: 150,
height: 150,
color: Colors.red,
),
Container(
width: 100,
height: 100,
color: Colors.yellow,
),
],
)
// ثلاث مربعات متراكبة: أزرق (خلف)، أحمر (وسط)، أصفر (أمام)
تبدأ الحاويات الثلاث جميعها من الزاوية العلوية اليسرى للـ Stack، مما ينشئ تأثير طبقات. يتحدد حجم Stack بأكبر عنصر فرعي غير محدد الموضع (200x200 في هذه الحالة).
عنصر Positioned
يُستخدم Positioned داخل Stack للتحكم بدقة في مكان وضع العنصر الفرعي. تحدد المسافات من حواف Stack باستخدام خصائص top و bottom و left و right.
مثال عنصر Positioned
Stack(
children: <Widget>[
Container(
width: 300,
height: 200,
color: Colors.grey[300],
),
Positioned(
top: 10,
left: 10,
child: Container(
width: 80,
height: 80,
color: Colors.blue,
child: Center(child: Text('أع', style: TextStyle(color: Colors.white))),
),
),
Positioned(
bottom: 10,
right: 10,
child: Container(
width: 80,
height: 80,
color: Colors.red,
child: Center(child: Text('أس', style: TextStyle(color: Colors.white))),
),
),
],
)
الحاوية الزرقاء موضوعة على بعد 10 بكسل من الحافتين العلوية واليسرى. الحاوية الحمراء موضوعة على بعد 10 بكسل من الحافتين السفلية واليمنى. الحاوية الرمادية غير محددة الموضع وتحدد حجم Stack.
left و right معاً) لتمديد عنصر Positioned عبر Stack. يتحدد حجم العنصر الفرعي حينها بالمسافة بين الحواف.Positioned.fill
حاجة شائعة هي جعل عنصر Positioned يملأ Stack بالكامل. Positioned.fill هو مُنشئ مريح يعيّن جميع الحواف الأربع إلى 0:
مثال Positioned.fill
Stack(
children: <Widget>[
// صورة الخلفية تملأ Stack بالكامل
Positioned.fill(
child: Image.network(
'https://example.com/photo.jpg',
fit: BoxFit.cover,
),
),
// طبقة داكنة
Positioned.fill(
child: Container(color: Colors.black.withOpacity(0.4)),
),
// نص في الأعلى
Center(
child: Text(
'مرحباً',
style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold),
),
),
],
)
المحاذاة في Stack
تتحكم خاصية alignment في مكان وضع العناصر الفرعية غير محددة الموضع داخل Stack. القيمة الافتراضية هي AlignmentDirectional.topStart.
محاذاة Stack
// توسيط جميع العناصر الفرعية غير محددة الموضع
Stack(
alignment: Alignment.center,
children: <Widget>[
Container(width: 200, height: 200, color: Colors.blue),
Container(width: 100, height: 100, color: Colors.red),
// الأحمر موسّط على الأزرق
],
)
// محاذاة أسفل-يمين
Stack(
alignment: Alignment.bottomRight,
children: <Widget>[
Container(width: 200, height: 200, color: Colors.blue),
Container(width: 100, height: 100, color: Colors.red),
// الأحمر في أسفل-يمين الأزرق
],
)
alignment تؤثر فقط على العناصر الفرعية غير محددة الموضع. العناصر المغلّفة بـ Positioned توضع وفقاً لقيم top/bottom/left/right الصريحة وتتجاهل محاذاة Stack.خاصية fit
تتحكم خاصية fit في كيفية تحديد حجم العناصر الفرعية غير محددة الموضع نسبةً إلى Stack:
- StackFit.loose (الافتراضي) -- يمكن للعناصر الفرعية غير محددة الموضع أن تكون بأي حجم حتى حجم Stack. يفرض Stack قيوداً قصوى من عنصره الأب لكنه يسمح للعناصر الفرعية بأن تكون أصغر.
- StackFit.expand -- تُجبر العناصر الفرعية غير محددة الموضع على ملء Stack بالكامل. تُعيّن القيود لتكون بالضبط بحجم Stack.
- StackFit.passthrough -- تُمرر القيود من عنصر Stack الأب مباشرةً إلى العناصر الفرعية غير محددة الموضع بدون تعديل.
أمثلة StackFit
// يمكن للعناصر الفرعية أن تكون بأي حجم (الافتراضي)
SizedBox(
width: 300,
height: 200,
child: Stack(
fit: StackFit.loose,
children: [
Container(width: 100, height: 100, color: Colors.blue),
// الأزرق بحجم 100x100 داخل Stack بحجم 300x200
],
),
)
// تُجبر العناصر الفرعية على ملء Stack
SizedBox(
width: 300,
height: 200,
child: Stack(
fit: StackFit.expand,
children: [
Container(color: Colors.blue),
// الأزرق يملأ Stack بالكامل 300x200
],
),
)
clipBehavior
تتحكم خاصية clipBehavior فيما يحدث عندما تمتد عناصر Positioned الفرعية خارج حدود Stack:
- Clip.hardEdge (الافتراضي) -- تُقطع العناصر الفرعية التي تتجاوز حدود Stack.
- Clip.none -- يمكن للعناصر الفرعية الامتداد بصرياً خارج حدود Stack. مفيد للظلال والتلميحات والعناصر الزخرفية.
- Clip.antiAlias -- قطع مع تنعيم الحواف لحواف أنعم.
مثال clipBehavior
Stack(
clipBehavior: Clip.none,
children: <Widget>[
Container(width: 200, height: 200, color: Colors.blue),
Positioned(
top: -20,
right: -20,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Center(child: Text('3', style: TextStyle(color: Colors.white))),
),
),
],
)
// الدائرة الحمراء تمتد 20 بكسل خارج الزاوية العلوية اليمنى للـ Stack
Clip.none، تُرسم العناصر الفرعية المتجاوزة لكنها لا تتلقى أحداث اختبار النقر خارج حدود Stack. هذا يعني أن المستخدمين لا يمكنهم النقر على الجزء المتجاوز من زر أو عنصر تفاعلي.IndexedStack
IndexedStack هو نوع مختلف من Stack يعرض عنصراً فرعياً واحداً فقط في كل مرة بناءً على فهرس. جميع العناصر الفرعية تُبنى وتحتفظ بحالتها، لكن فقط العنصر الفرعي عند الفهرس المحدد يكون مرئياً.
مثال IndexedStack
class TabView extends StatefulWidget {
@override
State<TabView> createState() => _TabViewState();
}
class _TabViewState extends State<TabView> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: IndexedStack(
index: _selectedIndex,
children: <Widget>[
Center(child: Text('الصفحة الرئيسية', style: TextStyle(fontSize: 24))),
Center(child: Text('صفحة البحث', style: TextStyle(fontSize: 24))),
Center(child: Text('صفحة الملف الشخصي', style: TextStyle(fontSize: 24))),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(icon: Icon(Icons.home), onPressed: () => setState(() => _selectedIndex = 0)),
IconButton(icon: Icon(Icons.search), onPressed: () => setState(() => _selectedIndex = 1)),
IconButton(icon: Icon(Icons.person), onPressed: () => setState(() => _selectedIndex = 2)),
],
),
],
);
}
}
مثال عملي: شارة على أيقونة
شارة إشعار على أيقونة هي أحد أكثر أنماط Stack شيوعاً:
شارة الإشعار
Stack(
clipBehavior: Clip.none,
children: <Widget>[
Icon(Icons.notifications, size: 32, color: Colors.grey[700]),
Positioned(
top: -5,
right: -5,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: BoxConstraints(minWidth: 20, minHeight: 20),
child: Text(
'5',
style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
),
],
)
مثال عملي: نص متراكب على صورة
صورة رئيسية مع نص متراكب، تُستخدم بشكل شائع في البطاقات والشعارات:
صورة مع نص متراكب
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: <Widget>[
// صورة الخلفية
Image.network(
'https://example.com/city.jpg',
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
// طبقة تدرج لوني
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withOpacity(0.7)],
),
),
),
),
// محتوى النص
Positioned(
bottom: 16,
left: 16,
right: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('مدينة نيويورك', style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('استكشف المدينة التي لا تنام', style: TextStyle(color: Colors.white70, fontSize: 14)),
],
),
),
],
),
)
مثال عملي: عناوين عائمة
حقل نصي مع عنوان عائم مخصص موضوع خارج حدود الحقل:
عنوان عائم مخصص
Stack(
clipBehavior: Clip.none,
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.email, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text('user@example.com', style: TextStyle(fontSize: 16)),
),
],
),
),
Positioned(
top: -10,
left: 12,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8),
color: Colors.white,
child: Text('البريد الإلكتروني', style: TextStyle(color: Colors.blue, fontSize: 12)),
),
),
],
)
مثال عملي: بطاقة مخصصة مع طبقات
بطاقة منتج مع شارة خصم وزر مفضلة ومحتوى متراكب:
بطاقة منتج مع طبقات
Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Stack(
children: <Widget>[
// صورة المنتج
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
'https://example.com/product.jpg',
width: double.infinity,
height: 180,
fit: BoxFit.cover,
),
Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('سماعات فاخرة', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('\$99.99', style: TextStyle(fontSize: 18, color: Colors.green, fontWeight: FontWeight.bold)),
],
),
),
],
),
// شارة الخصم (أعلى-يسار)
Positioned(
top: 8,
left: 8,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text('-30%', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
),
),
// زر المفضلة (أعلى-يمين)
Positioned(
top: 8,
right: 8,
child: CircleAvatar(
backgroundColor: Colors.white,
radius: 18,
child: Icon(Icons.favorite_border, color: Colors.red, size: 20),
),
),
],
),
)
الملخص
- Stack يتراكب العناصر الفرعية فوق بعضها -- العنصر الأول في الخلف، الأخير في المقدمة.
- Positioned يضع عنصراً فرعياً في إحداثيات دقيقة داخل Stack باستخدام top و bottom و left و right.
Positioned.fillيمدد عنصراً فرعياً لتغطية Stack بالكامل.- خاصية
alignmentتتحكم في مكان وضع العناصر الفرعية غير محددة الموضع. - StackFit يتحكم في التحجيم:
loose(العناصر تختار حجمها)،expand(العناصر تملأ Stack)،passthrough(قيود الأب تُمرر). clipBehaviorيتحكم في التجاوز:Clip.noneيسمح بتجاوز مرئي لكنه يمنع اختبار النقر خارج الحدود.- IndexedStack يعرض عنصراً فرعياً واحداً فقط في كل مرة مع الحفاظ على حالة جميع العناصر الفرعية.
- الأنماط الشائعة: الشارات، طبقات الصور، العناوين العائمة، وتخطيطات البطاقات المعقدة.