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

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

אחרי שמטרגטים ל-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.

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

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

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

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

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

שוליים פנימיים של סרגלי המערכת

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

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

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

כדי להימנע מחפיפה חזותית כזו במצב תנועות או במצב לחצנים, אפשר להגדיל את השוליים של התצוגה באמצעות 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:

סרגל ניווט שקוף שלא מכסה את כפתור הפעולה הצף
איור 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, מבצעים פעולת או לוגית בין סרגלי המערכת לבין סוגי המגרעת במסך.

מגדירים את 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. שוליים פנימיים של תנועות מובנות במערכת.

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

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

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

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

בדוגמת הקוד הבאה אפשר לראות איך מטמיעים את המאפיין systemGestureInsets:

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

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

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

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

שליחת מודעות מוטמעות שתואמות לדורות קודמים

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

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

כדי לוודא שהשוליים הפנימיים נשלחים לרכיבים באותה רמה בכל גרסאות 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 לשיטה setProtections. לפני שמבצעים את הפעולה, חשוב לוודא את הפרטים הבאים:window.isNavigationBarContrastEnforced = false
  • אחרת, מגדירים את window.isNavigationBarContrastEnforced = true.

טיפים נוספים

טיפים נוספים לטיפול בתמונות ממוזערות.

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

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

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

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

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

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

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);
            }
        }
    }
    ...
}

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

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

בלוגים

עיצוב

מסמכי תיעוד אחרים

סרטונים