Navigation Drawer — простейший пример


Navigation Drawer — это «выпадающее меню», которое появляется, когда пользователь кликает на иконку в Action Bar-е в левом верхнем углу. Такое меню заслоняет лишь часть экрана, как бы «налаживаясь» сверху на его левую часть. В выпадающем списке отображаются пункты меню, позволяющие быстро перейти в нужную часть приложения. Паттерн Navigation Drawer помогает улучшить юзабилити в тех ситуациях, когда ваше приложение содержит множество различных разделов.

Navigation Drawer

В данной статье приведен простой пример без дополнительной стилизации, чтобы не усложнять и без того непростой код. В примере используется множество различных компонентов, поэтому новичкам рекомендуется иметь хотя бы какое-то представление о том, как работать с фрагментами, меню опций, списками ListView, паттерном Selector.

Создадим простейшее меню из трех пунктов, при клике на которые будет меняться содержание экрана. По умолчанию (при запуске приложения) будет отображаться «первый экран», соответствующий первому пункту меню.

Итак, приступим.

1. Добавим строковые ресурсы — res/values/strings.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">DrawingNavigation</string>
    <string name="action_settings">Settings</string>
    <string-array name="views_array">
        <item>Первый экран</item>
        <item>Второй экран</item>
        <item>Третий экран</item>
    </string-array>
    <string name="menu">Меню</string>
    <string name="first_screen">Первый экран</string>
    <string name="second_screen">Второй экран</string>
    <string name="third_screen">Третий экран</string>
    <string name="open_menu">Open</string>
    <string name="close_menu">Close</string>
</resources>

2. Добавим ресурсы colors — res/values/colors.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="list_background">#000</color>
    <color name="list_background_pressed">#ccc</color>
    <color name="drawer_item_color">#fff</color>
    <color name="list_divider">#5e5e5e</color>
</resources>

3. Добавим верстку для основного layout-а, который будет включать в себя элементы FrameLayout и ListView. Первый будет заполняться содержимым, соответствующим активному пункту меню, а второй будет участвовать в формировании списка меню.

Код res/layout/activity_main.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <!-- The main content view -->
    <FrameLayout
        android:id="@+id/content_frame"
        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="match_parent"
        android:layout_gravity="start"
        android:choiceMode="singleChoice"
        android:divider="@color/list_divider"
        android:dividerHeight="1dp"
        android:listSelector="@drawable/list_selector"
        android:background="@color/list_background"/>
 
</android.support.v4.widget.DrawerLayout>

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

4. Макет для пункта меню Navigation Drawer — res/layout/drawer_list_item.xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/label"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="test"
    android:paddingLeft="10dp"
    android:textColor="@color/drawer_item_color"
    android:textSize="20sp">
</TextView>

5. Добавим в папку drawable макет list_selector.xml с описанием селектора:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item android:drawable="@drawable/list_item_bg_pressed" android:state_pressed="true"/>
    <item android:drawable="@drawable/list_item_bg_pressed" android:state_activated="true"/>    
    <item android:drawable="@drawable/list_item_bg_normal" />
 
</selector>

Здесь мы определяем разные состояния пунктов и соответствующий им макет. Соответственно, добавим также в ресурсы drawable макеты:

list_item_bg_pressed.xml

1
2
3
4
5
6
7
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
  <gradient
      android:startColor="@color/list_background_pressed"
      android:endColor="@color/list_background_pressed"
      android:angle="90" />
</shape>

list_item_bg_normal.xml

1
2
3
4
5
6
7
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
  <gradient
      android:startColor="@color/list_background"
      android:endColor="@color/list_background"
      android:angle="90" />
</shape>

6. Нам требуется три layout-а для каждого фрагмента и java-файл для каждого из них. Они схожи, однако, я приведу код для каждого из них, чтобы сэкономить ваше время.

res/layout/fragment_first.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
        android:id="@+id/txtLabel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="16sp"
        android:text="@string/first_screen"/>
 
    <ImageView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtLabel"
        android:src="@drawable/ic_launcher"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"/>
 
</RelativeLayout>

Java-код для данного фрагмента (в папке src) FirstFragment.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ru.androiddocs.drawingnavigation;
 
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
public class FirstFragment extends Fragment {
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
 
