Nawigacja przy użyciu gestów i interfejs od krawędzi do krawędzi

1. Wprowadzenie

Na urządzeniach z Androidem w wersji 10 lub nowszej gesty nawigacji są obsługiwane jako nowy tryb. Dzięki temu aplikacja może korzystać z pełnego ekranu i zapewnia bardziej atrakcyjne wrażenia. Gdy użytkownik przesunie palcem od dolnej krawędzi ekranu w górę, otworzy się ekran główny Androida. Gdy użytkownik przesunie palcem od lewej lub prawej krawędzi do środka, wyświetli się poprzedni ekran.

Dzięki tym dwóm gestom aplikacja może wykorzystać potencjał ekranu u dołu. Jeśli jednak aplikacja używa gestów lub zawiera elementy sterujące w obszarach gestów systemu, może to spowodować konflikt z gestami w całym systemie.

Dzięki temu ćwiczeniu w Codelabs dowiesz się, jak używać wkładek, aby uniknąć konfliktów gestów. Dzięki tym ćwiczeniom w Codelabs dowiesz się, jak używać interfejsu Gesty Exclusion API do elementów sterujących (np. uchwytów przeciągania) w strefach gestów.

Czego dowiesz się

  • Jak używać detektorów wstawionych w widokach
  • Jak korzystać z interfejsu Step Exclusion API
  • Jak działa tryb pojemny, gdy gesty są aktywne

Dzięki temu ćwiczeniu w Codelabs Twoja aplikacja będzie zgodna z gestami systemowymi. Nieistotne koncepcje i bloki kodu zostały zamaskowane i można je skopiować i wkleić.

Co utworzysz

Universal Android Music Player (UAMP) to przykładowa aplikacja odtwarzacza muzyki na Androida napisana w języku Kotlin. Skonfigurujesz usługę UAMP na potrzeby nawigacji przy użyciu gestów.

  • Użyj wektorów, aby odsunąć elementy sterujące poza obszar gestów
  • Aby zrezygnować z gestów cofania w przypadku elementów sterujących, które są w konflikcie, użyj interfejsu Gesty Exclusion API.
  • Używaj swoich kompilacji, aby badać zmiany w trybie pojemnym za pomocą nawigacji przy użyciu gestów

Czego potrzebujesz

  • urządzenia lub emulatora z Androidem 10 bądź nowszym,
  • Android Studio,

2. Aplikacje ogółem

Universal Android Music Player (UAMP) to przykładowa aplikacja odtwarzacza muzyki na Androida napisana w języku Kotlin. Obsługuje ona funkcje takie jak odtwarzanie w tle, obsługa ostrości dźwięku, integracja z Asystentem oraz wiele platform, takich jak Wear, TV i Auto.

Rysunek 1. Przepływ w UAMP

UAMP wczytuje katalog muzyczny ze zdalnego serwera i umożliwia użytkownikowi przeglądanie albumów i utworów. Użytkownik klika utwór, a potem odtwarzany jest przez połączone głośniki lub słuchawki. Aplikacja nie jest przeznaczona do obsługi gestów systemowych. Dlatego gdy uruchomisz UAMP na urządzeniu z Androidem 10 lub nowszym, początkowo pojawią się pewne problemy.

3. Konfiguracja

Aby pobrać przykładową aplikację, skopiuj repozytorium z GitHuba i przełącz się do gałęzi starter:

$  git clone https://github.com/googlecodelabs/android-gestural-navigation/

Możesz też pobrać repozytorium jako plik ZIP, rozpakować je i otworzyć w Android Studio.

Wykonaj te czynności:

  1. Otwórz i skompiluj aplikację w Android Studio.
  2. Utwórz nowe urządzenie wirtualne i wybierz Poziom API 29. Możesz też połączyć prawdziwe urządzenie z interfejsem API na poziomie 29 lub wyższym.
  3. Uruchom aplikację. Wyświetlana lista grupuje utwory w sekcjach Polecane i Albumy.
  4. Kliknij Polecane i wybierz utwór z listy.
  5. Aplikacja rozpocznie odtwarzanie utworu.

