Obsługa zmian konfiguracji

Elastyczny interfejs i nawigacja

Aby zapewnić użytkownikom maksymalną wygodę nawigacji, udostępnij interfejs nawigacyjny dostosowany do szerokości, wysokości i najmniejszej szerokości urządzenia użytkownika. Może to być dolny pasek aplikacji, zawsze dostępny lub zwijany szuflada nawigacji, linijka albo coś zupełnie nowego zależnie od dostępnego miejsca na ekranie i unikalnego stylu aplikacji.

przykłady panelu nawigacji, panelu nawigacji i dolnego paska aplikacji.
Rysunek 1. Przykładowe panele kolejowe, panele nawigacji i dolny pasek aplikacji.

Przewodnik po architekturze produktu Material Design zawiera dodatkowy kontekst i zapewnia dodatkowy kontekst, który warto wziąć pod uwagę podczas tworzenia elastycznego interfejsu, czyli interfejsu, który dynamicznie dostosowuje się do zmian środowiskowych. Kilka przykładów zmian środowiskowych to m.in. dostosowanie szerokości, wysokości, orientacji i preferencji języka użytkownika. Te właściwości środowiskowe są zbiorczo nazywane konfiguracją urządzenia.

Gdy co najmniej jedna z tych właściwości zmieni się w czasie działania, system operacyjny Android zareaguje, zniszczając, a potem ponownie tworząc działania i fragmenty aplikacji. Dlatego najlepszym sposobem na obsługę interfejsu elastycznego na Androidzie jest używanie kwalifikatorów konfiguracji zasobów tam, gdzie to możliwe, i unikanie zakodowanych na stałe rozmiarów układu.

Wdrażanie globalnej nawigacji w elastycznym interfejsie

Wdrożenie globalnej nawigacji w ramach elastycznego interfejsu użytkownika rozpoczyna się od działania, które hostuje wykres nawigacyjny. Zapoznaj się z praktycznym przykładem, które znajdziesz w ćwiczeniach z programowania dotyczących nawigacji. Ćwiczenia z programowania wykorzystują element NavigationView do wyświetlania menu nawigacyjnego, jak widać na ilustracji 2. W przypadku działania na urządzeniu, które renderuje się w szerokości co najmniej 960 dp, NavigationView zawsze jest widoczny na ekranie.

Ćwiczenia z programowania dotyczące nawigacji korzystają z widoku nawigacyjnego, który jest zawsze widoczny przy szerokości urządzenia co najmniej 960 dp
Rysunek 2. Ćwiczenia z programowania dotyczące nawigacji korzystają z elementu NavigationView do wyświetlania menu nawigacyjnego.

W razie potrzeby inne rozmiary i orientacje urządzeń dynamicznie przełączają się między DrawerLayout i BottomNavigationView.

dolny widok nawigacyjny i układ szuflady używane w razie potrzeby do menu nawigacyjnego w mniejszych układach urządzeń
Rysunek 3. Ćwiczenia z programowania dotyczące nawigacji korzystają z elementów BottomNavigationView i DrawerLayout, aby wyświetlać menu nawigacyjne na mniejszych urządzeniach.

Możesz zastosować to rozwiązanie, tworząc 3 różne układy, w których każdy układ definiuje wymagane elementy nawigacyjne i hierarchię widoków na podstawie bieżącej konfiguracji urządzenia.

Konfiguracja, do której ma zastosowanie każdy układ, zależy od struktury katalogów, w której znajduje się plik układu. Na przykład plik układu NavigationView znajduje się w katalogu res/layout-w960dp.

<!-- res/layout-w960dp/navigation_activity.xml -->
<RelativeLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_alignParentStart="true"
       app:elevation="0dp"
       app:headerLayout="@layout/nav_view_header"
       app:menu="@menu/nav_drawer_menu" />

   <View
       android:layout_width="1dp"
       android:layout_height="match_parent"
       android:layout_toEndOf="@id/nav_view"
       android:background="?android:attr/listDivider" />

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_alignParentTop="true"
       android:layout_toEndOf="@id/nav_view"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_below="@id/toolbar"
       android:layout_toEndOf="@id/nav_view"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />
</RelativeLayout>

Dolny widok nawigacyjny znajduje się w katalogu res/layout-h470dp:

<!-- res/layout-h470dp/navigation_activity.xml -->
<LinearLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:orientation="vertical"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <androidx.appcompat.widget.Toolbar
       android:id="@+id/toolbar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@color/colorPrimary"
       android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/my_nav_host_fragment"
       android:name="androidx.navigation.fragment.NavHostFragment"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       app:defaultNavHost="true"
       app:navGraph="@navigation/mobile_navigation" />

   <com.google.android.material.bottomnavigation.BottomNavigationView
       android:id="@+id/bottom_nav_view"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:menu="@menu/bottom_nav_menu" />
</LinearLayout>

Układ szuflady znajduje się w katalogu res/layout. Użyj tego katalogu w przypadku układów domyślnych bez kwalifikatorów konfiguracji:

<!-- res/layout/navigation_activity.xml -->
<androidx.drawerlayout.widget.DrawerLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.android.codelabs.navigation.MainActivity">

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">

       <androidx.appcompat.widget.Toolbar
           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:background="@color/colorPrimary"
           android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />

       <androidx.fragment.app.FragmentContainerView
           android:id="@+id/my_nav_host_fragment"
           android:name="androidx.navigation.fragment.NavHostFragment"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           app:defaultNavHost="true"
           app:navGraph="@navigation/mobile_navigation" />
   </LinearLayout>

   <com.google.android.material.navigation.NavigationView
       android:id="@+id/nav_view"
       android:layout_width="wrap_content"
       android:layout_height="match_parent"
       android:layout_gravity="start"
       app:menu="@menu/nav_drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

Podczas ustalania, które zasoby mają być zastosowane, Android przestrzega kolejności pierwszeństwa. W tym przykładzie -w960dp (lub dostępna szerokość >= 960 dp) ma pierwszeństwo przed wartością -h470dp (lub dostępną wysokością >= 470). Jeśli konfiguracja urządzenia nie spełnia żadnego z tych warunków, używany jest domyślny zasób układu (res/layout/navigation_activity.xml).

Podczas obsługi zdarzeń nawigacji musisz połączyć tylko te zdarzenia, które odpowiadają danym widżetom, tak jak w tym przykładzie.

Kotlin

class MainActivity : AppCompatActivity() {

   private lateinit var appBarConfiguration : AppBarConfiguration

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.navigation_activity)
      val drawerLayout : DrawerLayout? = findViewById(R.id.drawer_layout)
      appBarConfiguration = AppBarConfiguration(
                  setOf(R.id.home_dest, R.id.deeplink_dest),
                  drawerLayout)

      ...

      // Initialize the app bar with the navigation drawer if present.
      // If the drawerLayout is not null here, a Navigation button will be added
      // to the app bar whenever the user is on a top-level destination.
      setupActionBarWithNavController(navController, appBarConfig)

      // Initialize the NavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val sideNavView = findViewById<NavigationView>(R.id.nav_view)
      sideNavView?.setupWithNavController(navController)

      // Initialize the BottomNavigationView if it is present,
      // so that clicking an item takes
      // the user to the appropriate destination.
      val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
      bottomNav?.setupWithNavController(navController)

      ...
    }

    ...
}

Java

public class MainActivity extends AppCompatActivity {

   private AppBarConfiguration appBarConfiguration;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.navigation_activity);
       NavHostFragment host = (NavHostFragment) getSupportFragmentManager()
               .findFragmentById(R.id.my_nav_host_fragment);
       NavController navController = host.getNavController();

       DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
       appBarConfiguration = new AppBarConfiguration.Builder(
               R.id.home_dest, R.id.deeplink_dest)
               .setDrawerLayout(drawerLayout)
               .build();

       // Initialize the app bar with the navigation drawer if present.
       // If the drawerLayout is not null here, a Navigation button will be added to
       // the app bar whenever the user is on a top-level destination.
       NavigationUI.setupActionBarWithNavController(
               this, navController, appBarConfiguration);


       // Initialize the NavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       NavigationView sideNavView = findViewById(R.id.nav_view);
       if(sideNavView != null) {
           NavigationUI.setupWithNavController(sideNavView, navController);
       }

       // Initialize the BottomNavigationView if it is present,
       // so that clicking an item takes
       // the user to the appropriate destination.
       BottomNavigationView bottomNav = findViewById(R.id.bottom_nav_view);
       if(bottomNav != null) {
           NavigationUI.setupWithNavController(bottomNav, navController);
       }

   }
}

Jeśli konfiguracja urządzenia się zmieni, o ile nie została jawnie skonfigurowana, Android niszczy aktywność z poprzedniej konfiguracji wraz z powiązanymi z nią widokami. Następnie odtwarza aktywność z zasobami zaprojektowanymi pod kątem nowej konfiguracji. Aktywność jest niszczona i odtwarzana, a następnie automatycznie łączy odpowiednie globalne elementy nawigacyjne w onCreate().

Rozważ alternatywę dla układów podzielonego widoku

Układy z widokiem podzielonym, czyli układy główne i szczegółowe, były kiedyś bardzo popularnym i zalecanym sposobem projektowania na tablety i inne urządzenia z dużymi ekranami.

Od chwili wprowadzenia tabletów z Androidem ekosystem urządzeń bardzo się rozwinął. Jednym z czynników, które wpłynęły na projektowanie urządzeń z dużymi ekranami, jest wprowadzenie trybów wielu okien, a zwłaszcza okien swobodnych, których rozmiar można w pełni zmieniać, np. na urządzeniach z ChromeOS. Znacznie większy nacisk na to, by każdy ekran aplikacji reagował, zamiast na modyfikowaniu struktury nawigacji odpowiednio do rozmiaru ekranu.

Chociaż można wdrożyć interfejs układu z podzielonym widokiem za pomocą biblioteki nawigacji, warto rozważyć inne rozwiązania.

Nazwy miejsc docelowych

Jeśli podasz nazwy miejsc docelowych na wykresie za pomocą atrybutu android:label, zawsze używaj wartości zasobów, aby nadal móc zlokalizować treści.

<navigation ...>
    <fragment
        android:id="@+id/my_dest"
        android:name="com.example.MyFragment"
        android:label="@string/my_dest_label"
        tools:layout="@layout/my_fragment" />
    ...

Dzięki wartościom zasobów do miejsc docelowych automatycznie stosowane są najbardziej odpowiednie zasoby zawsze, gdy zmieni się konfiguracja.