        View rootView = inflater.inflate(R.layout.fragment_first, container, false);
        return rootView;
    }
}

Второй фрагмент.

res/layout/fragment_second.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
        android:id="@+id/txtLabel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="16sp"
        android:text="@string/second_screen"/>
 
    <ImageView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtLabel"
        android:src="@drawable/ic_launcher"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"/>
 
</RelativeLayout>

SecondFragment.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package ru.androiddocs.drawingnavigation;
 
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
public class SecondFragment extends Fragment {
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
 
        View rootView = inflater.inflate(R.layout.fragment_second, container, false);
 
        return rootView;
    }
}

Код для третьего фрагмента.

res/layout/fragment_third.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <TextView
        android:id="@+id/txtLabel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="16sp"
        android:text="@string/third_screen"/>
 
    <ImageView android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/txtLabel"
        android:src="@drawable/ic_launcher"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"/>
 
</RelativeLayout>

ThirdFragment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package ru.androiddocs.drawingnavigation;
 
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
public class ThirdFragment extends Fragment {
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
 
        View rootView = inflater.inflate(R.layout.fragment_third, container, false);
 
        return rootView;
    }
}

7. Добавьте код меню res/menu/main.xml

1
2
3
4
5
6
7
8
9
10
11
<menu 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" 
    tools:context=".MainActivity">
 
    <item android:id="@+id/action_settings" 
        android:title="@string/action_settings"
        android:orderInCategory="100" 
        app:showAsAction="never" />
 
</menu>

В своем проекте вы можете его не использовать (в этом случае можете немного видоизменить код Активити).

8. Добавим MainActivity.java. Тут код получился у нас весьма внушительный.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package ru.androiddocs.drawingnavigation;
 
import android.os.Bundle;
 
import android.app.Fragment;
import android.content.res.Configuration;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
 
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
 
 
public class MainActivity extends ActionBarActivity {
 
    private DrawerLayout myDrawerLayout;
    private ListView myDrawerList;
    private ActionBarDrawerToggle myDrawerToggle;
 
    // navigation drawer title
    private CharSequence myDrawerTitle;
    // used to store app title
    private CharSequence myTitle;
 
    private String[] viewsNames;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        myTitle =  getTitle();
        myDrawerTitle = getResources().getString(R.string.menu);
 
        // load slide menu items
        viewsNames = getResources().getStringArray(R.array.views_array);
        myDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        myDrawerList = (ListView) findViewById(R.id.left_drawer);
 
        myDrawerList.setAdapter(new ArrayAdapter<String>(this, 
                R.layout.drawer_list_item, viewsNames));
 
