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

1. Wprowadzenie

W Androidzie 10 i nowszym gesty nawigacyjne są obsługiwane jako nowy tryb. Dzięki temu aplikacja może korzystać z całego ekranu i zapewniać lepsze wrażenia podczas wyświetlania. Gdy użytkownik przesunie palcem od dolnej krawędzi ekranu w górę, przejdzie na ekran główny Androida. Gdy użytkownik przesunie palcem do wewnątrz od lewej lub prawej krawędzi, wróci do poprzedniego ekranu.

Dzięki tym dwóm gestom aplikacja może wykorzystać przestrzeń u dołu ekranu. Jeśli jednak Twoja aplikacja używa gestów lub ma elementy sterujące w obszarach gestów systemowych, może to powodować konflikty z gestami systemowymi.

W tym ćwiczeniu w Codelabs dowiesz się, jak używać wcięć, aby uniknąć konfliktów gestów. Ten samouczek ma też na celu nauczenie Cię, jak używać interfejsu API wykluczania gestów w przypadku elementów sterujących, takich jak uchwyty przeciągania, które muszą znajdować się w strefach gestów.

Czego się nauczysz

  • Jak używać detektorów wstawiania w przypadku widoków
  • Jak korzystać z interfejsu Gesture Exclusion API
  • Jak tryb pełnoekranowy działa, gdy gesty są aktywne

Te ćwiczenia z programowania mają na celu dostosowanie aplikacji do gestów systemowych. Nieistotne koncepcje i bloki kodu zostały pominięte. Można je po prostu skopiować i wkleić.

Co utworzysz

Uniwersalny odtwarzacz muzyki na Androida (UAMP) to przykładowa aplikacja do odtwarzania muzyki na Androida napisana w Kotlinie. Skonfigurujesz UAMP do nawigacji przy użyciu gestów.

  • Używaj wcięć, aby odsunąć elementy sterujące od obszarów gestów
  • Używanie interfejsu Gesture Exclusion API do rezygnacji z gestu wstecz w przypadku elementów sterujących, które powodują konflikt
  • Sprawdzanie zmian w trybie immersyjnym w przypadku nawigacji gestami

Czego potrzebujesz

  • urządzenie lub emulator z Androidem 10 lub nowszym,
  • Android Studio

2. Aplikacje ogółem

Uniwersalny odtwarzacz muzyki na Androida (UAMP) to przykładowa aplikacja do odtwarzania muzyki na Androida napisana w Kotlinie. Obsługuje funkcje takie jak odtwarzanie w tle, zarządzanie aktywnością audio, integracja z Asystentem i wiele platform, np. Wear, TV i Auto.

Rysunek 1.: przepływ w UAMP

UAMP wczytuje katalog muzyczny z serwera zdalnego i umożliwia użytkownikowi przeglądanie albumów i utworów. Użytkownik klika utwór, który jest odtwarzany przez podłączone głośniki lub słuchawki. Aplikacja nie jest przeznaczona do współpracy z gestami systemowymi. Dlatego podczas uruchamiania UAMP na urządzeniu z Androidem 10 lub nowszym początkowo możesz napotkać pewne problemy.

3. Konfiguracja

Aby pobrać przykładową aplikację, sklonuj repozytorium z GitHuba i przejdź do gałęzi starter:

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

Możesz też pobrać repozytorium jako plik ZIP, rozpakować go 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 interfejsu API 29. Możesz też podłączyć prawdziwe urządzenie z interfejsem API na poziomie 29 lub nowszym.
  3. Uruchom aplikację. Na wyświetlonej liście utwory są pogrupowane w sekcjach Polecane i Albumy.
  4. Kliknij Polecane i wybierz utwór z listy.
  5. Aplikacja rozpocznie odtwarzanie utworu.

Włączanie nawigacji przy użyciu gestów

Jeśli uruchomisz nową instancję emulatora z poziomem interfejsu API 29, nawigacja gestami 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 za pomocą nawigacji przy użyciu gestów

