הצגת התוכן מקצה לקצה בתצוגות

איך עושים את זה ב-Compose
‫Jetpack Compose היא ערכת הכלים המומלצת לפיתוח ממשק משתמש ב-Android. איך עובדים עם פריסה מקצה לקצה ב-Compose.

אחרי שמטרגטים ל-SDK 35 ומעלה במכשיר עם Android 15 ומעלה, האפליקציה מוצגת מקצה לקצה. החלון מתפרס על כל הרוחב והגובה של המסך, והתוכן מוצג מאחורי סרגלי המידע. סרגלי המידע כוללים את שורת הסטטוס, שורת הכיתוב וסרגל הניווט.

בהרבה אפליקציות יש סרגל אפליקציה עליון. סרגל האפליקציה העליון צריך להגיע עד לקצה העליון של המסך ולהופיע מאחורי שורת הסטטוס. אפשר גם להגדיר שסרגל האפליקציה העליון יתכווץ לגובה של שורת הסטטוס כשגוללים בתוכן.

בהרבה אפליקציות יש גם סרגל אפליקציה תחתון או סרגל ניווט תחתון. הסרגלים האלה צריכים גם להגיע עד לקצה התחתון של המסך ולהופיע מאחורי סרגל הניווט. אחרת, האפליקציות צריכות להציג תוכן נגלל מאחורי סרגל הניווט.

איור 1. סרגלי מידע בפריסה מקצה לקצה.

כשמטמיעים פריסה מקצה לקצה באפליקציה, חשוב לזכור את הנקודות הבאות:

  1. הפעלת תצוגה מקצה לקצה
  2. להטמיע פריסות מותאמות כדי לשפר את חוויית המשתמש בגורמי צורה שונים
  3. טיפול בחפיפות חזותיות
  4. כדאי להציג שכבות מעומעמות מאחורי סרגלי המידע
דוגמה לתמונה מאחורי שורת הסטטוס
איור 2. דוגמה לתמונות מאחורי שורת הסטטוס.

הפעלת תצוגה מקצה לקצה

אם האפליקציה מטרגטת ל-SDK 35 ואילך, התצוגה מקצה לקצה מופעלת באופן אוטומטי במכשירים עם Android 15 ואילך.

כדי להפעיל את התצוגה מקצה לקצה בגרסאות קודמות של Android, צריך להפעיל באופן ידני את enableEdgeToEdge ב-onCreate של ה-Activity.

Kotlin

 override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         WindowCompat.enableEdgeToEdge(window)
        ...
      }

Java

 @Override
      protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WindowCompat.enableEdgeToEdge(getWindow());
        ...
      }

כברירת מחדל, enableEdgeToEdge() הופך את סרגלי המידע לשקופים, חוץ מבשיטת מעבר בין מסכים ב-3 כפתורים שבה סרגל הניווט מקבל מסך שקוף למחצה. הצבעים של סמלי המערכת והמסך המעומעם משתנים בהתאם לעיצוב הבהיר או הכהה של המערכת.

כדי להפעיל תצוגה מקצה לקצה באפליקציה בלי להשתמש בפונקציה enableEdgeToEdge(), עיינו במאמר הגדרה ידנית של תצוגה מקצה לקצה.

טיפול בחפיפות באמצעות הוספת מרווחים

יכול להיות שחלק מהתצוגות של האפליקציה יופיעו מאחורי סרגלי המידע, כמו שמוצג באיור 3.

כדי לטפל בחפיפות, אפשר להגיב לתוספות, שמציינות אילו חלקים במסך חופפים לממשק המשתמש של המערכת, כמו סרגל הניווט או שורת הסטטוס. החפיפה יכולה להיות הצגה מעל התוכן, אבל היא יכולה גם להודיע לאפליקציה על תנועות מובנות במערכת.

סוגי ה-inset שרלוונטיים להצגת האפליקציה מקצה לקצה הם:

  • מרווחים פנימיים של סרגלי המידע: הכי מתאימים לתצוגות שניתן ללחוץ עליהן ושסרגלי המידע לא צריכים להסתיר אותן.

  • הוספת מגרעת במסך: לאזורים שבהם יכולה להיות מגרעת במסך בגלל צורת המכשיר.

  • הוספות של תנועות מובנות במערכת: לאזורים של ניווט באמצעות תנועות שמשמשים את המערכת, והם בעדיפות גבוהה יותר מהאפליקציה.

