نتعرف على ما هو ConstraintLayout — نظام تحديد مواضع مرن لنظام Android يتيح بناء تسلسلات هرمية مسطحة للعرض باستخدام القيود (constraints) بدلاً من LinearLayout و RelativeLayout المتداخلة. يحل ConstraintLayout مشكلة "التداخل الجهنمي" (layout nesting hell)، مما يقلل عمق التسلسل الهرمي إلى مستوى واحد ويسرع عرض الشاشة. المكتبة جزء من Jetpack ومتاحة بدءاً من Android 2.3 (API 9) عبر support-library. الآليات الرئيسية موصوفة في التوثيق الرسمي لنظام Android.
النقاط الرئيسية
ConstraintLayout هو ViewGroup من مكتبة AndroidX ConstraintLayout، مصمم لإنشاء واجهات مرنة وعالية الأداء من خلال قيود تصريحية. على عكس LinearLayout الذي يرتب العناصر في سطر واحد، أو RelativeLayout الذي يحدد مواضع العناصر بالنسبة للمجاورة، يتيح ConstraintLayout تثبيت كل عنصر بالنسبة لأي عناصر أخرى والأصل في وقت واحد.
تم الإعلان عن المكتبة في Google I/O 2016 كحل لتسريع عرض الشاشات المعقدة. المشكلة الرئيسية التي يحلها ConstraintLayout هي تداخل التخطيطات. كل ViewGroup متداخلة تضيف على الأقل تمريرتين measure وتمريرة layout واحدة. شاشة ذات 4 مستويات من التداخل تنفذ 8 تمريرات measure؛ ConstraintLayout بنفس الوظائف ينفذ تمريرتين فقط. وفقاً لـ Google (Android Performance Blog, 2017)، استبدال ثلاثة LinearLayouts متداخلة بـ ConstraintLayout واحد يقلل وقت onMeasure بنسبة 40%.
الإصدار الحالي ConstraintLayout 2.1.4 يعمل بثبات على Android 2.3+ (API 9) عبر AndroidX. الإصدار 2.0 قدم تحديد المواقع الدائري و Flow (التفاف العناصر تلقائياً) ودعم MotionLayout. ConstraintLayout ضروري لفهم تطوير Android الحديث — يُستخدم في Jetpack Compose كمفهوم أساسي للمعدّلات، وفي قوالب Android Studio الافتراضية، وفي Material Design 3.
التسلسل الهرمي المسطح لـ ConstraintLayout يعني أن جميع العروض التابعة في نفس مستوى التداخل. بدلاً من وضع العنصر A في LinearLayout، و LinearLayout في RelativeLayout، يتم ربط جميع العناصر مباشرة بـ ConstraintLayout الأصل أو ببعضها البعض من خلال السمات. يوفر هذا: استهلاكاً أقل للذاكرة (كل ViewGroup هي كائن في Java heap)، وlayout pass أسرع (استدعاءات تكرارية أقل)، وسلوكاً أكثر قابلية للتنبؤ عند تغير أحجام الشاشة.
القيد (constraint) هو اتصال بين حافة عرض (أو مركزها) وحافة عرض آخر أو الأصل. يمكن أن يحتوي كل عرض على ما يصل إلى 8 قيود: left و top و right و bottom و start و end و baseline و center. كحد أدنى، يكفي قيدان متعامدان لتحديد الموضع (مثل top + left).
تنسيق السمة: app:layout_constraint[Source]_to[Target]Of="[id]" — حيث Source هي الحافة المرتبطة (Left, Right, Top, Bottom, Start, End, Baseline) و Target هي الحافة الهدف. مثال: app:layout_constraintTop_toBottomOf="@+id/header" يعني "الحافة العلوية للعنصر الحالي مرتبطة بالحافة السفلية لعنصر header". للربط بالأصل، يُستخدم id parent.
الانحياز (Bias) هو معلمة تعمل عند وجود قيود متعاكسة (left + right أو top + bottom). تتراوح القيم من 0 إلى 1: 0 — مضغوط نحو الحافة اليسرى/العليا، 0.5 — في المنتصف، 1 — نحو الحافة اليمنى/السفلى. السمات: layout_constraintHorizontal_bias (0.0–1.0) و layout_constraintVertical_bias. يتم تعيين الهوامش باستخدام android:layout_margin* القياسية، لكن القيود والهوامش تعمل بشكل مستقل: الهامش هو إزاحة من القيد، وليس من العرض المجاور.
منذ ConstraintLayout 1.1+، تمت إضافة دعم الأحجام المئوية عبر layout_constraintWidth_percent و layout_constraintHeight_percent. القيمة 0.3 تعني 30% من عرض/ارتفاع الأصل. بالاقتران مع الانحياز، يتيح ذلك إنشاء تخطيطات متكيفة دون برمجة.
السلسلة (Chain) هي مجموعة من عرضين أو أكثر متصلة بقيود ثنائية الاتجاه (A مرتبط بـ B، B مرتبط بـ A). توزع السلاسل المساحة تلقائياً بين العناصر وفقاً لأحد الأوضاع: spread (بالتساوي مع مراعاة الهوامش)، spread_inside (بالتساوي، العناصر الخارجية بدون هامش حافة)، packed (العناصر مضغوطة معاً بانحياز مشترك). يُحدد الوضع عبر السمة app:layout_constraintHorizontal_chainStyle أو layout_constraintVertical_chainStyle.
الخط الإرشادي (Guideline) هو عرض مساعد، غير مرئي أثناء التشغيل، يحدد خطاً للربط. يمكن أن يكون الخط الإرشادي أفقياً أو عمودياً، موضوعاً بـ dp أو نسب مئوية (app:layout_constraintGuide_percent) أو بإزاحة من الحافة (app:layout_constraintGuide_begin/end). الخطوط الإرشادية لا غنى عنها للتخطيطات المتكيفة — على سبيل المثال، لتقسيم الشاشة إلى نصفين متساويين بغض النظر عن حجم الجهاز.
وفقاً لـ Google I/O 2017، السلاسل مع spread_inside أكثر كفاءة بنسبة 15–20% من LinearLayouts المتداخلة مع weight، لأنها تتجنب تمريرة measure المزدوجة اللازمة لحساب weight.
Barrier (الحاجز) هو عرض افتراضي يضبط موضعه ديناميكياً بناءً على حجم مجموعة من العناصر. على عكس Guideline ذو الموضع الثابت، يتم "دفع" الحاجز بواسطة أوسع عنصر في المجموعة. على سبيل المثال، إذا كان لديك عنوان ووصف بأطوال غير معروفة، فإن Barrier مرتبط بالحافة اليمنى لأوسع نص يتيح وضع أيقونة بعده مباشرة. السمات: app:barrierDirection (left, right, top, bottom, start, end) و app:constraint_referenced_ids (قائمة ids مفصولة بفواصل).
Group هو حاوية افتراضية تدير رؤية عروض متعددة في وقت واحد. بدلاً من استدعاء setVisibility لكل عنصر على حدة، يكفي تغيير رؤية Group واحد. Group لا يؤثر على تحديد المواقع — فقط على الرؤية. Flow هو مساعد افتراضي لإنشاء تخطيطات "متدفقة": تنتقل العناصر تلقائياً إلى صف/عمود جديد عند نفاد المساحة، مثل النص في فقرة. يدعم Flow wrapMode: none و chain و aligned.
تسمى هذه الأدوات (Barrier و Group و Flow و Guideline) بالمساعدات الافتراضية لأنها ليست عروضاً بالمعنى التقليدي — فهي لا تشغل مساحة في التسلسل الهرمي ولا تشارك في أحداث التركيز أو اللمس. الغرض منها هو تبسيط صيانة التخطيطات المعقدة دون إضافة حاويات متداخلة.
نموذج تسجيل دخول بسيط مع حقل بريد إلكتروني وحقل كلمة مرور وزر. جميع العناصر مرتبطة بالأصل، باستثناء الزر — فهو أسفل حقل كلمة المرور. يتم استخدام تسلسل هرمي مسطح — جميع العناصر الثلاثة في نفس المستوى.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"
android:layout_marginHorizontal="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/password_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/email_input"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp" />
<Button
android:id="@+id/login_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/password_input"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"
android:layout_marginHorizontal="16dp"
android:text="تسجيل الدخول" />
</androidx.constraintlayout.widget.ConstraintLayout>
جميع العناصر بعرض 0dp (match_constraint)، مما يعني أنها تتمدد من قيد start إلى end مع مراعاة الهوامش الأفقية. هذا يعادل match_parent مع هوامش، ولكن بدون تداخل.
ثلاثة أزرار موزعة بالتساوي أفقياً مع هوامش حافة. تضع سلسلة spread_inside الأزرار الخارجية عند الحواف والزر الأوسط في المنتصف بينهما.
<Button
android:id="@+id/btn_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/btn_center"
android:text="يسار" />
<Button
android:id="@+id/btn_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@+id/btn_left"
app:layout_constraintRight_toLeftOf="@+id/btn_right"
android:text="وسط" />
<Button
android:id="@+id/btn_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@+id/btn_center"
app:layout_constraintRight_toRightOf="parent"
android:text="يمين" />
يتم إنشاء السلسلة تلقائياً عندما تحتوي العناصر على قيود ثنائية الاتجاه. يتم تعيين وضع spread_inside على أي عنصر من السلسلة عبر app:layout_constraintHorizontal_chainStyle="spread_inside". هذا يلغي الحاجة إلى LinearLayout مع weightSum و layout_weight.
إنشاء عمودين متساويين باستخدام Guideline عمودية بنسبة 50%. العنصر الأيسر مرتبط بالأصل الأيسر وحافته اليمنى بالخط الإرشادي؛ العنصر الأيمن مرتبط بحافته اليسرى بالخط الإرشادي وبالأصل الأيمن.
<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_midpoint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/left_card"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/gl_midpoint"
android:layout_margin="8dp"
android:background="@color/card_background" />
<TextView
android:id="@+id/right_card"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/gl_midpoint"
app:layout_constraintRight_toRightOf="parent"
android:layout_margin="8dp"
android:background="@color/card_background" />
Guideline بنسبة 0.5 تتكيف تلقائياً مع عرض الشاشة. على كل من الجهاز اللوحي والهاتف، تبقى نسبة الأعمدة 50/50. للتسمية يسار/يمين، استخدم سمات start/end للتوافق مع RTL.
جدول مقارنة لثلاث ViewGroups رئيسية لتطوير Android: ConstraintLayout و LinearLayout و RelativeLayout. المعايير: المرونة والأداء وتعقيد الكود وحالات الاستخدام.
| الخاصية | ConstraintLayout | LinearLayout | RelativeLayout |
|---|---|---|---|
| التداخل | مسطح (مستوى واحد) | يتطلب تداخلاً للتخطيطات المعقدة | مستوى واحد لكن مرونة محدودة |
| أداء measure | تمريرتان (~40% أسرع) | 4+ تمريرات مع weight | تمريرتان |
| أحجام مئوية | نعم (guide_percent, width_percent) | فقط عبر weight/frame | لا |
| دعم RTL | مدمج (start/end) | مدمج | عبر start/end (API 17+) |
| Barrier/Group/Flow | نعم (مساعدات افتراضية) | لا | لا |
| رسوم متحركة MotionLayout | نعم | لا | لا |
| متى يُستخدم | جميع التخطيطات المعقدة، شاشات >5 عناصر | قوائم أحادية الاتجاه بسيطة، صفوف بأزرار | تخطيطات نسبية بسيطة (كود قديم) |
وفقاً لـ Android Vitals (Google, 2025)، التطبيقات التي تستخدم ConstraintLayout كحاوية رئيسية تظهر في المتوسط 18% أقل من إطارات jank عند عرض شاشات معقدة مقارنة بالتطبيقات التي تستخدم LinearLayouts متداخلة. في IT Sectr، انتقلنا إلى ConstraintLayout كمعيار لجميع تخطيطات XML في 2018 — مما قلل متوسط عمق التسلسل الهرمي للشاشة من 4.2 إلى 1.8 مستوى وسرّع تطوير النماذج الجديدة بنسبة 25%.
الأسئلة الشائعة
match_parent في ConstraintLayout يعمل كالمعتاد — يمد العرض إلى حجم الأصل. 0dp (match_constraint) يعني أن حجم العرض يُحسب من القيود: إذا تم تعيين قيود left و right مع هوامش، العرض = parent — marginLeft — marginRight. الفرق في السلوك: match_parent يتجاهل الانحياز وقد يتجاوز الحدود أثناء الحركة؛ match_constraint يحترم جميع القيود بشكل صحيح وتوصي به Google كوضع رئيسي لـ ConstraintLayout.
استخدم مزيجاً من: الأحجام المئوية (layout_constraintWidth_percent) للعناصر التي يجب أن تشغل حصة من الشاشة؛ Guideline بنسب مئوية لتقسيم الشاشة إلى مناطق؛ Barrier لتحديد الموضع بالنسبة للمحتوى الديناميكي؛ Flow مع wrapMode لتفاف البطاقات إلى صف جديد. نهج بديل هو استخدام SlidingPaneLayout مع ConstraintLayout لواجهات master-detail على الأجهزة اللوحية.
Jetpack Compose لا يستخدم ConstraintLayout كـ ViewGroup، لكنه يوفر إصدار ConstraintLayout لـ compose (androidx.constraintlayout:constraintlayout-compose) بنفس API في Kotlin DSL: createRefFor() و constrainAs() و linkTo() و chain() و guideFrom(). هذا مفيد للتخطيطات المعقدة التي يسهل وصفها عبر القيود بدلاً من Column/Row. ومع ذلك، في Compose، يُوصى بالبدء بـ Column/Row/Box والانتقال إلى ConstraintLayout فقط عند الحاجة لتحديد مواضع نسبية معقدة.
في Android Studio، افتح Layout Inspector (Tools → Layout Inspector)، اختر التطبيق قيد التشغيل ومرر فوق العنصر المشكل. سترى جميع القيود والهوامش والحشو والانحياز في تمثيل ثلاثي الأبعاد. لـ XML، استخدم لوحة Design في محرر التخطيطات — يبرز تعارضات القيود باللون الأصفر والقيود المفقودة باللون الأحمر. في الكود، تأكد من أن كل عرض لديه قيدان متعامدان، وإلا سينتهي العنصر عند (0,0).
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.