Włącz nawigację przy użyciu gestów

Jeśli uruchomisz nową instancję emulatora z interfejsem API poziomu 29, nawigacja przy użyciu gestów może nie być domyślnie włączona. Aby włączyć Nawigację przy użyciu gestów, wybierz Ustawienia systemu > System > Nawigacja w systemie > Nawigacja przy użyciu gestów.

Uruchamianie aplikacji przy użyciu nawigacji przy użyciu gestów

Jeśli uruchomisz aplikację z włączoną Nawigacją przy użyciu gestów i rozpoczniesz odtwarzanie utworu, możesz zauważyć, że elementy sterujące odtwarzaczem znajdują się bardzo blisko obszaru ekranu głównego i cofnięcia.

4. Od krawędzi do krawędzi

Co to jest profilowanie „od brzegu do krawędzi”?

Aplikacje na Androidzie 10 lub nowszym mogą wyświetlać cały ekran bez względu na to, czy nawigacja jest włączona za pomocą gestów czy przycisków. Aby aplikacje były jak najbardziej komfortowe, muszą znajdować się za przezroczystymi paskami stanu i nawigacją.

Rysowanie za paskiem nawigacyjnym

Aby aplikacja renderowała treść pod paskiem nawigacyjnym, musisz najpierw ustawić przezroczyste tło paska nawigacyjnego. Następnie pasek stanu musi być przezroczysty. W ten sposób aplikacja będzie wyświetlać się na całej wysokości ekranu.

Aby zmienić kolor paska nawigacyjnego i stanu, wykonaj następujące czynności:

  1. Pasek nawigacyjny: otwórz res/values-29/styles.xml i ustaw navigationBarColor na color/transparent.
  2. Pasek stanu: w podobny sposób ustaw statusBarColor na color/transparent.

Zapoznaj się z tym przykładowym kodem pliku res/values-29/styles.xml:

<!-- change navigation bar color -->
<item name="android:navigationBarColor">
    @android:color/transparent
</item>

<!-- change status bar color -->
<item name="android:statusBarColor">
    @android:color/transparent
</item>

Flagi widoczności interfejsu systemu

Musisz też ustawić flagi widoczności interfejsu systemu, aby system umieścić aplikację pod paskami systemowymi. Interfejsy API systemUiVisibility w klasie View umożliwiają ustawianie różnych flag. Wykonaj te czynności:

  1. Otwórz klasę MainActivity.kt i znajdź metodę onCreate(). Pobieram instancję fragmentContainer.
  2. Ustaw wartość content.systemUiVisibility:

Zapoznaj się z tym przykładowym kodem pliku MainActivity.kt:

  val content: FrameLayout = findViewById(R.id.fragmentContainer)
  content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

Gdy ustawisz te flagi, informujesz system, że aplikacja ma wyświetlać się na pełnym ekranie, tak jakby nie było na niej nawigacji ani pasków stanu. Wykonaj te czynności:

  1. Uruchom aplikację i przejdź do ekranu odtwarzacza, a potem wybierz utwór, który chcesz odtworzyć.
  2. Sprawdź, czy pod paskiem nawigacyjnym znajdują się elementy sterujące odtwarzaczem, co utrudnia dostęp:

  1. Przejdź do Ustawień systemu, wróć do trybu nawigacji przy użyciu 3 przycisków i wróć do aplikacji.
  2. Upewnij się, że korzystanie z paska nawigacyjnego z 3 przyciskami jest jeszcze trudniejsze. Zwróć uwagę, że przycisk SeekBar jest ukryty za paskiem nawigacyjnym, a funkcja Odtwórz/Wstrzymaj jest w większości zakryta przez pasek nawigacyjny.
  3. Odkrywaj i eksperymentuj. Gdy skończysz, przejdź do ustawień systemu i wróć do Nawigacji przy użyciu gestów:

741ef664e9be5e7f.gif

Aplikacja jest obecnie rysowana od krawędzi do krawędzi, ale występują problemy z obsługą, konflikty i nakładanie się – trzeba je rozwiązać.

5. Wcięcia

WindowInsets informuje aplikację, w którym miejscu nad Twoimi treściami znajduje się interfejs systemu oraz które obszary ekranu Gesty systemowe mają wyższy priorytet niż gesty w aplikacji. Wstawki są reprezentowane przez klasę WindowInsets i klasę WindowInsetsCompat w Jetpack. Aby zapewnić spójne działanie na wszystkich poziomach interfejsu API, zdecydowanie zalecamy używanie interfejsu WindowInsetsCompat.

Wstawki systemu i obowiązkowe wstawki systemu

Najczęściej używane typy wstawianych interfejsów API to:

  • Wbudowane okna systemowe: wskazują, w którym miejscu nad aplikacją znajduje się interfejs systemu. Omówiliśmy, jak za pomocą wkładek systemowych odsunąć elementy sterujące od pasków systemowych.
  • Wbudowane gesty systemowe: zwracają wszystkie obszary gestów. Wszelkie elementy sterujące przesuwaniem w aplikacji w tych regionach mogą przypadkowo aktywować gesty systemowe.
  • Obowiązkowe wstawienia gestów: są podzbiorem wstawienia gestów systemowych i nie można ich zastąpić. Wskazują obszary ekranu, w których gesty systemowe mają zawsze wyższy priorytet niż gesty w aplikacji.

Używanie wkładek do przenoszenia elementów sterujących aplikacji

Teraz, gdy wiesz już więcej o interfejsach API wstawianych, możesz naprawić elementy sterujące aplikacji w sposób opisany poniżej:

  1. Pobierz instancję playerLayout z instancji obiektu view.
  2. Dodaj OnApplyWindowInsetsListener do: playerView.
  3. Odsuń widok poza obszar gestów: znajdź wartość systemową wstawki w dolnej części i zwiększ dopełnienie widoku o tę wartość. Aby odpowiednio zaktualizować dopełnienie widoku, dodaj do [wartość powiązaną z dopełnieniem na dole aplikacji] dodaj [wartość powiązaną z dolną wartością wstawienia systemu].

Zapoznaj się z tym przykładowym kodem pliku NowPlayingFragment.kt:

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Uruchom aplikację i wybierz utwór. Zwróć uwagę, że elementy sterujące odtwarzaczem nic się nie zmieniły. Jeśli dodasz punkt przerwania i uruchomisz aplikację w trybie debugowania, zauważysz, że detektor nie jest wywoływany.
  2. Aby rozwiązać ten problem, przełącz się na usługę FragmentContainerView, która automatycznie rozwiązuje ten problem. Otwórz aplikację activity_main.xml i zmień FrameLayout na FragmentContainerView.

Zapoznaj się z tym przykładowym kodem pliku activity_main.xml:

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    tools:context="com.example.android.uamp.MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
  1. Uruchom aplikację jeszcze raz i przejdź do ekranu odtwarzacza. Elementy sterujące dolnym odtwarzaczem zostały przesuwane poza dolny obszar gestów.

Elementy sterujące aplikacji działają teraz z nawigacją przy użyciu gestów, ale elementy sterujące mogą być przesuwane bardziej niż oczekiwano. Musisz rozwiązać ten problem.

Zachowaj bieżące dopełnienie i marginesy

Jeśli przełączysz się na inną aplikację lub wrócisz do ekranu głównego, ale nie będziesz jej zamykać, elementy sterujące odtwarzaczem za każdym razem przesuwają się w górę.

Dzieje się tak, ponieważ aplikacja wyzwala requestApplyInsets() przy każdym rozpoczęciu aktywności. Nawet bez tego wywołania element WindowInsets może być wysyłany wiele razy w dowolnym momencie w cyklu życia wyświetlenia.