הוספת סרגלי מידע

הסוג הנפוץ ביותר של הוספות הוא הוספות של סרגל המידע. הם מייצגים את האזור שבו ממשק המשתמש של המערכת מוצג בציר Z מעל האפליקציה. מומלץ להשתמש בהם כדי להזיז או להוסיף ריפוד לתצוגות באפליקציה שניתן ללחוץ עליהן, ושסרגלי המידע לא צריכים להסתיר אותן.

לדוגמה, כפתור הפעולה הצף (FAB) באיור 3 מוסתר חלקית על ידי סרגל הניווט:

דוגמה להטמעה מקצה לקצה, אבל סרגל הניווט מכסה את ה-FAB
איור 3. סרגל ניווט שחופף ל-FAB בפריסה מקצה לקצה.

כדי למנוע חפיפה חזותית כזו במצב תנועות או במצב לחצנים, אתם יכולים להגדיל את השוליים של התצוגה באמצעות getInsets(int) עם WindowInsetsCompat.Type.systemBars().

בדוגמת הקוד הבאה אפשר לראות איך מטמיעים את המרווחים הפנימיים של סרגל המידע:

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(fab) { v, windowInsets ->
  val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
  // Apply the insets as a margin to the view. This solution sets
  // only the bottom, left, and right dimensions, but you can apply whichever
  // insets are appropriate to your layout. You can also update the view padding
  // if that's more appropriate.
  v.updateLayoutParams<MarginLayoutParams> {
      leftMargin = insets.left
      bottomMargin = insets.bottom
      rightMargin = insets.right
  }

  // Return CONSUMED if you don't want the window insets to keep passing
  // down to descendant views.
  WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(fab, (v, windowInsets) -> {
  Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
  // Apply the insets as a margin to the view. This solution sets only the
  // bottom, left, and right dimensions, but you can apply whichever insets are
  // appropriate to your layout. You can also update the view padding if that's
  // more appropriate.
  MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
  mlp.leftMargin = insets.left;
  mlp.bottomMargin = insets.bottom;
  mlp.rightMargin = insets.right;
  v.setLayoutParams(mlp);

  // Return CONSUMED if you don't want the window insets to keep passing
  // down to descendant views.
    return WindowInsetsCompat.CONSUMED;
});

אם תיישמו את הפתרון הזה בדוגמה שמוצגת באיור 3, לא תהיה חפיפה חזותית במצב לחצן, כמו שמוצג באיור 4:

סרגל ניווט שקוף למחצה שלא מכסה את ה-FAB
איור 4. פתרון בעיה של חפיפה חזותית במצב כפתור.

אותו עיקרון חל על מצב ניווט באמצעות תנועות, כמו שמוצג באיור 5:

מקצה לקצה עם ניווט באמצעות תנועות
איור 5. פתרון בעיה של חפיפה חזותית במצב ניווט באמצעות תנועות.

הוספת מגרעת במסך

בחלק מהמכשירים יש מגרעות במסך. בדרך כלל, המגרעת נמצאת בחלק העליון של המסך ונכללת בשורת הסטטוס. כשהמסך של המכשיר בפריסה לרוחב, המגרעת יכולה להיות בקצה האנכי. בהתאם לתוכן שהאפליקציה מציגה במסך, כדאי להטמיע ריפוד כדי להימנע ממגרעות במסך, כי כברירת מחדל, האפליקציות יציירו במגרעת במסך.

לדוגמה, בהרבה מסכים באפליקציות מוצגת רשימה של פריטים. אל תסתירו פריטים ברשימה באמצעות המגרעת במסך או סרגלי המידע.

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { v, insets ->
  val bars = insets.getInsets(
    WindowInsetsCompat.Type.systemBars()
      or WindowInsetsCompat.Type.displayCutout()
  )
  v.updatePadding(
    left = bars.left,
    top = bars.top,
    right = bars.right,
    bottom = bars.bottom,
  )
  WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(mBinding.recyclerView, (v, insets) -> {
  Insets bars = insets.getInsets(
    WindowInsetsCompat.Type.systemBars()
    | WindowInsetsCompat.Type.displayCutout()
  );
  v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
  return WindowInsetsCompat.CONSUMED;
});

כדי לקבוע את הערך של WindowInsetsCompat, צריך להשתמש באופרטור הלוגי or של סרגלי המידע וסוגי המגרעות במסך.

מגדירים את clipToPadding ל-RecyclerView כדי שהריפוד ייגלל עם הפריטים ברשימה. כך הפריטים יכולים לעבור מאחורי סרגלי המידע כשהמשתמש גולל, כמו בדוגמה הבאה.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

הוספת תנועות מובנות במערכת

ההגדרות של תנועות מובנות במערכת מייצגות את האזורים בחלון שבהם התנועות המובנות במערכת מקבלות עדיפות על פני האפליקציה. האזורים האלה מוצגים בכתום באיור 6:

דוגמה לתוספות של תנועות מובנות במערכת
איור 6. תנועות מובנות במערכת.

כמו במרווחים של סרגל המידע, אפשר למנוע חפיפה של המרווחים של התנועות המובנות במערכת באמצעות getInsets(int) עם WindowInsetsCompat.Type.systemGestures().

אפשר להשתמש בתוספות האלה כדי להזיז או להרחיק מהקצוות תצוגות שאפשר להחליק. תרחישים נפוצים לדוגמה כוללים גיליונות תחתונים, החלקה במשחקים וקרוסלות שמוטמעות באמצעות ViewPager2.

ב-Android מגרסה 10 ואילך, אזורי inset של תנועות מובנות במערכת מכילים אזור inset תחתון לתנועת החזרה למסך הבית, ואזור inset שמאלי וימני לתנועות החזרה:

דוגמה למידות של תנועות מובנות במערכת
איור 7. מידות של תנועות מובנות במערכת.

בדוגמת הקוד הבאה אפשר לראות איך מטמיעים הוספת מרווחים לתנועות מובנות במערכת:

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets ->
    val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
    // Apply the insets as padding to the view. Here, set all the dimensions
    // as appropriate to your layout. You can also update the view's margin if
    // more appropriate.
    view.updatePadding(insets.left, insets.top, insets.right, insets.bottom)

    // Return CONSUMED if you don't want the window insets to keep passing down
    // to descendant views.
    WindowInsetsCompat.CONSUMED
}

Java

ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> {
    Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures());
    // Apply the insets as padding to the view. Here, set all the dimensions
    // as appropriate to your layout. You can also update the view's margin if
    // more appropriate.
    view.setPadding(insets.left, insets.top, insets.right, insets.bottom);

    // Return CONSUMED if you don't want the window insets to keep passing down
    // to descendant views.
    return WindowInsetsCompat.CONSUMED;
});

