واجهة أندرويد والأنشطة والتنقّل

التخطيطات: LinearLayout وConstraintLayout

18 دقيقة الدرس 1 من 12

التخطيطات: LinearLayout وConstraintLayout

في أندرويد، كل شاشة عبارة عن هرمية من كائنات View وViewGroup. الـView هو عنصر واجهة واحد — زر، أو ملصق نصي، أو صورة. أما الـViewGroup فهو حاوية تضع العناصر الأبناء وفق قواعد تخطيط معينة. يتناول هذا الدرس مدير التخطيط الذي ستلجأ إليه أكثر من غيره: LinearLayout للمداخل البسيطة، وConstraintLayout لكل شيء أكثر تعقيدًا.

كيف يُضخّم أندرويد التخطيط

تُعرَّف واجهة مستخدم أندرويد في الغالب بملفات XML مخزونة في res/layout/. في وقت التشغيل، تقوم Activity.setContentView(R.layout.activity_main) بتضخيم (inflate) ذلك XML إلى كائنات Java حقيقية. تقرأ عملية التضخيم كل عنصر XML، تُنشئ فئته المقابلة (مثل android.widget.TextView)، تطبّق سمات XML على شكل استدعاءات لطرق، ثم تربط الجميع في شجرة العروض.

لماذا XML وليس Java الصرف؟ يمنحك XML الخاص بالتخطيط فصلًا واضحًا بين بنية الواجهة والمنطق، ومعاينةً حية في Android Studio، وإمكانية استخدام مؤهلات الموارد (تخطيطات مختلفة للهاتف مقابل الجهاز اللوحي، الوضع الأفقي مقابل العمودي، الفاتح مقابل الداكن) دون المساس بكود Java.

LinearLayout — حاوية التكديس

يرتّب LinearLayout أبناءه في خط واحد — إما تكديس رأسي أو صف أفقي. يُقاس كل ابن بالتتابع ويوضع في نهاية السابق. إنه أبسط الحاويات والأنسب للنماذج وأشرطة الأدوات وأي واجهة تتدفق عناصرها في اتجاه واحد.

السمتان اللتان تضعهما على كل LinearLayout:

  • android:orientation="vertical" — يُرتّب الأبناء من الأعلى إلى الأسفل.
  • android:orientation="horizontal" — يُرتّب الأبناء من اليسار إلى اليمين (أو من اليمين إلى اليسار في اللغات RTL).

يجب على كل عرض (view) داخل LinearLayout أن يُعلن حجمه باستخدام android:layout_width وandroid:layout_height. القيمتان الخاصتان هما:

  • match_parent — يملأ المساحة المتبقية التي يقدّمها الأب.
  • wrap_content — يكون بالضبط بحجم ما يتطلّبه المحتوى.

مثال على LinearLayout رأسي بملصق وزر:

<!-- res/layout/activity_main.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:id="@+id/labelName" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Enter your name:" android:textAppearance="?attr/textAppearanceBodyLarge" /> <EditText android:id="@+id/inputName" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:hint="Full name" android:inputType="textPersonName" /> <Button android:id="@+id/btnSubmit" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Submit" /> </LinearLayout>

الوزن — توزيع المساحة المتبقية

أقوى خاصية في LinearLayout هي android:layout_weight. بعد قياس الأبناء ذوي الأحجام الثابتة، يُوزَّع أي مساحة متبقية على الأبناء الذين يُعلنون وزنًا، بنسبة قيم الأوزان. اضبط الحجم في اتجاه التخطيط على 0dp عند استخدام الوزن حتى يأخذ العرض حصته فقط من المساحة المتبقية.

<!-- زرّان يتقاسمان الصف بنسبة 1:2 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Cancel" /> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:text="Confirm" /> </LinearLayout>
تجنّب تداخل LinearLayouts بعمق كبير. كل مستوى تداخل يُضيف مسة قياس إضافية. ثلاثة أو أربعة مستويات تداخل تكون إشارة إلى أنك ينبغي التحوّل إلى ConstraintLayout الذي يحقق تموضعًا معقدًا بتسلسل هرمي مسطح من مستوى واحد.

ConstraintLayout — تموضع مرن ومسطّح