Jeśli uruchomisz aplikację z włączonym sterowaniem gestami i rozpoczniesz odtwarzanie utworu, możesz zauważyć, że elementy sterujące odtwarzacza znajdują się bardzo blisko obszarów gestów powrotu na ekran główny i wstecz.

4. Wyświetlanie bez ramki

Co to jest wyświetlacz od krawędzi do krawędzi?

Aplikacje działające na Androidzie 10 lub nowszym mogą oferować pełny ekran bez ramki, niezależnie od tego, czy do nawigacji są włączone gesty czy przyciski. Aby zapewnić wyświetlanie od krawędzi do krawędzi, aplikacje muszą być rysowane za przezroczystymi paskami nawigacyjnymi i paskami stanu.

Rysuj za paskiem nawigacyjnym

Aby aplikacja mogła renderować treści pod paskiem nawigacyjnym, musisz najpierw ustawić przezroczyste tło paska nawigacyjnego. Następnie musisz ustawić przezroczystość paska stanu. Dzięki temu aplikacja może wyświetlać się na całej wysokości ekranu.

Aby zmienić kolor paska nawigacyjnego i paska stanu:

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

Zapoznaj się z poniższym przykładowym kodem 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 poinformować system, że aplikacja ma być wyświetlana 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(). Uzyskaj instancję fragmentContainer.
  2. Ustaw te opcje na content.systemUiVisibility:

Zapoznaj się z poniższym przykładowym kodem 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

Ustawiając te flagi razem, informujesz system, że chcesz, aby aplikacja była wyświetlana na pełnym ekranie, tak jakby nie było pasków nawigacji i stanu. Wykonaj te czynności:

  1. Uruchom aplikację i wybierz utwór, aby przejść do ekranu odtwarzacza.
  2. Sprawdź, czy elementy sterujące odtwarzacza są wyświetlane pod paskiem nawigacyjnym, co utrudnia do nich dostęp:

  1. Otwórz ustawienia systemu, wróć do trybu nawigacji za pomocą 3 przycisków i ponownie otwórz aplikację.
  2. Sprawdź, czy sterowanie jest jeszcze trudniejsze w przypadku paska nawigacyjnego z 3 przyciskami: zauważ, że SeekBar jest ukryty za paskiem nawigacyjnym, a przycisk Odtwórz/Wstrzymaj jest w większości zasłonięty przez pasek nawigacyjny.
  3. Eksperymentuj. Gdy skończysz, otwórz ustawienia systemu i przywróć nawigację przy użyciu gestów:

741ef664e9be5e7f.gif

Aplikacja jest teraz wyświetlana na całej powierzchni ekranu, ale występują problemy z łatwością obsługi i konflikty między elementami sterującymi, które się nakładają. Musisz je rozwiązać.

5. Wstawki

WindowInsets informują aplikację, gdzie interfejs systemu pojawia się nad treścią, a także w których regionach ekranu gesty systemowe mają priorytet przed gestami w aplikacji. W Jetpacku wstawki są reprezentowane przez klasę WindowInsets i klasę WindowInsetsCompat. Zdecydowanie zalecamy używanie WindowInsetsCompat, aby zapewnić spójne działanie na wszystkich poziomach interfejsu API.

Wstawki systemowe i obowiązkowe wstawki systemowe

Najczęściej używane typy wstawki to te interfejsy API:

  • Odcięcia okna systemowego: informują, gdzie interfejs systemu jest wyświetlany nad aplikacją. Wyjaśniamy, jak używać odcięć systemowych, aby odsunąć elementy sterujące od pasków systemowych.
  • Wcięcia gestów systemowych: zwracają wszystkie obszary gestów. W tych regionach gesty wykonywane w aplikacjach mogą przypadkowo uruchamiać gesty systemowe.
  • Obowiązkowe wcięcia na gesty: są podzbiorem wcięć na gesty systemowe i nie można ich zastąpić. Wskazują one obszary ekranu, w których gesty systemowe mają zawsze wyższy priorytet niż gesty w aplikacji.

Używanie wstawek do przenoszenia elementów sterujących aplikacją

