Pull to refresh

ActionBar на Android 2.1+ с помощью Support Library. Часть 2 — Навигация

Reading time 8 min
Views 19K
Привет, Хабр!

В предыдущей статье я рассказал о добавлении 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, в конце каждого casereturn 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 — Дополнительные функции
Tags:
Hubs:
+6
Comments 0
Comments Leave a comment

Articles