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

от автора

Привет, Хабр!

В предыдущей статье я рассказал о добавлении 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); 	} 
Послесловие

Ну вот в общем-то и всё, что я хотел сказать. Статья получилась большая, но надеюсь полезная)

ссылка на оригинал статьи http://habrahabr.ru/post/189678/


Комментарии

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

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