Teraz, gdy wiesz już więcej o interfejsach API wstawki, możesz naprawić elementy sterujące aplikacji, wykonując czynności opisane poniżej:

  1. Pobierz instancję playerLayout z instancji obiektu view.
  2. Dodaj OnApplyWindowInsetsListener do playerView.
  3. Przesuń widok z obszaru gestów: znajdź wartość wstawki systemowej u dołu i zwiększ o tę wartość dopełnienie widoku. Aby odpowiednio zaktualizować dopełnienie widoku, do [wartości powiązanej z dopełnieniem u dołu aplikacji] dodaj [wartość powiązaną z wartością wstawki systemowej u dołu].

Zapoznaj się z poniższym przykładowym kodem 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. Zauważ, że w elementach sterujących odtwarzacza nic się nie zmienia. Jeśli dodasz punkt przerwania i uruchomisz aplikację w trybie debugowania, zobaczysz, że detektor nie jest wywoływany.
  2. Aby to naprawić, przełącz się na FragmentContainerView, które automatycznie rozwiązuje ten problem. Otwórz activity_main.xml i zmień FrameLayout na FragmentContainerView.

Zapoznaj się z poniższym przykładowym kodem 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 ponownie aplikację i przejdź do ekranu odtwarzacza. Elementy sterujące odtwarzaczem u dołu są przesunięte od obszaru gestów u dołu.

Elementy sterujące aplikacji działają teraz z nawigacją gestami, ale poruszają się bardziej niż oczekiwano. Musisz rozwiązać ten problem.

Zachowaj bieżące dopełnienie i marginesy

Jeśli przełączysz się na inne aplikacje lub przejdziesz do ekranu głównego i wrócisz do aplikacji bez jej zamykania, zauważysz, że elementy sterujące odtwarzaczem za każdym razem przesuwają się w górę.

Dzieje się tak, ponieważ aplikacja wywołuje zdarzenie requestApplyInsets() za każdym razem, gdy rozpoczyna się aktywność. Nawet bez tego wywołania funkcja WindowInsets może być wysyłana wielokrotnie w dowolnym momencie cyklu życia widoku.

Obecny InsetListener na playerView działa prawidłowo za pierwszym razem, gdy dodasz wartość dolnego wcięcia do wartości dolnego dopełnienia aplikacji zadeklarowanej w activity_main.xml. Kolejne wywołania nadal dodają wartość dolnego wcięcia do dolnego dopełnienia już zaktualizowanego widoku.

Aby rozwiązać ten problem, wykonaj te czynności:

  1. Zapisz początkową wartość dopełnienia widoku. Utwórz nową zmienną val i zapisz w niej początkową wartość dopełnienia widoku playerView tuż przed kodem detektora.

Zapoznaj się z poniższym przykładowym kodem NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Użyj tej wartości początkowej, aby zaktualizować dopełnienie u dołu widoku, co pozwoli Ci uniknąć używania bieżącej wartości dopełnienia u dołu aplikacji.

Zapoznaj się z poniższym przykładowym kodem NowPlayingFragment.kt:

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Uruchom aplikację ponownie. Przełączaj się między aplikacjami i wracaj do ekranu głównego. Gdy wrócisz do aplikacji, elementy sterujące odtwarzaczem będą znajdować się tuż nad obszarem gestów.

Przeprojektowane elementy sterujące aplikacją

Pasek przewijania odtwarzacza znajduje się zbyt blisko obszaru gestów u dołu ekranu, co oznacza, że użytkownik może przypadkowo wywołać gest powrotu do ekranu głównego, gdy wykona przesunięcie w poziomie. Jeśli jeszcze bardziej zwiększysz margines, problem może zniknąć, ale odtwarzacz może zostać przesunięty wyżej, niż chcesz.

Użycie wstawek pozwala rozwiązać konflikty gestów, ale czasami dzięki niewielkim zmianom w projekcie można ich całkowicie uniknąć. Aby zmienić układ elementów sterujących odtwarzacza i uniknąć konfliktów gestów, wykonaj te czynności:

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