        // enabling action bar app icon and behaving it as toggle button
        android.support.v7.app.ActionBar actionBar = getSupportActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);
 
        myDrawerToggle = new ActionBarDrawerToggle(this, myDrawerLayout,
                R.string.open_menu,
                R.string.close_menu
        ) {
            public void onDrawerClosed(View view) {
                getSupportActionBar().setTitle(myTitle);
                // calling onPrepareOptionsMenu() to show action bar icons
                invalidateOptionsMenu();
            }
 
            public void onDrawerOpened(View drawerView) {
                getSupportActionBar().setTitle(myDrawerTitle);
                // calling onPrepareOptionsMenu() to hide action bar icons
                invalidateOptionsMenu();
            }
        };
        myDrawerLayout.setDrawerListener(myDrawerToggle);
 
        if (savedInstanceState == null) {
            // on first time display view for first nav item
            displayView(0);
        }
 
        myDrawerList.setOnItemClickListener(new DrawerItemClickListener());
    }
 
    private class DrawerItemClickListener implements ListView.OnItemClickListener {
        @Override
        public void onItemClick(
                AdapterView<?> parent, View view, int position, long id
        ) {
            // display view for selected nav drawer item
            displayView(position);
        }
    }
 
    private void displayView(int position) {
        // update the main content by replacing fragments
        Fragment fragment = null;
        switch (position) {
            case 0:
                fragment = new FirstFragment();
                break;
            case 1:
                fragment = new SecondFragment();
                break;
            case 2:
                fragment = new ThirdFragment();
                break;
            default:
                break;
        }
 
        if (fragment != null) {
            android.app.FragmentManager fragmentManager = getFragmentManager();
            fragmentManager.beginTransaction()
                    .replace(R.id.content_frame, fragment).commit();
 
            // update selected item and title, then close the drawer
            myDrawerList.setItemChecked(position, true);
            myDrawerList.setSelection(position);
            setTitle(viewsNames[position]);
            myDrawerLayout.closeDrawer(myDrawerList);
 
        } else {
            // error in creating fragment
            Log.e("MainActivity", "Error in creating fragment");
        }
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // toggle nav drawer on selecting action bar app icon/title
        if (myDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
        // Handle action bar actions click
        switch (item.getItemId()) {
            case R.id.action_settings:
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }
 
    /**
     * Called when invalidateOptionsMenu() is triggered
     */
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // if navigation drawer is opened, hide the action items
        boolean drawerOpen = myDrawerLayout.isDrawerOpen(myDrawerList);
        menu.findItem(R.id.action_settings).setVisible(!drawerOpen);
        return super.onPrepareOptionsMenu(menu);
    }
 
    @Override
    public void setTitle(CharSequence title) {
        myTitle = title;
        getSupportActionBar().setTitle(myTitle);
    }
 
    /**
     * When using the ActionBarDrawerToggle, you must call it during
     * onPostCreate() and onConfigurationChanged()...
     */
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        // Sync the toggle state after onRestoreInstanceState has occurred.
        myDrawerToggle.syncState();
    }
 
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        // Pass any configuration change to the drawer toggls
        myDrawerToggle.onConfigurationChanged(newConfig);
    }
}

В методе onCreate() мы инициализируем нужные переменные, получаем доступ до основных элементов экрана.

В строчке

1
myDrawerList.setAdapter(new ArrayAdapter<String>(this, R.layout.drawer_list_item, viewsNames));

задаем список пунктов меню для Navigation Drawer с помощью Array-адаптера. В качестве обработчика клика по пунктам меню используется класс DrawerItemClickListener.

При первой загрузке приложения мы задаем номер отображаемого фрагмента (нумерация с 0), т.к. пользователь еще ничего не успел выбрать:

1
2
3
if (savedInstanceState == null) {   
    displayView(0);
}

Метод displayView(int position) получает в параметрах id нужного view, а далее блок switch определяет, каким именно фрагментом нужно заполнить FrameLayout.

Существует несколько вариантов вызова конструктора ActionBarDrawerToggle. Я использовал вариант с 4-мя параметрами, где в качестве 3-го и 4-го указываются строковые ресурсы для состояний меню «открыто/закрыто». Если честно, не совсем понял смысл этих ресурсов, т.к. добиться их отображения не смог. В документации про них сказано:

String resources must be provided to describe the open/close drawer actions for accessibility services.

Что тут подразумевается под «accessibility services» мне не очень понятно.

Проект я собирал в Android Studio. На всякий случай, содержимое моего build.gradle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apply plugin: 'com.android.application'
 
android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"
 
    defaultConfig {
        applicationId "ru.androiddocs.drawingnavigation"
        minSdkVersion 11
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
 
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
}

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

Navigation Drawer

Navigation Drawer

Navigation Drawer

