Перевод статьи «Фрагментарный Navigation Drawer»

Источник: http://guides.codepath.com/android/Fragment-Navigation-Drawer

В кратких обзорах Общих парадигм навигации мы обсуждаем различные навигационные структуры, доступные в приложениях на Android. Одной из наиболее гибких является Navigation Drawer. На конференции I/O 2015 Google выпустили в релиз NavigationView, что значительно упрощает его создание, по сравнению с прежними задокументированными инструкциями.

С релизом Android 5.0 Lolipop, navigation drawer в новом стиле material design размещается по всей высоте экрана и отображается над ActionBar, перекрывая полупрозрачный StatusBar. Прочитайте navigation drawer в стиле material design, чтобы ознакомиться с инструкциями по стилизации вашего navigation drawer.

Использование

В этом руководстве объясняется настройка drawer’а, оформленного в стиле базового material design’а и наполненного элементами навигации, которые переключают различные фрагменты в зоне контента. Таким образом, можно определить несколько фрагментов, а затем определить список вариантов, которые будут отображаться в списке элементов drawer’ов. Каждый элемент при нажатии будет внедрен в соответствующий фрагмент во view-контейнере activity.

Установка

Не забудьте установить библиотеку Design Support от Google перед тем как использовать их новый NavigationView, объявленный в качестве составляющей релиза Android M. NavigationView должен быть обратно совместим со всеми версиями Android, вплоть до 2.1.

Убедитесь, что у вас есть эта Gradle-зависимость, добавленная к вашему файлу app/build.gradle:

dependencies {
compile 'com.android.support:design:25.1.1'
}

Загрузка иконок элементов Nav Drawer’а

Загрузите представленные ниже иконки и добавьте их в ваши папки drawable, копируя и вставляя их в папку drawable, либо используя новое диалоговое окно New Image Asset, чтобы создать версии для любой плотности пикселей.

· первая иконка

· вторая иконка

· третья иконка

Если вы используете диалоговое окно New Image Asset, то выберите приоритетный цвет и измените имя ресурса.

Ресурсы для настройки Drawer’а

Создайте файл menu/drawer_view.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/nav_first_fragment"
android:icon="@drawable/ic_one"
android:title="First" />
<item
android:id="@+id/nav_second_fragment"
android:icon="@drawable/ic_two"
android:title="Second" />
<item
android:id="@+id/nav_third_fragment"
android:icon="@drawable/ic_three"
android:title="Third" />
</group>
</menu>

Обратите внимание, что вы можете установить один из этих элементов в качестве элемента, выбранного по умолчанию, при помощи android:checked=”true”.

При этом вы можете создать подзаголовки и в то же время группировать элементы вместе:

<item android:title="Sub items">
<menu>
<group android:checkableBehavior="single">
<item
android:icon="@drawable/ic_dashboard"
android:title="Sub item 1" />
<item
android:icon="@drawable/ic_forum"
android:title="Sub item 2" />
</group>
</menu>
</item>

Определение фрагментов

Далее, вы должны определить свои фрагменты, которые будут отображаться внутри drawer’а. Это могут быть любые поддерживаемые фрагменты, которые вы определили в своём приложении. Убедитесь, что все фрагменты наследуются от android.support.v4.app.Fragment.

Настройка панели инструментов

Чтобы наш navigation drawer плавно выдвинулся над ActionBar’ом, нам нужно использовать новый виджет Toolbar, как это повелось в 21'ой версии библиотеки AppCompat. Toolbar можно встроить в вашу иерархию view’ов, что гарантирует скольжение drawer’а над ActionBar’ом.

Создайте новый файл layout’а «res/layout/toolbar.xml» с помощью следующего кода:

<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:minHeight="?attr/actionBarSize"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:background="?attr/colorPrimaryDark">
</android.support.v7.widget.Toolbar>

Обратите внимание, что когда атрибут android:fitsSystemWindows установлен для view как true, view будет расположен так, как если бы присутствовали StatusBar и ActionBar, т. е. находящийся сверху UI получает такой внутренний отступ, который будет достаточным для того, чтобы UI не был сокрыт панелью навигации. Без этого атрибута величина внутреннего отступа будет недостаточно учтена при расчёте ToolBar’а:

Нам требуется, чтобы у view основного контента была панель навигации и в связи с этим атрибут android:fitsSystemWindows установлен как true для Toolbar’а.

Чтобы использовать Toolbar в роли ActionBar’а, вам нужно отключить дефолтный ActionBar. Это можно сделать путём установки темы приложения в файле styles.xml.

<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">#673AB7</item>
<item name="colorPrimaryDark">#512DA8</item>
<item name="colorAccent">#FF4081</item>
</style>
</resources>