Bieżąca wartość InsetListener w tabeli playerView działa idealnie za pierwszym razem, gdy dodasz wartość dopełnienia dolnej części aplikacji zadeklarowaną w zasadzie activity_main.xml. Kolejne wywołania będą jednak nadal dodawać wstawiona dolną wartość do już zaktualizowanego dolnego dopełnienia widoku.

Aby temu zapobiec, wykonaj te czynności:

  1. Zarejestruj początkową wartość dopełnienia widoku. Utwórz nową wartość i zapisz początkową wartość dopełnienia widoku danych playerView, tuż przed kodem detektora.

Zapoznaj się z tym przykładowym kodem pliku NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Za pomocą tej wartości początkowej zaktualizuj dopełnienie u dołu widoku, dzięki czemu unikniesz użycia bieżącej wartości dopełnienia u dołu aplikacji.

Zapoznaj się z tym przykładowym kodem pliku NowPlayingFragment.kt:

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Ponownie uruchom aplikację. Możesz przechodzić między aplikacjami i przejść do ekranu głównego. Po zwróceniu aplikacji elementy sterujące odtwarzaczem znajdują się tuż nad obszarem gestów.

Nowy wygląd ustawień aplikacji

Pasek przewijania w odtwarzaczu znajduje się zbyt blisko dolnego obszaru gestów, co oznacza, że użytkownik może przypadkowo uruchomić gest ekranu głównego, gdy przesunie w poziomie. Zwiększenie dopełnienia może rozwiązać problem, ale może też przesunąć odtwarzacz wyżej, niż chcesz.

Zastosowanie wstawienia pozwala rozwiązać konflikty gestów, ale czasami po wprowadzeniu niewielkich zmian w wyglądzie można całkowicie uniknąć konfliktu gestów. Aby przeprojektować elementy sterujące odtwarzaczem, by uniknąć konfliktów gestów, wykonaj te czynności:

  1. Otwórz pokój fragment_nowplaying.xml. Przełącz się na widok projektu i u dołu kliknij SeekBar:

74918dec3926293f.png

  1. Przełącz na widok kodu.
  2. Aby przenieść SeekBar na górę elementu playerLayout, zmień layout_constraintTop_toBottomOf paska przewijania na parent.
  3. Aby ograniczyć inne elementy w elemencie playerView w dolnej części elementu SeekBar, zmień ustawienie layout_constraintTop_toTopOf z elementu nadrzędnego na @+id/seekBar w systemach media_button, title i position.

Zapoznaj się z tym przykładowym kodem pliku fragment_nowplaying.xml:

<androidx.constraintlayout.widget.ConstraintLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:padding="8dp"
   android:layout_gravity="bottom"
   android:background="@drawable/media_overlay_background"
   android:id="@+id/playerLayout">

   <ImageButton
       android:id="@+id/media_button"
       android:layout_width="@dimen/exo_media_button_width"
       android:layout_height="@dimen/exo_media_button_height"
       android:background="?attr/selectableItemBackground"
       android:scaleType="centerInside"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:srcCompat="@drawable/ic_play_arrow_black_24dp"
       tools:ignore="ContentDescription" />

   <TextView
       android:id="@+id/title"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Song Title" />

   <TextView
       android:id="@+id/subtitle"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@+id/title"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Artist" />

   <TextView
       android:id="@+id/position"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <TextView
       android:id="@+id/duration"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@id/position"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <SeekBar
       android:id="@+id/seekBar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Uruchom aplikację i wejdź w interakcję z odtwarzaczem i paskiem przewijania.

Te drobne zmiany znacznie poprawiają działanie aplikacji.

6. Interfejs API wykluczania gestów

Naprawiono elementy sterujące odtwarzaczem w obszarze gestów w obszarze głównym. Obszar gestów cofania może też powodować konflikty z elementami sterującymi aplikacji. Na zrzucie ekranu widać, że pasek przewijania odtwarzacza znajduje się obecnie w obszarze gestów w prawej i lewej części ekranu:

e6d98e94dcf83dde.png

SeekBar automatycznie obsługuje konflikty gestów. Aby wyzwalać konflikty gestów, konieczne może być jednak użycie innych komponentów interfejsu. W takich przypadkach możesz użyć Gesture Exclusion API, aby częściowo zrezygnować z gestu cofania.

Używanie interfejsu Gesty Exclusion API

Aby utworzyć strefę wykluczenia gestów, wywołaj w widoku setSystemGestureExclusionRects() z listą obiektów rect. Te rect obiekty są mapowane na współrzędne wykluczonych obszarów prostokątnych. To wywołanie należy wykonać w metodach onLayout() lub onDraw() widoku danych. Aby to zrobić:

  1. Utwórz nowy pakiet o nazwie view.
  2. Aby wywołać ten interfejs API, utwórz nową klasę o nazwie MySeekBar i rozszerz zakres AppCompatSeekBar.

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

class MySeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = android.R.attr.seekBarStyle
) : androidx.appcompat.widget.AppCompatSeekBar(context, attrs, defStyle) {

}
  1. Utwórz nową metodę o nazwie updateGestureExclusion().

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

private fun updateGestureExclusion() {

}
  1. Dodaj zaznaczenie, aby pominąć to wywołanie, na poziomie API 28 lub niższym.

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Interfejs Gesty Exclusion API ma limit wynoszący 200 dp, dlatego pomijaj tylko kciuk paska przewijania. Pobierz kopię granic paska przewijania i dodaj każdy obiekt do listy zmiennych.

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
}
  1. Zadzwoń do użytkownika systemGestureExclusionRects(), korzystając z utworzonych list gestureExclusionRects.

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
    // Finally pass our updated list of rectangles to the system
    systemGestureExclusionRects = gestureExclusionRects
}
  1. Wywołaj metodę updateGestureExclusion() z numeru onDraw() lub onLayout(). Zastąp onDraw() i dodaj wywołanie do updateGestureExclusion.

Zapoznaj się z tym przykładowym kodem pliku MySeekBar.kt:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Musisz zaktualizować pliki referencyjne SeekBar. Najpierw otwórz fragment_nowplaying.xml.
  2. Zmień SeekBar na com.example.android.uamp.view.MySeekBar.

Zapoznaj się z tym przykładowym kodem pliku fragment_nowplaying.xml:

<com.example.android.uamp.view.MySeekBar
    android:id="@+id/seekBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />
  1. Aby zaktualizować odwołania do SeekBar w NowPlayingFragment.kt, otwórz NowPlayingFragment.kt i zmień typ positionSeekBar na MySeekBar. Aby dopasować typ zmiennej, zmień ogólną kategorię SeekBar w wywołaniu findViewById na MySeekBar.

Zapoznaj się z tym przykładowym kodem pliku NowPlayingFragment.kt:

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Uruchom aplikację i wejdź w interakcję z SeekBar. Jeśli konflikty gestów nadal występują, możesz poeksperymentować i zmienić granice kciuka w usłudze MySeekBar. Uważaj, aby nie utworzyć strefy wykluczenia gestów szerszej, niż jest to konieczne, bo ogranicza to liczbę innych potencjalnych wykluczeń gestów i powoduje niespójne zachowanie użytkownika.

7. Gratulacje

Gratulacje! Już wiesz, jak unikać konfliktów i rozwiązywać konflikty za pomocą gestów systemowych.

Aplikacja przełącza się w tryb pełnego ekranu, gdy korzystasz z całkowitej krawędzi i używasz wkładek, aby odsunąć elementy sterujące aplikacji od stref gestów. Wiesz już też, jak wyłączyć systemowy gest cofania w elementach sterujących aplikacji.

Znasz już najważniejsze czynności, które musisz wykonać, aby korzystać z gestów systemowych w aplikacjach.

Materiały dodatkowe

Dokumentacja