74918dec3926293f.png

  1. Przełącz na widok kodu.
  2. Aby przenieść SeekBar na górę playerLayout, zmień layout_constraintTop_toBottomOf w przypadku paska przewijania na parent.
  3. Aby przyciągnąć inne elementy w playerView do dołu SeekBar, zmień layout_constraintTop_toTopOf z wartości „parent” na @+id/seekBar w przypadku elementów media_button, titleposition.

Zapoznaj się z poniższym przykładowym kodem 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 skorzystaj z odtwarzacza oraz paska przewijania.

Te niewielkie zmiany w projekcie znacznie poprawiają działanie aplikacji.

6. Gesture Exclusion API

Elementy sterujące odtwarzacza w przypadku konfliktów gestów w obszarze gestów domowych są stałe. Obszar gestu wstecz może też powodować konflikty z elementami sterującymi aplikacji. Poniższy zrzut ekranu pokazuje, że pasek przewijania odtwarzacza znajduje się obecnie w obszarach gestów wstecz po prawej i lewej stronie:

e6d98e94dcf83dde.png

SeekBar automatycznie rozwiązuje konflikty gestów. Może jednak zajść potrzeba użycia innych komponentów interfejsu, które powodują konflikty gestów. W takich przypadkach możesz użyć Gesture Exclusion API, aby częściowo zrezygnować z gestu wstecz.

Korzystanie z interfejsu Gesture Exclusion API

Aby utworzyć strefę wykluczenia gestów, wywołaj metodę setSystemGestureExclusionRects() w widoku z listą obiektów rect. Te obiekty rect są mapowane na współrzędne wykluczonych obszarów prostokątnych. To wywołanie musi być wykonane w metodach onLayout() lub onDraw() widoku. Aby to zrobić, wykonaj te czynności:

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

Zapoznaj się z poniższym przykładowym kodem 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 poniższym przykładowym kodem MySeekBar.kt:

private fun updateGestureExclusion() {

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

Zapoznaj się z poniższym przykładowym kodem 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 Gesture Exclusion API ma limit 200 dp, więc wyklucz tylko kciuk paska przewijania. Uzyskaj kopię granic paska przewijania i dodaj każdy obiekt do listy modyfikowalnej.

Zapoznaj się z poniższym przykładowym kodem 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 systemGestureExclusionRects() za pomocą utworzonych przez siebie list gestureExclusionRects.

Zapoznaj się z poniższym przykładowym kodem 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()onDraw() lub onLayout(). Zastąp onDraw() i dodaj połączenie do updateGestureExclusion.

Zapoznaj się z poniższym przykładowym kodem MySeekBar.kt:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Musisz zaktualizować odniesienia do SeekBar. Aby rozpocząć, otwórz fragment_nowplaying.xml.
  2. Zmień SeekBar na com.example.android.uamp.view.MySeekBar.

Zapoznaj się z poniższym przykładowym kodem 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 SeekBarNowPlayingFragment.kt, otwórz NowPlayingFragment.kt i zmień typ positionSeekBar na MySeekBar. Aby dopasować typ zmiennej, zmień typy ogólne SeekBar w wywołaniu findViewById na MySeekBar.

Zapoznaj się z poniższym przykładowym kodem NowPlayingFragment.kt:

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Uruchom aplikację i kliknij SeekBar. Jeśli nadal występują konflikty gestów, możesz eksperymentować i modyfikować granice kciuka w MySeekBar. Uważaj, aby nie tworzyć strefy wykluczenia gestów większej niż to konieczne, ponieważ ogranicza to inne potencjalne wywołania wykluczenia gestów i powoduje niespójne zachowanie dla użytkownika.

7. Gratulacje

Gratulacje! Wiesz już, jak unikać konfliktów z gestami systemowymi i jak je rozwiązywać.

Aplikacja korzysta z trybu pełnoekranowego, gdy rozszerzysz ją na cały ekran i użyjesz wstawek, aby przenieść elementy sterujące aplikacji poza strefy gestów. Dowiedziałeś się też, jak wyłączyć gest cofania w elementach sterujących aplikacji.

Znasz już najważniejsze kroki, które musisz wykonać, aby Twoje aplikacje działały z gestami systemowymi.

Dodatkowe materiały

Dokumentacja