Также обратите внимание, что обычно цветовую схему вы должны определить посредством перехода к Material Palette и выбора основного и тёмного основного цветов. Для этого примера мы выберем цвета, основанные на фиолетовом, как показано на скриншоте.

Примечание: Если вы забыли отключить ActionBar в styles.xml, то, вероятно, вы увидите исключение java.lang.illegalStateException с сообщением об ошибке, которое гласит: «У этой Activity уже есть action bar, предоставленный оформлением окна». Не запрашивайте Window.FEATURE_ACTION_BAR, а установите вместо этого в своей теме windowActionBar к false, чтобы использовать Toolbar. Если вы видите это сообщение, вы должны убедиться, что придерживаетесь предыдущих шагов.

Настройка Drawer’а в Activity

Давайте теперь настроим главный navigation drawer, основанный на приведенном ниже файле layout’а, полная настройка drawer’а которого находится в res/layout/activity_main.xml. Обратите внимание, что Toolbar добавляется в качестве первого потомка основного контентного view посредством добавления тега include.

<!-- This DrawerLayout has two children at the root  -->
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<!--
Этот LinearLayout отображает содержимое экрана -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!-- ActionBar отображается сверху -->
<include
layout="@layout/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<!--
Основной контентный view, куда загружаются фрагменты -->
<FrameLayout
android:id="@+id/flContent"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

<!--
navigation drawer, который появляется слева -->
<!--
Обратите внимание, что `android:layout_gravity` нужно установить в 'start' -->
<android.support.design.widget.NavigationView
android:id="@+id/nvView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
app:menu="@menu/drawer_view" />
</android.support.v4.widget.DrawerLayout>

Давайте теперь настроим drawer в нашей activity. Мы также можем заодно настроить иконку меню.

public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawer;
private Toolbar toolbar;
private NavigationView nvDrawer;

//
Убедитесь, что используется версия
// android.support.v7.app.ActionBarDrawerToggle.

// android.support.v4.app.ActionBarDrawerToggle
устарел.

private ActionBarDrawerToggle drawerToggle;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//
Установить Toolbar для замены ActionBar'а.
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

// Найти наш view drawer'а
mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
//
Действие home/up action bar'а должно открывать или закрывать drawer.
switch (item.getItemId()) {
case android.R.id.home:
mDrawer.openDrawer(GravityCompat.START);
return true;
}

return super.onOptionsItemSelected(item);
}
}

Навигация между элементами меню

Настройте обработчик так, чтобы он отвечал на события нажатия по элементам навигации и выгружал фрагмент. Это можно включить прямо в activity:

public class MainActivity extends AppCompatActivity {

// ...


@Override
protected void onCreate(Bundle savedInstanceState) {
// ...Из параграфа выше...

// Найти наш view drawer'а
nvDrawer = (NavigationView) findViewById(R.id.nvView);
        // Настроить view drawer'а
setupDrawerContent(nvDrawer);
}

private void setupDrawerContent(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(
new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
selectDrawerItem(menuItem);
return true;
}
});
}

public void selectDrawerItem(MenuItem menuItem) {
// Создать новый фрагмент и задать фрагмент для отображения
// на основе нажатия на элемент навигации
Fragment fragment = null;
Class fragmentClass;
switch(menuItem.getItemId()) {
case R.id.nav_first_fragment:
fragmentClass = FirstFragment.class;
break;
case R.id.nav_second_fragment:
fragmentClass = SecondFragment.class;
break;
case R.id.nav_third_fragment:
fragmentClass = ThirdFragment.class;
break;
default:
fragmentClass = FirstFragment.class;
}

try {
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}

// Вставить фрагмент, заменяя любой существующий
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction().replace(R.id.flContent, fragment).commit();

// Выделение существующего элемента выполнено с помощью
// NavigationView
menuItem.setChecked(true);
        // Установить заголовок для action bar'а
setTitle(menuItem.getTitle());
        // Закрыть navigation drawer
mDrawer.closeDrawers();
}

// ...

}

Добавляем navigation header

Также NavigationView принимает кастомный атрибут, который может ссылаться на layout, предоставляющий header нашего layout’а. Например, можно создать файл layout/nav_header.xml, аналогичный следующему:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="192dp"
android:background="?attr/colorPrimaryDark"
android:padding="16dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:orientation="vertical"
android:gravity="bottom">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Header"
android:textColor="@android:color/white"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>

</LinearLayout>

Затем вы бы сослались на это в layout’е res/layout/activity_main.xml в NavigationView с помощью атрибута app:headerLayoutcustom:

<!-- res/layout/activity_main.xml -->

