Wyświetlanie treści od krawędzi do krawędzi

Wypróbuj Compose
Jetpack Compose to zalecany zestaw narzędzi do tworzenia interfejsu na Androidzie. Dowiedz się, jak pracować z wyświetlaniem bez ramki w Compose.

Gdy kierujesz aplikację na pakiet SDK 35 lub nowszy na urządzeniu z Androidem 15 lub nowszym, jest ona wyświetlana bez ramki. Okno zajmuje całą szerokość i wysokość wyświetlacza, ponieważ jest rysowane za paskami systemu. Paski systemu to pasek stanu, pasek podpisów i pasek nawigacyjny.

Wiele aplikacji ma górny pasek aplikacji. Górny pasek aplikacji powinien rozciągać się do górnej krawędzi ekranu i wyświetlać się za paskiem stanu. Opcjonalnie górny pasek aplikacji może się zmniejszać do wysokości paska stanu, gdy przewijana jest treść.

Wiele aplikacji ma też dolny pasek aplikacji lub dolny pasek nawigacyjny. Te paski powinny również rozciągać się do dolnej krawędzi ekranu i wyświetlać się za paskiem nawigacyjnym. W przeciwnym razie aplikacje powinny wyświetlać przewijaną treść za paskiem nawigacyjnym.

Rysunek 1. Paski systemu w układzie bez ramki.

Podczas implementowania w aplikacji układu bez ramki pamiętaj o tych kwestiach:

  1. Włącz wyświetlanie bez ramki.
  2. Zaimplementuj układy adaptacyjne, aby zoptymalizować wrażenia użytkowników na różnych urządzeniach.
  3. Obsłuż nakładanie się elementów wizualnych.
  4. Rozważ wyświetlanie półprzezroczystych nakładek za paskami systemu.
przykład obrazu za paskiem stanu,
Rysunek 2. Przykład obrazu za paskiem stanu.

Włącz wyświetlanie bez ramki

Jeśli aplikacja jest kierowana na pakiet SDK 35 lub nowszy, wyświetlanie bez ramki jest automatycznie włączane na urządzeniach z Androidem 15 lub nowszym.

Aby włączyć wyświetlanie bez ramki w poprzednich wersjach Androida, ręcznie wywołaj enableEdgeToEdge w onCreate klasy 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());
        ...
      }

Domyślnie funkcja enableEdgeToEdge() sprawia, że paski systemu są przezroczyste, z wyjątkiem trybu nawigacji przy użyciu 3 przycisków, w którym pasek nawigacyjny ma półprzezroczystą nakładkę. Kolory ikon systemowych i nakładki są dostosowywane na podstawie jasnego lub ciemnego motywu systemu.

Aby włączyć wyświetlanie bez ramki w aplikacji bez użycia funkcji enableEdgeToEdge(), przeczytaj artykuł Ręczne konfigurowanie wyświetlania bez ramki.

Obsługa nakładania się elementów za pomocą odcięć

Niektóre widoki aplikacji mogą być rysowane za paskami systemu, jak pokazano na rysunku 3.

Nakładanie się elementów możesz rozwiązać, reagując na odcięcia, które określają, które części ekranu przecinają się z interfejsem systemu, np. z paskiem nawigacyjnym lub paskiem stanu. Przecinanie się może oznaczać wyświetlanie nad treścią, ale może też informować aplikację o gestach systemowych.

Typy odcięć, które mają zastosowanie do wyświetlania aplikacji bez ramki:

  • Odcięcia pasków systemu: najlepsze w przypadku widoków, które można kliknąć i które nie mogą być wizualnie zasłonięte przez paski systemu.

  • Odcięcia wycięcia w ekranie: w przypadku obszarów, w których może występować wycięcie w ekranie ze względu na kształt urządzenia.

  • Odcięcia gestów systemowych: w przypadku obszarów nawigacji gestami używanych przez system, które mają priorytet przed aplikacją.

Odcięcia pasków systemu

Odcięcia pasków systemu to najczęściej używany typ odcięć. Reprezentują one obszar, w którym interfejs systemu wyświetla się na osi Z nad aplikacją. Najlepiej używać ich do przenoszenia lub dodawania dopełnienia do widoków w aplikacji, które można kliknąć i które nie mogą być wizualnie zasłonięte przez paski systemu.

Na przykład pływający przycisk czynności (FAB) na rysunku 3 jest częściowo zasłonięty przez pasek nawigacyjny:

przykład implementacji od krawędzi do krawędzi, ale pasek nawigacyjny zasłania przycisk FAB;
Rysunek 3. Pasek nawigacyjny nakładający się na pływający przycisk czynności w układzie bez ramki.

Aby uniknąć tego rodzaju nakładania się elementów wizualnych w trybie gestów lub trybie przycisków, możesz zwiększyć marginesy widoku za pomocą getInsets(int) z parametrem WindowInsetsCompat.Type.systemBars().

Poniższy przykład kodu pokazuje, jak zaimplementować odcięcia pasków systemu:

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