يُعدّ ConstraintLayout (من مكتبة androidx.constraintlayout) التخطيط الافتراضي الذي تولّده Android Studio. بدلًا من تداخل الحاويات، تربط قيودًا بكل عرض — قواعد تصل حافته أو مركزه بحواف عروض أخرى أو بحدود الحاوية الأم. تحلّ المحرك جميع القيود في آنٍ واحد وتضع كل عرض في مسحة واحدة.

أضف التبعية في build.gradle (Module):

dependencies { implementation "androidx.constraintlayout:constraintlayout:2.1.4" }

فهم القيود

القيد دائمًا اتصال بين نقطتَي ارتساء. لكل عرض ستة ارتساءات محتملة: العلوي والسفلي والبداية (اليسار) والنهاية (اليمين) وخطَّا أساس. خصائص القيود الشائعة:

  • app:layout_constraintTop_toTopOf="parent" — محاذاة الحافة العلوية لهذا العرض بالحافة العلوية للأب.
  • app:layout_constraintStart_toStartOf="parent" — محاذاة حافة البداية بحافة بداية الأب.
  • app:layout_constraintTop_toBottomOf="@id/labelName" — وضع هذا العرض مباشرةً أسفل عرض آخر.
  • app:layout_constraintEnd_toEndOf="parent" — تثبيت حافة النهاية بحافة نهاية الأب.
يجب أن يمتلك كل عرض على الأقل قيدًا أفقيًا واحدًا وقيدًا رأسيًا واحدًا. العرض الذي يمتلك قيدًا علويًا فقط دون قيد سفلي سيُترجَم دون أخطاء لكنه قد يقفز إلى الموضع 0,0 في وقت التشغيل — وهو خطأ شائع للمبتدئين.

نفس نموذج إدخال الاسم مُعاد بناؤه باستخدام ConstraintLayout:

<!-- res/layout/activity_main.xml --> <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" android:padding="16dp"> <TextView android:id="@+id/labelName" android:layout_width="0dp" android:layout_height="wrap_content" android:text="Enter your name:" android:textAppearance="?attr/textAppearanceBodyLarge" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <EditText android:id="@+id/inputName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:hint="Full name" android:inputType="textPersonName" app:layout_constraintTop_toBottomOf="@id/labelName" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <Button android:id="@+id/btnSubmit" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Submit" app:layout_constraintTop_toBottomOf="@id/inputName" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>

لاحظ أن layout_width="0dp" على TextView وEditText يعني "اشغل قيد الأفقي" — يمتد من constraintStart_toStartOf="parent" إلى constraintEnd_toEndOf="parent". هذا يحلّ محل الحاجة إلى match_parent داخل أبناء ConstraintLayout.

السلاسل — توزيع العروض على محور

السلسلة هي مجموعة من العروض مرتبطة بقيود ثنائية الاتجاه. لتكوين سلسلة أفقية، اربط نهاية العرض A ببداية العرض B، وبداية B بنهاية A. ثم اضبط app:layout_constraintHorizontal_chainStyle على أول عرض (رأس السلسلة) للتحكم في التباعد:

  • spread (الافتراضي) — يوزّع العروض والفجوات بالتساوي عبر المساحة المتاحة.
  • spread_inside — يدفع العروض نحو الحواف ويوزّع الفجوات بينها فقط.
  • packed — يجمع جميع العروض معًا؛ استخدم layout_constraintHorizontal_bias للإزاحة يسارًا أو يمينًا.

الإشارة إلى العروض في Java

بعد تضخيم التخطيط تُشير إلى العروض من خلال android:id الخاص بها في XML باستخدام findViewById():

// داخل Activity.onCreate()، بعد setContentView(...) Button btnSubmit = findViewById(R.id.btnSubmit); EditText inputName = findViewById(R.id.inputName); btnSubmit.setOnClickListener(view -> { String name = inputName.getText().toString().trim(); if (!name.isEmpty()) { // معالجة الإدخال } });

الاختيار بين الاثنين

  • استخدم LinearLayout للنماذج ذات اتجاه تدفق واحد، وأشرطة الأدوات البسيطة، والانقسامات الموزونة بين عرضين أو ثلاثة.
  • استخدم ConstraintLayout لأي شاشة تحتوي عروضًا متموضعة نسبةً لبعضها في بعدين، أو محاذاة معقدة، أو سلوكًا متجاوبًا عبر أحجام الشاشات.

في الدرس القادم ستملأ هذه التخطيطات بالنطاق الكامل من الأدوات المدمجة في أندرويد وستتعلم متى تستخدم كل واحدة منها.