<!-- The navigation drawer -->
<android.support.design.widget.NavigationView
...
app:headerLayout="@layout/nav_header">

</android.support.design.widget.NavigationView>

Этот app:headerLayout автоматически раздувает указанный layout до header’а.

// Поиск navigation view
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_draw);
// Раздуть header view во время выполнения
View headerLayout = navigationView.inflateHeaderView(R.layout.nav_header);
// Теперь, при необходимости, мы можем найти элементы внутри 
// header'а
ImageView ivHeaderPhoto = headerLayout.findViewById(R.id.imageView);

Получение ссылок на заголовки

Примечание: В версии 23.1.0 библиотеки design support NavigationView переходит на использование RecyclerView’а, и выбрасываются NPE (null-исключения) при поиске в header’е до тех пор, пока header не будет добавлен во время выполнения. Если вам нужно получить ссылку на header, то вам необходимо использовать новый метод getHeaderView(), представленный в последнем обновлении v23.1.1:

// Обычно есть только один header view.
// Технически, можно добавить во время выполнения несколько header
// view.
// Мы можем использовать navigationView.getHeaderCount(), чтобы
// определить общее количество.
View headerLayout = navigationView.getHeaderView(0);

Анимируем иконку гамбургера

Чтобы иконка гамбургера воспроизводила анимацию для обозначения того, что drawer открывается и закрывается, нужно использовать класс ActionBarDrawerToggle.

В вашем файле res/values/strings.xml добавьте:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
</resources>

Нам нужно связать вместе DrawerLayout и Toolbar:

protected void onCreate(Bundle savedInstanceState) { 
// Установить Toolbar для замены ActionBar'а.

toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

// Найти наш view drawer'а

mDrawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawerToggle = setupDrawerToggle();

// Привязать события DrawerLayout'а к ActionBarToggle
mDrawer.addDrawerListener(drawerToggle);
}

private ActionBarDrawerToggle setupDrawerToggle() {
// Примечание: Убедитесь, что вы передаёте допустимую ссылку
// на toolbar
// ActionBarDrawToggle() не предусматривает в ней
// необходимости и не будет отображать иконку гамбургера без
// неё

return new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.drawer_open, R.string.drawer_close);
}

Затем нам необходимо убедиться, что мы синхронизируем состояние всякий раз, когда экран восстанавливается, либо когда конфигурация меняется (т. е. при повороте экрана).

// `onPostCreate` вызывается при завершении запуска activity после 
// `onStart()`

// ПРИМЕЧАНИЕ 1: Не забудьте переопределить метод при помощи
// только одного аргумента `Bundle`

// ПРИМЕЧАНИЕ 2: Убедитесь, что вы реализуете соответствующий метод `onPostCreate(Bundle savedInstanceState)`.

//
Есть две сигнатуры и только `onPostCreate(Bundle state)`
// показывает иконку гамбургера.
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
//
Синхронизировать состояние переключения после того, как
// возникнет onRestoreInstanceState
drawerToggle.syncState();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//
Передать любые изменения конфигурации переключателям
// drawer'а
drawerToggle.onConfigurationChanged(newConfig);
}

При этом нам нужно изменить метод onOptionsItemSelected() и позволить ActionBarToggle обрабатывать события.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
}
return super.onOptionsItemSelected(item);
}

ActionBarToggle будет осуществлять ту же функцию, что и раньше, но добавляет еще немного проверок и делает возможными щелчки мышью по иконке, чтобы открывать и закрывать drawer. Больше контекста можно увидеть в исходном коде.

Также стоит отметить, что ActionBarDrawerToggle отображает вам кастомный DrawerArrowDrawable, относящийся к иконке гамбургера.

Кроме того, убедитесь, что используется версия android.support.v7.app.ActionBarDrawerToggle. android.support.v4.app.ActionBarDrawerToggle устарела.

Делаем Status Bar полупрозрачным

Чтобы получить прозрачный status bar и плавно выдвигающийся над ним drawer, нам необходимо присвоить true атрибуту android:windowTranslucentStatus. Поскольку этот стиль не доступен для устройств с pre Kitkat, мы добавим файл res/values-v19/styles.xml для API, начиная с его 19 версии.

Примечание: Если вы измените ваш res/values/styles.xml напрямую с помощью этой строчки android:windowTranslucentStatus, то, вероятно, вам понадобится выполнить компиляцию только для SDK 19'ой версии и старше, что, очевидно, ограничит вас в поддержке многих более старых устройств.

В res/values-v19/styles.xml можем добавить следующее:

<resources>
<!--
Базовая тема приложения. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--
Кастомизируем вашу тему здесь. -->
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>

