Привет, Хабр!
В предыдущей статье я рассказал о добавлении Support Library в ваш проект и привёл простой пример SupportActionBar. Но очень часто ActionBar используется не только как замена меню, но и как способ навигации по приложению. Под катом написано, как её реализовать.
У ActionBar есть 3 способа навигации:
NAVIGATION_MODE_STANDART – по сути вообще не навигация, просто ActionBar с элементами;
NAVIGATION_MODE_LIST – вместо заголовка выпадающий список;
NAVIGATION_MODE_TABS – вкладки под ActionBar.
Давайте не будем ничего создавать, а возьмём проект из предыдущей статьи. Создадим новый класс – ScreenFragment, он будет аналогом разных экранов приложения:
Я не стал создавать отдельный xml-файл разметки, он здесь не особо нужен. Мы берём из аргументов номер экрана и вставляем его в программно созданный TextView, который потом показываем.
Изменим код метода onCreate() и добавим ещё один в MainActivity:
В onCreate мы говорим ActionBar, что будем использовать метод навигации – список, и подготавливаем адаптер для него, а также присваиваем обработчик событий. У него всего один метод — onNavigationItemSelected(int position, long id). Он вызывается, когда пользователь выбирает какой–нибудь элемент выпадающего списка. Здесь мы создаём новый ScreenFragment и даём ему номер экрана, чтобы он мог его показать. Затем начинаем FragmentTransaction и добавляем этот фрагмент в View с id=android.support.v7.appcompat.R.id.action_bar_activity_content. Это FrameLayout, куда добавляется наш layout из setContentView(). Запускаем приложение и выбираем различные экраны:
В качестве разметки для элементов выпадающего списка я использую системный layout, но он выглядит не очень красиво. Поэтому лучше использовать свой. За его добавление отвечает метод Adapter.setDropDownViewResource().
Чтобы изменить способ навигации на вкладки, подправим MainActivity:
Также нужно сделать MainActivity... implements... TabListener. Это обработчик нажатий на вкладки. У него есть целых 3 метода:
onTabUnselected(Tab tab, FragmentTransaction ft) — вызывается, когда текущая вкладка закрывается;
onTabSelected(Tab tab, FragmentTransaction ft) — вызывается, когда открывается новая вкладка (срабатывает сразу после предыдущего);
onTabReselected(Tab tab, FragmentTransaction ft) — когда пользователь нажимает на уже открытую вкладку:
Здесь нам уже не нужно создавать FragmentTransaction, она даётся нам изначально (предполагается, что мы будем работать с фрагментами). Но для этой FragmentTransaction нельзя вызывать методы addToBackStack() и commit(). Также у нас есть нажатая вкладка, из которой мы можем вытащить всё, что нужно — текст, иконку, позицию и т.д.
Вкладкам можно присваивать свой View, если системный вас не устраивает — setCustomView(int layoutResId)
Запускаем приложение, щёлкаем по вкладкам:
Кстати, если вкладок очень много, то их заголовки можно скроллить по горизонтали (как в Google Play), но ниже заголовков свайп не работает.
Скорее всего, при нажатии на уже выбранный элемент навигации, на экране ничего не нужно менять. Ну, со вкладками всё понятно — не трогать метод onTabReselected() и всё. А как же быть со списком? Всё очень просто: добавляем в MainActivity переменную
И изменяем код onNavigationItemSelected(int position, long id):
Теперь новый экран будет открываться только при выборе не открытого элемента навигации.
На разных вкладках обычно размещается разный контент, и меню для него должно быть разным. Ребята из гугла сделали такую возможность. Далее я буду показывать всё на примере вкладок. Добавим в ScreenFragment следующий код:
Создадим в папке res/menu/ три файла:
screen_1.xml:
screen_2.xml:
screen_3.xml:
Изменим onTabSelected():
Теперь нужно удалить (ну или лучше закомментить) метод onCreateOptionsMenu — он нам сейчас будет только мешать. И onOptionsItemSelected() в MainActivity тоже подправим:
Сейчас поясню, что я здесь накодил. Дело в том, что во фрагменте тоже можно создавать меню. Чтобы оно было видно, нужно вызывать метод Fragment.setHasOptionsMenu(true). Если мы создаём меню не в Activity, а в фрагменте, то метод onOptionsItemSelected() вызывается сначала в MainActivity, а лишь затем в ScreenFragment, если в Activity возвращается false. Здесь вместо if должно быть switch/case, в конце каждого case — return true; Это значит, что мы уже обработали нажатие и не нужно вызывать onOptionsItemSelected во фрагменте. Например, на каждой вкладке есть пункт меню «Настройки». Чтобы не набирать код в каждом фрагменте, при нажатии на этот пункт возвращаем true. Тогда onOptionsItemSelected() вызывается только в Activity, где мы можем открыть новую SettingsActivity, например. Если запустить программу и на разных вкладках нажимать кнопку «Меню» на устройстве, то будут показаны разные элементы.
При нажатии на пункты меню в логах будет не только их имя, но и в каком классе были обработаны нажатия. А можно вообще создать отдельный xml-файл в папке res/menu/ с этим самым элементом Settings, а в MainActivity в методе onCreateOptionsMenu() создавать меню из этого файла. Тогда 2 меню как бы объединятся, и будут видны пункты обоих.
Часто бывает, что при переключении между вкладками состояние контента на них должно сохраняться. Для этого у фрагментов есть специальный метод — setRetainInstance(boolean retain). Если ему передать true в параметре, то фрагмент не будет создаваться заново. Чтобы проверить это, перепишем метод onTabSelected() в MainActivity:
Ну вот в общем-то и всё, что я хотел сказать. Статья получилась большая, но, надеюсь, полезная)
Часть 1 — Добавление Support Library в проект, простой пример, поиск
Часть 3 — Дополнительные функции
В предыдущей статье я рассказал о добавлении Support Library в ваш проект и привёл простой пример SupportActionBar. Но очень часто ActionBar используется не только как замена меню, но и как способ навигации по приложению. Под катом написано, как её реализовать.
Способы навигации
У ActionBar есть 3 способа навигации:
NAVIGATION_MODE_STANDART – по сути вообще не навигация, просто ActionBar с элементами;
NAVIGATION_MODE_LIST – вместо заголовка выпадающий список;
NAVIGATION_MODE_TABS – вкладки под ActionBar.
Выпадающий список
Давайте не будем ничего создавать, а возьмём проект из предыдущей статьи. Создадим новый класс – ScreenFragment, он будет аналогом разных экранов приложения:
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class ScreenFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
TextView tv = new TextView(getActivity());
tv.setText("Screen " + getArguments().getInt(MainActivity.key_screen_number));
tv.setTextSize(30);
return tv;
}
}
Я не стал создавать отдельный xml-файл разметки, он здесь не особо нужен. Мы берём из аргументов номер экрана и вставляем его в программно созданный TextView, который потом показываем.
Изменим код метода onCreate() и добавим ещё один в MainActivity:
public static final String key_screen_number = "key_screen_number";
ActionBar ab;
FragmentTransaction ft;
ScreenFragment screen_fragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ab = getSupportActionBar();
ab.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
String[] screens = new String[] {"Screen 1", "Screen 2", "Screen 3"};
ArrayAdapter<String> sp_adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, screens);
sp_adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
ab.setListNavigationCallbacks(sp_adapter, this);
selected_list_item_position = -1;
ab.setSelectedNavigationItem(0);
}
public boolean onNavigationItemSelected(int position, long id) {
ft = getSupportFragmentManager().beginTransaction();
screen_fragment = new ScreenFragment();
Bundle args = new Bundle();
args.putInt(key_screen_number, position + 1);
screen_fragment.setArguments(args);
ft.replace(android.support.v7.appcompat.R.id.action_bar_activity_content, screen_fragment);
ft.commit();
return true;
}
В onCreate мы говорим ActionBar, что будем использовать метод навигации – список, и подготавливаем адаптер для него, а также присваиваем обработчик событий. У него всего один метод — onNavigationItemSelected(int position, long id). Он вызывается, когда пользователь выбирает какой–нибудь элемент выпадающего списка. Здесь мы создаём новый ScreenFragment и даём ему номер экрана, чтобы он мог его показать. Затем начинаем FragmentTransaction и добавляем этот фрагмент в View с id=android.support.v7.appcompat.R.id.action_bar_activity_content. Это FrameLayout, куда добавляется наш layout из setContentView(). Запускаем приложение и выбираем различные экраны:
В качестве разметки для элементов выпадающего списка я использую системный layout, но он выглядит не очень красиво. Поэтому лучше использовать свой. За его добавление отвечает метод Adapter.setDropDownViewResource().
Вкладки
Чтобы изменить способ навигации на вкладки, подправим MainActivity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ab = getSupportActionBar();
ab.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
Tab tab = ab.newTab();
tab.setText("Screen 1");
tab.setTabListener(this);
ab.addTab(tab, 0, true);
tab = ab.newTab();
tab.setText("Screen 2");
tab.setTabListener(this);
ab.addTab(tab, 1, false);
tab = ab.newTab();
tab.setText("Screen 3");
tab.setTabListener(this);
ab.addTab(tab, 2, false);
}
Также нужно сделать MainActivity... implements... TabListener. Это обработчик нажатий на вкладки. У него есть целых 3 метода:
onTabUnselected(Tab tab, FragmentTransaction ft) — вызывается, когда текущая вкладка закрывается;
onTabSelected(Tab tab, FragmentTransaction ft) — вызывается, когда открывается новая вкладка (срабатывает сразу после предыдущего);
onTabReselected(Tab tab, FragmentTransaction ft) — когда пользователь нажимает на уже открытую вкладку:
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
}
public void onTabSelected(Tab tab, FragmentTransaction ft) {
screen_fragment = new ScreenFragment();
Bundle args = new Bundle();
args.putInt(key_screen_number, tab.getPosition() + 1);
screen_fragment.setArguments(args);
ft.replace(android.support.v7.appcompat.R.id.action_bar_activity_content, screen_fragment);
}
public void onTabReselected(Tab tab, FragmentTransaction ft) {
}
Здесь нам уже не нужно создавать FragmentTransaction, она даётся нам изначально (предполагается, что мы будем работать с фрагментами). Но для этой FragmentTransaction нельзя вызывать методы addToBackStack() и commit(). Также у нас есть нажатая вкладка, из которой мы можем вытащить всё, что нужно — текст, иконку, позицию и т.д.
Вкладкам можно присваивать свой View, если системный вас не устраивает — setCustomView(int layoutResId)
Запускаем приложение, щёлкаем по вкладкам:
Кстати, если вкладок очень много, то их заголовки можно скроллить по горизонтали (как в Google Play), но ниже заголовков свайп не работает.
Дополнение к «Выпадающий список»
Скорее всего, при нажатии на уже выбранный элемент навигации, на экране ничего не нужно менять. Ну, со вкладками всё понятно — не трогать метод onTabReselected() и всё. А как же быть со списком? Всё очень просто: добавляем в MainActivity переменную
private int selected_list_item_position;
И изменяем код onNavigationItemSelected(int position, long id):
public boolean onNavigationItemSelected(int position, long id) {
if (position != selected_list_item_position) {
ft = getSupportFragmentManager().beginTransaction();
screen_fragment = new ScreenFragment();
Bundle args = new Bundle();
args.putInt(key_screen_number, position + 1);
screen_fragment.setArguments(args);
ft.replace(android.support.v7.appcompat.R.id.action_bar_activity_content, screen_fragment);
ft.commit();
selected_list_item_position = position;
return true;
}
return false;
}
Теперь новый экран будет открываться только при выборе не открытого элемента навигации.
Меню
На разных вкладках обычно размещается разный контент, и меню для него должно быть разным. Ребята из гугла сделали такую возможность. Далее я буду показывать всё на примере вкладок. Добавим в ScreenFragment следующий код:
public static final String key_menu_resource = "key_menu_resource";
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(getArguments().getInt(key_menu_resource), menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
Log.d("MENU", "Cliced MenuItem is " + item.getTitle() + " (from ScreenFragment)");
return true;
}
Создадим в папке res/menu/ три файла:
screen_1.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/item1"
android:title="Item 1"
android:icon="@android:drawable/ic_menu_add"/>
<item
android:id="@+id/settings"
android:title="Settings"
android:icon="@android:drawable/ic_menu_edit"/>
</menu>
screen_2.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/item2"
android:title="Item 2"
android:icon="@android:drawable/ic_menu_camera"/>
<item
android:id="@+id/settings"
android:title="Settings"
android:icon="@android:drawable/ic_menu_edit"/>
</menu>
screen_3.xml:
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/item3"
android:title="Item 3"
android:icon="@android:drawable/ic_menu_call" />
</menu>
Изменим onTabSelected():
private int[] menu_resources = new int[] {R.menu.screen_1, R.menu.screen_2, R.menu.screen_3};
public void onTabSelected(Tab tab, FragmentTransaction ft) {
screen_fragment = new ScreenFragment();
Bundle args = new Bundle();
args.putInt(key_screen_number, tab.getPosition() + 1);
args.putInt(ScreenFragment.key_menu_resource, menu_resources[tab.getPosition()]);
screen_fragment.setArguments(args);
screen_fragment.setHasOptionsMenu(true);
ft.replace(android.support.v7.appcompat.R.id.action_bar_activity_content, screen_fragment);
}
Теперь нужно удалить (ну или лучше закомментить) метод onCreateOptionsMenu — он нам сейчас будет только мешать. И onOptionsItemSelected() в MainActivity тоже подправим:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() != R.id.settings) {
return false;
} else {
Log.d("MENU", "Cliced MenuItem is " + item.getTitle() + " (from MainActivity)");
return true;
}
}
Сейчас поясню, что я здесь накодил. Дело в том, что во фрагменте тоже можно создавать меню. Чтобы оно было видно, нужно вызывать метод Fragment.setHasOptionsMenu(true). Если мы создаём меню не в Activity, а в фрагменте, то метод onOptionsItemSelected() вызывается сначала в MainActivity, а лишь затем в ScreenFragment, если в Activity возвращается false. Здесь вместо if должно быть switch/case, в конце каждого case — return true; Это значит, что мы уже обработали нажатие и не нужно вызывать onOptionsItemSelected во фрагменте. Например, на каждой вкладке есть пункт меню «Настройки». Чтобы не набирать код в каждом фрагменте, при нажатии на этот пункт возвращаем true. Тогда onOptionsItemSelected() вызывается только в Activity, где мы можем открыть новую SettingsActivity, например. Если запустить программу и на разных вкладках нажимать кнопку «Меню» на устройстве, то будут показаны разные элементы.
При нажатии на пункты меню в логах будет не только их имя, но и в каком классе были обработаны нажатия. А можно вообще создать отдельный xml-файл в папке res/menu/ с этим самым элементом Settings, а в MainActivity в методе onCreateOptionsMenu() создавать меню из этого файла. Тогда 2 меню как бы объединятся, и будут видны пункты обоих.
Сохранение состояния
Часто бывает, что при переключении между вкладками состояние контента на них должно сохраняться. Для этого у фрагментов есть специальный метод — setRetainInstance(boolean retain). Если ему передать true в параметре, то фрагмент не будет создаваться заново. Чтобы проверить это, перепишем метод onTabSelected() в MainActivity:
private int[] menu_resources = new int[] {R.menu.screen_1, R.menu.screen_2, R.menu.screen_3};
private ScreenFragment[] screens = new ScreenFragment[] {new ScreenFragment(), new ScreenFragment(), new ScreenFragment()};
public void onTabSelected(Tab tab, FragmentTransaction ft) {
screen_fragment = screens[tab.getPosition()];
Bundle args = new Bundle();
args.putInt(key_screen_number, tab.getPosition() + 1);
args.putInt(ScreenFragment.key_menu_resource, menu_resources[tab.getPosition()]);
screen_fragment.setArguments(args);
screen_fragment.setHasOptionsMenu(true);
screen_fragment.setRetainInstance(true);
ft.replace(android.support.v7.appcompat.R.id.action_bar_activity_content, screen_fragment);
}
Послесловие
Ну вот в общем-то и всё, что я хотел сказать. Статья получилась большая, но, надеюсь, полезная)
Часть 1 — Добавление Support Library в проект, простой пример, поиск
Часть 3 — Дополнительные функции