Комментариев: 23 на “Navigation Drawer — простейший пример

  1. Lambrador

    С какой версии android поддерживается?

    Reply
    1. admin Post author

      Данный пример оттестирован на minSdkVersion=»11″ (Android от 3.0.x). Для SDK от 8 можно использовать Support Library (немного отличается работа с Action Bar)

      Reply
      1. Lambrador

        Спасибо за статью, для новичков, как я, будет полезно.

        Reply
    1. admin Post author

      Возможно, у Вас где-то есть ошибки в импортах, проверьте. Также попробуйте использовать логи, чтобы найти источник ошибки

      Reply
  2. WiskiWiski

    Большое спасибо за статью! Все просто и понятно. Автор молодец!

    Reply
  3. Beepop

    Здравствуйте.
    У меня получилось так, что окно тринадцатого по счёту пункта не открывается, а в панеле пункт остаётся выбранным. Пробовал добавить пункты-все последующие не открываются также, а при удалении, самый последний пункт из 10 оставшихся свободно выбирается, но прогрузка активити не идёт.
    Можно ли это решить?

    Reply
    1. admin Post author

      Так сложно ответить, в чем причина. Попробуйте задать вопрос на stackoverflow с публикацией кода.

      Reply
  4. Константин

    По вашему коду видно, что вы заменяете фрагменты каждый раз, когда нажимаете на элемент из навигационной панели.
    А как сделать так, чтобы скрывать видимый фрагмент, а выбранный в меню показывать?

    Reply
    1. admin Post author

      Какую задачу/проблему вы хотите этим решить?

      Reply
  5. Михаил

    Здравствуйте. Спасибо за стаю.
    Подскажите, как добавить сепаратор в меню?

    Reply
    1. admin Post author

      Если я правильно понял вас, то это свойство divider для ListView:
      android:divider и android:dividerHeight
      Тут можно указывать либо цветовой ресурс, либо картинку.

      Reply
  6. Михаил

    Насколько я понимаю, divider разграничивает элементы в listview, и задается как шаблон для всего списка. Мне нужно немного другое — сепаратор с надписью категорий, наподобие header. Например:

    Соц.сети /* сепаратор, не активный элемент
    ВК /* активный элемент
    Одноклассники /* активный элемент
    Справочники /* сепаратор, не активный элемент
    Автомобили /* активный элемент
    Страны /* активный элемент

    Reply
  7. Алексей

    Спасибо за пример. Но допишите пожалуйста про ресурсы menu, которые у Вас упоминаются:
    getMenuInflater().inflate(R.menu.main, menu);
    ….
    case R.id.action_settings:

    Reply
  8. Дмитрий

    Спасибо за статью. Есть вопрос, как менять содержимое FrameLayout я понял, а как с ним работать? Где писать код чтобы например считать введенные данные с поля.

    Reply
    1. admin Post author

      Все зависит от реализации. Попробуйте посмотреть больше примеров работы с фрагментами. Думаю, станет понятнее.

      Reply
  9. Анастасия

    Здравствуйте!
    Пишет ошибку на main в этой строке:
    getMenuInflater().inflate(R.menu.main, menu);

    и на SecondFragment/ThirdFragment в этой части кода:
    case 1:
    fragment = new SecondFragment();
    break;
    case 2:
    fragment = new ThirdFragment();
    С чем это может быть связано? Как устранить эти ошибки? Заранее спасибо.

    Reply
    1. admin Post author

      Добрый день, в статье опущен файл res/menu/main.xml (меню). Вы можете добавить его самостоятельно (статью обновлю в конце недели). SecondFragment/ThirdFragment — это нужно сделать наподобие FirstFragment.java со своими layout-ами

      Reply
  10. Александр

    Огромное спасибо за пример! Всё просто и понятно.
    Как стану программистом — обязательно отблагодарю автора материально !

    Reply
  11. Сергей

    С вашего разрешения хочу поправить вас! В этом примере иконка стандартная, а иконка ic_drawer у вас загружена в пустую. можно исправить так: «mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
    R.drawable.ic_drawer,
    R.string.drawer_open,
    R.string.drawer_close) {»
    и еще добавить что бы не конфликтовал:
    import android.support.v4.app.ActionBarDrawerToggle;
    import android.support.v4.widget.DrawerLayout;

    Reply
    1. admin Post author

      Большое спасибо за Вашу внимательность!:) Этот код я правил неоднократно, и после последней чистки от deprecated-методов упустил из виду, что иконка уже совсем не используется (хотя изначально их было три:)). Я изменил параметры для конструктора ActionBarDrawerToggle. Правда, сделал это с 4-мя параметрами. Импорты править не пришлось.

      Reply
  12. dmi3coder

    Спасибо большое, мучался с этим 3 месяца пока не наткнулся на эту статью! Продолжайте в том же духе!

    Reply
  13. Павел

    Добрый день. Почему-то не работает приложение, в логах всегда появляется ошибка в строке MainActivity
    setContentView(R.layout.activity_main);
    Все делал как у Вас. После даже копировал ваши файлы, чтоб уж точно без ошибок было, но эта ошибка остается. Не подскажете в чем может быть проблема?

    Reply

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*