Теперь, если запустите своё приложение, то должны увидеть navigation drawer и сможете выбирать из своих фрагментов.

Добавление custom view’ов navigation drawer’у

Одно из улучшений, включённых в библиотеку design support 23.1.0 — добавление поддержки custom view для элементов navigation drawer. Например, мы можем создать кастомный переключатель, наподобие navigation drawer’а от Google Play Movies для одной из этих строк:

Принцип тот же, что и при добавлении элементов ActionView в ActionBar. Нам нужно просто определить разделение layout’а, такое же, как в представленном ниже сниппете. Мы обратимся к этому файлу «action_view_switch.xml»:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.SwitchCompat
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:text="Switch"/>
</LinearLayout>

Потом мы ссылаемся на этот layout, используя атрибут app:actionLayout. Нужно задать заголовок, но его можно сделать и пустым:

<menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/nav_switch"
app:actionLayout="@layout/action_view_switch"
android:title="Downloaded only" />
</item>
</menu>

Вы можете подключить события непосредственно в XML, при условии, что ваша Activity будет реализовывать метод. Чтобы программно посредством Java добавить обработчик события к переключателю вам сначала понадобится получить экземпляр меню и иметь доступ к соответствующему ActionView:

Menu menu = navigationView.getMenu();
MenuItem menuItem = menu.findItem(R.id.nav_switch);
View actionView = MenuItemCompat.getActionView(menuItem);
actionView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

Кастомные виджеты, использующие app:actionViewClass при этом тоже можно использовать для элементов меню аналогичным образом, что и сейчас. Для дополнительных сведений об Action View’ах, смотрите руководство по добавлению SearchView к ActionBar’у.

Неизменяемый Navigation Drawer

В определенных ситуациях, особенно на планшетах, navigation drawer должен быть постоянным элементом activity, действующим как sidebar:

Для достижения такого эффекта посмотрите на представленные ниже ссылки, которые описывают один подход:

· Статичный Nav Drawer

· Вопросы, связанные со Stackoverflow

· Пример кода

Сторонние библиотеки также могут облегчить осуществление этого.

Сторонние библиотеки

Есть несколько сторонних библиотек, которые всё еще актуальны в качестве возможных альтернатив прямому использованию DrawerLayout’а, и которые автоматически предоставляют определенные элементы material design’а.

· MaterialDrawer

· NavigationDrawerMaterial

Зачастую они не являются обязательными, но проверьте их, чтобы увидеть ту функциональность, которую они обеспечивают.

Ограничения

Текущая версия библиотеки design support включает в себя ограничения. Основная проблема связана с системой, которая выделяет текущий элемент в навигационном меню. Атрибут itemBackground для NavigationView не обрабатывает корректно состояние «отмечено» для элемента: почему-то выделяются либо все элементы, либо ни один из них. Это делает данный атрибут по сути дела непригодным для большинства приложений.

Альтернатива фрагментам

Несмотря на то, что многие примеры navigation drawer’а показывают, как можно использовать фрагменты с navigation drawer, вы также можете использовать RelativeLayout/LinearLayout, если желаете воспользоваться drawer’ом как оверлеем для вашей отображаемой в данный момент Activity.

Вы можете вместо <FrameLayout> использовать <LinearLayout>

<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/drawer_layout">

<LinearLayout
android:id="@+id/content_frame"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<!-- The navigation drawer -->
<ListView android:id="@+id/left_drawer"
android:layout_width="240dp"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:divider="@android:color/transparent"
android:dividerHeight="0dp"
android:background="#111"/>

</android.support.v4.widget.DrawerLayout>

Вместо вот этого:

// Вставить фрагмент, заменяя любой существующий
getFragmentManager().beginTransaction()
.replace(R.id.content_frame, fragment)
.commit();

Вы можете использовать контейнер LinearLayout, чтобы раздуть Activity напрямую:

LayoutInflater inflater = getLayoutInflater();
LinearLayout container = (LinearLayout) findViewById(R.id.content_frame);
inflater.inflate(R.layout.activity_main, container);

Ссылки

· http://android-developers.blogspot.com/2014/10/appcompat-v21-material-design-for-pre.html

· http://stackoverflow.com/questions/26440879/how-do-i-use-drawerlayout-to-display-over-the-actionbar-toolbar-and-under-the-st

· http://antonioleiva.com/navigation-view/

Спасибо, если дочитали до конца! Приглашаю вас в свою группу переводов материалов о программировании: https://vk.com/web_translate; в Telegram: https://telegram.me/web_translate, https://telegram.me/joinchat/EQpU2gkfzdUJ8nnOCKnVlg.

Перевёл и оформил Romanov Sergei, 16.02.2017.