Jeśli zastosujesz to rozwiązanie do przykładu pokazanego na rysunku 3, w trybie przycisków nie będzie nakładania się elementów wizualnych, jak pokazano na rysunku 4:

przezroczysty pasek nawigacyjny, który nie zasłania przycisku FAB;
Rysunek 4. Rozwiązywanie problemu z nakładaniem się elementów wizualnych w trybie przycisków.

To samo dotyczy trybu nawigacji przy użyciu gestów, jak pokazano na rysunku 5:

od krawędzi do krawędzi z nawigacją przy użyciu gestów,
Rysunek 5. Rozwiązywanie problemu z nakładaniem się elementów wizualnych w trybie nawigacji gestami.

Odcięcia wycięcia w ekranie

Niektóre urządzenia mają wycięcia w ekranie. Zwykle wycięcie znajduje się u góry ekranu i jest uwzględniane na pasku stanu. Gdy ekran urządzenia jest w trybie poziomym, wycięcie może znajdować się na krawędzi pionowej. W zależności od treści wyświetlanej przez aplikację na ekranie należy zaimplementować dopełnienie, aby uniknąć wycięć w ekranie, ponieważ domyślnie aplikacje będą rysować w wycięciu w ekranie.

Na przykład na wielu ekranach aplikacji wyświetla się lista elementów. Nie zasłaniaj elementów listy wycięciem w ekranie ani paskami systemu.

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

Określ wartość WindowInsetsCompat, wykonując logiczną operację lub na paskach systemu i typach wycięć w ekranie.

Ustaw wartość clipToPadding na RecyclerView, aby dopełnienie przewijało się z elementami listy. Dzięki temu elementy mogą znajdować się za paskami systemu, gdy użytkownik przewija, jak pokazano w tym przykładzie.

<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" />

Odcięcia gestów systemowych

Odcięcia gestów systemowych reprezentują obszary okna, w których gesty systemowe mają priorytet przed aplikacją. Obszary te są oznaczone na rysunku 6 kolorem pomarańczowym:

Przykład wstawień gestów systemowych
Rysunek 6. Odcięcia gestów systemowych.

Podobnie jak w przypadku odcięć pasków systemu, możesz uniknąć nakładania się odcięć gestów systemowych za pomocą getInsets(int) z parametrem WindowInsetsCompat.Type.systemGestures().

Użyj tych odcięć, aby przenosić lub dodawać dopełnienie do widoków, które można przesuwać, z dala od krawędzi. Typowe przypadki użycia to arkusze dolne, przesuwanie w grach i karuzele zaimplementowane za pomocąViewPager2.

W Androidzie 10 lub nowszym odcięcia gestów systemowych zawierają dolne odcięcie dla gestu powrotu do ekranu głównego oraz lewe i prawe odcięcie dla gestów wstecznych:

przykład pomiarów wcięcia gestu systemowego
Rysunek 7. Wymiary odcięć gestów systemowych.

Poniższy przykład kodu pokazuje, jak zaimplementować odcięcia gestów systemowych:

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

Komponenty Material

Wiele komponentów Material na Androida opartych na widokach Android Material Components (com.google.android.material) automatycznie obsługuje odcięcia, w tym BottomAppBar, BottomNavigationView, NavigationRailView i NavigationView

Jednak AppBarLayout nie obsługuje automatycznie odcięć. Aby obsługiwać górne odcięcia, dodaj android:fitsSystemWindows="true".

Dowiedz się, jak obsługiwać odcięcia za pomocą komponentów Material w Compose.

Wysyłanie odcięć zgodne wstecz

Aby zatrzymać wysyłanie odcięć do widoków podrzędnych i uniknąć nadmiernego dopełnienia, możesz użyć stałej WindowInsetsCompat.CONSUMED. Jednak na urządzeniach z Androidem 10 (API na poziomie 29 i starszym) odcięcia nie są wysyłane do elementów równorzędnych po wywołaniu WindowInsetsCompat.CONSUMED, co może powodować niezamierzone nakładanie się elementów wizualnych.

Przykład nieprawidłowego wysyłania wstawki
Rysunek 8. Przykład nieprawidłowego wysyłania odcięć. Odcięcia nie są wysyłane do widoków równorzędnych po tym, jak ViewGroup 1 użyje odcięć na Androidzie 10 (API na poziomie 29) i starszym, co powoduje, że TextView 2 nakłada się na pasek nawigacyjny systemu. Jednak odcięcia są wysyłane do widoków równorzędnych na Androidzie 11 (API na poziomie 30) i nowszym zgodnie z oczekiwaniami.

Aby potwierdzić, że odcięcia są wysyłane do elementów równorzędnych we wszystkich obsługiwanych wersjach Androida , przed użyciem odcięć użyj metody ViewGroupCompat#installCompatInsetsDispatch, która jest dostępna w AndroidX Core i Core-ktx 1.16.0-alpha01 i nowszych.

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);
Przykład wysyłania z ustalonym wcięciem
Rysunek 9. Prawidłowe wysyłanie odcięć po wywołaniu metody ViewGroupCompat#installCompatInsetsDispatch.