רכיבי חומרים

הרבה רכיבי Material Design ל-Android (com.google.android.material) שמבוססים על תצוגות מטפלים באופן אוטומטי במרווחים הפנימיים, כולל BottomAppBar,‏ BottomNavigationView,‏ NavigationRailView ו-NavigationView

עם זאת, AppBarLayout לא מטפל באופן אוטומטי ב-inset. מוסיפים android:fitsSystemWindows="true" כדי לטפל בתוספות העליונות.

איך מטפלים בתוספות באמצעות רכיבי Material ב-Compose

שליחת הוספה שתואמת לדורות קודמים

כדי להפסיק את שליחת הריפודים לתצוגות הצאצאים ולמנוע ריפוד יתר, אפשר לצרוך ריפודים באמצעות הקבוע WindowInsetsCompat.CONSUMED. עם זאת, במכשירים עם Android בגרסה 10 (רמת API‏ 29 ומטה), ה-insets לא נשלחים לאלמנטים אחים אחרי הקריאה ל-WindowInsetsCompat.CONSUMED, מה שעלול לגרום לחפיפה חזותית לא מכוונת.

דוגמה לשליחה לא תקינה של הוספה
איור 8. דוגמה לשליחת הוספה פגומה. ה-insets לא נשלחים לתצוגות אחיות אחרי ש-ViewGroup 1 צורך insets ב-Android בגרסה 10 (רמת API‏ 29) ומטה, וכתוצאה מכך TextView 2 חופף לסרגל הניווט של המערכת. עם זאת, ב-Android מגרסה 11 (רמת API ‏30) ואילך, ה-insets נשלחים לתצוגות אחיות, כמו שצריך.

