القوائم وأشرطة الأدوات وتصميم Material
التطبيق المصقول لا يكتفي بعرض البيانات، بل يُقدّم إجراءاته بوضوح ويلتزم بلغة التصميم البصري التي يعرفها المستخدمون. في هذا الدرس ستتقن شريط التطبيق (الشريط الأفقي في أعلى كل شاشة)، وتحميل موارد القوائم فيه، والاستجابة لتحديدات العناصر، وتطبيق مبادئ Material Design الأساسية لكي تبدو كل شاشة متعمَّدة واحترافية.
شريط التطبيق: Toolbar مقابل ActionBar
في الإصدارات الأولى من Android كان هناك ActionBar مدمج في أعلى كل Activity. بديله الحديث هو androidx.appcompat.widget.Toolbar — وهو عنصر عادي تضعه في تخطيطك. يهمّ هذا الأمر لأنك تستطيع تحديد موقعه وتنسيقه وتحريكه بحرية، وتضمينه داخل CoordinatorLayout للتفاعل مع التمرير، وإبقاء السيطرة الكاملة على ارتفاعه ولونه دون تعقيدات السمات.
الإعداد المعياري يستخدم سمة NoActionBar لإزالة الشريط النظامي، ثم تُرقّي Toolbar الخاص بك ليكون شريط الإجراءات في النشاط:
<!-- res/values/themes.xml -->
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
</style>
<!-- res/layout/activity_main.xml -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp" />
</com.google.android.material.appbar.AppBarLayout>
<!-- المحتوى الرئيسي أسفل الشريط -->
</androidx.coordinatorlayout.widget.CoordinatorLayout>
في Activity، رقّ الشريط بعد استدعاء setContentView:
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); // يستبدل ActionBar النظامي
getSupportActionBar().setTitle("My App"); // تعيين العنوان المعروض
}
}
لماذا setSupportActionBar؟ يربط Toolbar الخاص بك بنظام قوائم AppCompat، لذا تعمل دوال الاستجابة onCreateOptionsMenu وonOptionsItemSelected بشكل متطابق عبر جميع مستويات API. بدون هذا الاستدعاء لن تُطلق عناصر الشريط تلك الدوال أبدًا.
تعريف مورد القائمة
تُعلن القوائم في XML ضمن المجلد res/menu/. كل <item> له id وعنوان title (يظهر في قائمة الفائض) وأيقونة icon اختيارية. تتحكّم السمة app:showAsAction في مكان العنصر:
always — يظهر دائمًا كأيقونة في الشريط.
ifRoom — يظهر أيقونةً إذا سمحت المساحة الأفقية، وإلا ينهار في قائمة الفائض.
never — دائمًا في قائمة الفائض (قائمة النقاط الثلاث).
<!-- res/menu/menu_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="Search"
android:icon="@drawable/ic_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_settings"
android:title="Settings"
app:showAsAction="never" />
<item
android:id="@+id/action_about"
android:title="About"
app:showAsAction="never" />
</menu>
تحميل عناصر القائمة والاستجابة لها
تجاوز onCreateOptionsMenu لتحميل المورد، وonOptionsItemSelected للاستجابة للنقرات. أعد true من الأخير عند معالجة عنصر ما لإيقاف Android عن نشر الحدث.
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true; // إعادة true تُظهر القائمة
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_search) {
// فتح واجهة البحث
Toast.makeText(this, "Search", Toast.LENGTH_SHORT).show();
return true;
} else if (id == R.id.action_settings) {
startActivity(new Intent(this, SettingsActivity.class));
return true;
} else if (id == R.id.action_about) {
showAboutDialog();
return true;
}
return super.onOptionsItemSelected(item); // تفويض العناصر غير المُعالجة
}
استخدم item.getItemId() بدلًا من جملة switch مع معرّفات الموارد. منذ Android Gradle Plugin 8.x لم تعد معرّفات الموارد ثوابت وقت الترجمة، لذا يُنتج switch عليها تحذيرًا أو خطأ في المترجم. استخدم سلاسل if / else if بدلًا من ذلك.
وضع الإجراء السياقي
عندما يضغط المستخدم لفترة طويلة على عنصر في قائمة فإنك غالبًا تريد شريط الإجراءات السياقي (CAB) — شريط مؤقت يحلّ محل شريط الأدوات ويُظهر إجراءات خاصة بالتحديد. نفّذ الواجهة ActionMode.Callback:
private ActionMode actionMode;
private final ActionMode.Callback actionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.menu_context, menu);
mode.setTitle("1 selected");
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // أعد true فقط إذا تم تحديث القائمة
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == R.id.action_delete) {
deleteSelectedItem();
mode.finish(); // يرفض CAB
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
actionMode = null;
clearSelection();
}
};
// التفعيل عند الضغط الطويل:
someView.setOnLongClickListener(v -> {
actionMode = startSupportActionMode(actionModeCallback);
return true;
});
أساسيات Material Design
Material Design هو نظام تصميم Google. يمنح تطبيقات Android (والويب) قواعد بصرية متسقة. المفاهيم الأساسية التي يحتاجها المطوّر:
- الارتفاع والظلال — كل سطح يعيش على مستوى z معين؛ السطوح الأعلى تُلقي ظلالًا أكبر. عيّنه باستخدام
android:elevation.
- نظام الألوان —
colorPrimary وcolorSecondary وcolorSurface وcolorOnPrimary (النصوص/الأيقونات على اللون الأساسي) وغيرها. عرّفها مرة واحدة في سمتك وارجع إليها عبر ?attr/ لكي يستخدم كل عنصر الألوان المتسقة تلقائيًا.
- تدرّج الخطوط — عنوان رئيسي وعنوان ونص وعنوان فرعي. استخدم أنماط
TextAppearance.MaterialComponents.* بدلًا من أحجام الخطوط الخام.
- تأثير الموجة (Ripple) —
MaterialButton وMaterialCardView وسائر عناصر Material تتضمّن الموجة افتراضيًا؛ تجنّب استخدام Button أو View العادية حيث يوجد بديل Material.
استخدام MaterialButton وFloatingActionButton
استبدل Button العادي بـ MaterialButton للحصول على الحشو الصحيح ونصف قطر الزاوية والموجة واللون من سمتك تلقائيًا:
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Save"
style="@style/Widget.MaterialComponents.Button" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_add"
android:contentDescription="Add item" />
ربط زر الإجراء العائم في Java:
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(v -> openCreateScreen());
Snackbar: الطريقة Material لعرض التغذية الراجعة
Toast يعمل لكنه لا يمكن رفضه ولا يستطيع حمل إجراء. Snackbar هو البديل المادي. يحتاج إلى عرض لربطه به (استخدم CoordinatorLayout الجذر لكي ينزلق تلقائيًا فوق زر الإجراء العائم):
import com.google.android.material.snackbar.Snackbar;
View rootView = findViewById(R.id.coordinator_root);
Snackbar.make(rootView, "Item deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO", v -> undoDelete())
.show();
اربط Snackbar بـ CoordinatorLayout وليس بعرض داخلي. عند الربط بـ CoordinatorLayout، يرفع الإطار زر الإجراء العائم تلقائيًا حتى لا يغطيه Snackbar. الربط بأي حاوية أخرى يكسر هذه التوافقية التلقائية ويظهر Snackbar فوق الزر العائم.
درج التنقّل مع Material Design
للتطبيقات التي تحتوي على عدة وجهات رئيسية، درج التنقّل (Navigation Drawer) هو النمط المعياري. التف تخطيطك في DrawerLayout واستخدم NavigationView من مكتبة Material:
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- المحتوى الرئيسي + شريط الأدوات هنا -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/menu_drawer"
app:headerLayout="@layout/nav_header" />
</androidx.drawerlayout.widget.DrawerLayout>
في Java، قم بتبديل الدرج من أيقونة الهامبرغر في شريط الأدوات وعالج تحديدات التنقل:
DrawerLayout drawer = findViewById(R.id.drawerLayout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar,
R.string.nav_open, R.string.nav_close);
drawer.addDrawerListener(toggle);
toggle.syncState(); // يزامن أيقونة الهامبرغر/السهم مع حالة الدرج
NavigationView navView = findViewById(R.id.navView);
navView.setNavigationItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
// تحميل جزء الصفحة الرئيسية
} else if (id == R.id.nav_profile) {
// تحميل جزء الملف الشخصي
}
drawer.closeDrawer(GravityCompat.START);
return true;
});
الخلاصة
لقد بنيت شريط تطبيق كاملًا باستخدام Toolbar وsetSupportActionBar، وأعلنت عناصر القائمة في الشريط وفي قائمة الفائض وعالجتها، ونفّذت شريط الإجراءات السياقي للضغط الطويل، وطبّقت Material Design من خلال السمات المتسقة وMaterialButton وFloatingActionButton وSnackbar ودرج التنقّل. في الدرس الأخير ستجمع كل شيء في مشروع قائمة-تفاصيل كامل.