Tryb pojemny

Niektóre treści najlepiej wyświetlać na pełnym ekranie, aby zapewnić użytkownikowi bardziej wciągające wrażenia. Aby ukryć paski systemu w trybie pojemnym, możesz użyć WindowInsetsController i WindowInsetsControllerCompat bibliotek:

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());

Więcej informacji o implementowaniu tej funkcji znajdziesz w artykule Ukrywanie pasków systemu w trybie pojemnym.

Ikony pasków systemu

Wywołanie funkcji enableEdgeToEdge zapewnia, że kolory ikon pasków systemu będą się aktualizować, gdy zmieni się motyw urządzenia.

Podczas wyświetlania bez ramki może być konieczne ręczne zaktualizowanie kolorów ikon pasków systemu, aby kontrastowały z tłem aplikacji. Aby na przykład utworzyć jasne ikony paska stanu:

Kotlin

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

Java

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

Ochrona pasków systemu

Gdy aplikacja jest kierowana na pakiet SDK 35 lub nowszy, wyświetlanie bez ramki jest wymuszane. Pasek stanu systemu i paski nawigacji przy użyciu gestów są przezroczyste, ale pasek nawigacji z 3 przyciskami jest półprzezroczysty. Aby zapewnić zgodność wsteczną, wywołaj funkcję enableEdgeToEdge.

Jednak ustawienia domyślne systemu mogą nie działać we wszystkich przypadkach użycia. Aby określić, czy używać przezroczystych czy półprzezroczystych pasków systemu, zapoznaj się ze wskazówkami dotyczącymi projektowania pasków systemu na Androidzie i projektowania bez ramki.

Tworzenie przezroczystych pasków systemu

Aby utworzyć przezroczysty pasek stanu, kieruj aplikację na Androida 15 (SDK 35) lub nowszego albo wywołaj funkcję enableEdgeToEdge() z argumentami domyślnymi w przypadku starszych wersji.

Aby utworzyć przezroczysty pasek nawigacji przy użyciu gestów, kieruj aplikację na Androida 15 lub nowszego albo wywołaj funkcję enableEdgeToEdge() z argumentami domyślnymi w przypadku starszych wersji. W przypadku paska nawigacji z 3 przyciskami ustaw wartość Window.setNavigationBarContrastEnforced na false, w przeciwnym razie zostanie zastosowana półprzezroczysta nakładka.

Tworzenie półprzezroczystych pasków systemu

Aby utworzyć półprzezroczysty pasek stanu:

  1. Zaktualizuj zależność androidx-core do wersji 1.16.0-beta01 lub nowszej.
  2. Owiń układ XML w element androidx.core.view.insets.ProtectionLayout i przypisz mu identyfikator.
  3. Programowo uzyskaj dostęp do elementu ProtectionLayout, aby ustawić ochronę, określając stronę i element GradientProtection dla paska stanu.

<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
            )
        )
    )

Upewnij się, że wartość ColorInt przekazana do elementu GradientProtection jest zgodna z tłem treści. Na przykład układ szczegółowej listy wyświetlany na urządzeniu składanym może mieć różne GradientProtections o różnych kolorach dla panelu listy i panelu szczegółów.

Rysunek 1. Ochrona gradientem o różnych kolorach.

Nie twórz półprzezroczystego paska nawigacji przy użyciu gestów. Aby utworzyć półprzezroczysty pasek nawigacji z 3 przyciskami, wykonaj jedną z tych czynności:

  • Jeśli układ jest już owinięty w element ProtectionView, możesz przekazać dodatkowy element ColorProtection lub GradientProtection do metody setProtections. Zanim to zrobisz, upewnij się, że window.isNavigationBarContrastEnforced = false.
  • W przeciwnym razie ustaw wartość window.isNavigationBarContrastEnforced = true.

Inne wskazówki

Dodatkowe wskazówki dotyczące obsługi odcięć.

Wyświetlanie przewijanej treści bez ramki

Sprawdź, czy ostatni element listy nie jest zasłonięty przez paski systemu w elemencie RecyclerView lub NestedScrollView, obsługując odcięcia i ustawiając wartość clipToPadding na false.

Film poniżej przedstawia element RecyclerView z wyłączonym (po lewej) i włączonym (po prawej) wyświetlaniem bez ramki:

Przykładowy kod znajdziesz we fragmentach kodu w sekcji Tworzenie list dynamicznych za pomocą RecyclerView.

Wyświetlanie okien dialogowych na pełnym ekranie bez ramki

Aby wyświetlać okna dialogowe na pełnym ekranie bez ramki, wywołaj funkcję enableEdgeToEdge w oknie dialogowym.

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

Dodatkowe materiały

Więcej informacji o wyświetlaniu bez ramki znajdziesz w tych materiałach.

Blogi

Design

Inna dokumentacja

Filmy