כדי לוודא שההוספות נשלחות לאחים בכל גרסאות Android הנתמכות, צריך להשתמש ב-ViewGroupCompat#installCompatInsetsDispatch לפני שמשתמשים בהוספות, שזמינות ב-AndroidX Core וב-Core-ktx 1.16.0-alpha01 ואילך.

Kotlin

// Use the i.d. assigned to your layout's root view, e.g. R.id.main
val rootView = findViewById(R.id.main)
// Call before consuming insets
ViewGroupCompat.installCompatInsetsDispatch(rootView)

Java

// Use the i.d. assigned to your layout's root view, e.g. R.id.main
LinearLayout rootView = findViewById(R.id.main);
// Call before consuming insets
ViewGroupCompat.installCompatInsetsDispatch(rootView);
דוגמה לשליחת אינסטים קבועים
איור 9. תוקנה בעיה בשליחת ה-inset אחרי קריאה ל-ViewGroupCompat#installCompatInsetsDispatch.

מצב עשיר

יש תכנים שמומלץ לצפות בהם במסך מלא, כדי שהמשתמשים ייהנו מחוויה עשירה יותר. אתם יכולים להסתיר את סרגלי המידע כדי לעבור למצב אימרסיבי באמצעות הספריות WindowInsetsController ו-WindowInsetsControllerCompat:

Kotlin

val windowInsetsController =
      WindowCompat.getInsetsController(window, window.decorView)

// Hide the system bars.
windowInsetsController.hide(Type.systemBars())

// Show the system bars.
windowInsetsController.show(Type.systemBars())

Java

Window window = getWindow();
WindowInsetsControllerCompat windowInsetsController =
      WindowCompat.getInsetsController(window, window.getDecorView());
if (windowInsetsController == null) {
    return;
  }
// Hide the system bars.
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());

// Show the system bars.
windowInsetsController.show(WindowInsetsCompat.Type.systemBars());

למידע נוסף על הטמעת התכונה הזו, אפשר לעיין במאמר הסתרת סרגלי המידע במצב אימרסיבי.

סמלים בסרגל המידע

הפעלת enableEdgeToEdge מבטיחה שצבעי הסמלים בסרגל המידע יתעדכנו כשעיצוב המכשיר משתנה.

במצב מקצה לקצה, יכול להיות שתצטרכו לעדכן באופן ידני את צבעי הסמלים בסרגל המידע כדי שתהיה ניגודיות בינם לבין הרקע של האפליקציה. לדוגמה, כדי ליצור סמלים בהירים בשורת הסטטוס:

Kotlin

WindowCompat.getInsetsController(window, window.decorView)
    .isAppearanceLightStatusBars = false

Java

WindowCompat.getInsetsController(window, window.getDecorView())
    .setAppearanceLightStatusBars(false);

הגנה על סרגל המידע

אחרי שהאפליקציה מטרגטת ל-SDK 35 ומעלה, התצוגה מקצה לקצה נאכפת. שורת הסטטוס של המערכת וסרגלי הניווט באמצעות תנועות שקופים, אבל סרגל הניווט עם שלושת הכפתורים שקוף למחצה. כדי להגדיר את האפשרות כתואמת לדור הקודם, צריך להפעיל את enableEdgeToEdge.

עם זאת, יכול להיות שברירות המחדל של המערכת לא יתאימו לכל תרחישי השימוש. כדי להחליט אם להשתמש בסרגלי מידע שקופים או שקופים למחצה, כדאי לעיין בהנחיות לעיצוב סרגלי מידע ב-Android ובעיצוב מקצה לקצה.

יצירת סרגלי מידע שקופים

כדי ליצור שורת סטטוס שקופה, צריך לטרגט ל-Android 15‏ (SDK 35) ומעלה, או להפעיל את enableEdgeToEdge() עם ארגומנטים שמוגדרים כברירת מחדל בגרסאות קודמות.

כדי ליצור סרגל ניווט שקוף באמצעות תנועות, צריך לטרגט ל-Android מגרסה 15 ואילך או להפעיל את enableEdgeToEdge() עם ארגומנטים שמוגדרים כברירת מחדל בגרסאות קודמות. בסרגל ניווט עם שלושה כפתורים, מגדירים את Window.setNavigationBarContrastEnforced לערך false, אחרת תוחל שכבת הגנה שקופה למחצה.

יצירת סרגלי מידע שקופים למחצה

כדי ליצור שורת סטטוס שקופה למחצה, מבצעים את הפעולות הבאות:

  1. צריך לעדכן את התלות androidx-core לגרסה 1.16.0-beta01 ומעלה
  2. עוטפים את פריסת ה-XML ב-androidx.core.view.insets.ProtectionLayout ומקצים מזהה.
  3. אפשר לגשת באופן פרוגרמטי אל ProtectionLayout כדי להגדיר הגנות, ולציין את הצד ואת GradientProtection לשורת הסטטוס.

<androidx.core.view.insets.ProtectionLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list_protection"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:id="@+id/item_list"
        android:clipToPadding="false"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <!--items-->

    </ScrollView>

</androidx.core.view.insets.ProtectionLayout>

findViewById<ProtectionLayout>(R.id.list_protection)
    .setProtections(
        listOf(
            GradientProtection(
                WindowInsetsCompat.Side.TOP,
                // Ideally, this is the pane's background color
                paneBackgroundColor
            )
        )
    )

חשוב לוודא שהערך של ColorInt שמועבר אל GradientProtection תואם לרקע של התוכן. לדוגמה, בפריסה של רשימה ופרטים שמוצגת במכשיר מתקפל, יכולים להיות ערכי GradientProtections שונים בצבעים שונים לחלונית הרשימה ולחלונית הפרטים.

תרשים 1. הגנה הדרגתית בצבעים שונים.

אל תיצרו סרגל ניווט שקוף למחצה לתנועות. כדי ליצור סרגל ניווט שקוף למחצה עם שלושה כפתורים, צריך לבצע אחת מהפעולות הבאות:

  • אם הפריסה כבר מוקפת ב-ProtectionView, אפשר להעביר ColorProtection או GradientProtection נוספים ל-method‏ setProtections. לפני שאתם עושים את זה, ודאו שwindow.isNavigationBarContrastEnforced = false.
  • אחרת, מגדירים את window.isNavigationBarContrastEnforced = true.

טיפים נוספים

טיפים נוספים לטיפול בתוספות.

הגדרת תוכן בגלילה מקצה לקצה

מוודאים שהפריט האחרון ברשימה לא מוסתר על ידי סרגלי המידע ב-RecyclerView או ב-NestedScrollView, על ידי טיפול בתוספות והגדרת clipToPadding ל-false.

בסרטון הבא מוצג מכשיר RecyclerView כשהתצוגה מקצה לקצה מושבתת (מימין) ומופעלת (משמאל):

בקטע יצירת רשימות דינמיות באמצעות RecyclerView יש דוגמת קוד.

הצגת תיבות דו-שיח במסך מלא מקצה לקצה

כדי להציג תיבת דו-שיח במסך מלא מקצה לקצה, צריך להפעיל את enableEdgeToEdge בתיבת הדו-שיח.

Kotlin

class MyAlertDialogFragment : DialogFragment() {
    override fun onStart(){
        super.onStart()
        dialog?.window?.let { WindowCompat.enableEdgeToEdge(it) }
    }
    ...
}

Java

public class MyAlertDialogFragment extends DialogFragment {
    @Override
    public void onStart() {
        super.onStart();
        Dialog dialog = getDialog();
        if (dialog != null) {
            Window window = dialog.getWindow();
            if (window != null) {
                WindowCompat.enableEdgeToEdge(window);
            }
        }
    }
    ...
}

מקורות מידע נוספים

למידע נוסף על פריסה מקצה לקצה, אפשר לעיין במקורות המידע הבאים.

בלוגים

עיצוב

מסמכים אחרים

סרטונים