ActionBar для Android 2.1+ при помощи Support Library.

15 ноября 2013 г.

Импорт библиотеки

Чтобы наш ActionBar был виден на старых устройствах, нужно наследовать тему приложения от Theme.AppCompat. Сама собой она ниоткуда не возьмётся, поэтому нужно создать проект библиотеки (оригинальная инструкция на английском для Eclipse и Android Studio здесь(смотреть пункт Adding libraries with resources)):
1. Сначала нужно убедиться, что у нас закачана последняя версия Support Library. Для этого открываем SDK Manager и листаем в самый низ, до папки Extras. В ней есть пункт Android Support Library – он то нам и нужен. Обновляем его до последней версии (сейчас – rev. 18), если не сделали этого раньше.
2. Обновили? Молодцы. Теперь нажимаем File > New > В папке Android выбираем Android Project from existing code.
3. Нажимаем кнопку Browse… и ищем нужную нам папку. Путь до неё примерно такой: <ваш SDK>/extras/android/support/v7/appcompat/. Выделяем появившийся пункт в списке и нажимаем Finish.
4. Теперь у вас в Project Explorer должен появиться проект android-support-v7-appcompat. Открываем его, в папке libs/ на обеих .jar – файлах кликаем правой кнопкой и нажимаем Build Path > Add to Build Path.
5. Щелчок правой кнопкой мыши по проекту, выбираем Build Path > Configure Build Path.
6. На странице Build Path во вкладке Order and Export отмечаем два только что добавленных .jar – файла и снимаем отметку с Android Dependencies.
7. Нажимаем ОК для сохранения изменений. Всё – библиотека готова к использованию!

Создание приложения

Теперь создадим проект своего приложения, которое мы и будем делать. Имя – SupportActionBarDemo, пакет – com.habrahabr.sabd, минимальный API level 7 (таковы требования библиотеки). Создаём Activity, имя – MainActivity, layout – main. Теперь кликаем правой кнопкой мыши по этому проекту и нажимаем Properties. На странице Android под заголовком Library нажимаем Add и выбираем в появившемся окне android-support-v7-appcompat, затем – ОК и ещё раз ОК, чтобы сохранить изменения. Теперь библиотека добавлена в проект!

Простой пример

Прежде всего идём в res/values/styles.xml, res/values-v11/styles.xml, res/values-v14/styles.xml и пишем

    <style parent="@style/Theme.AppCompat.Light" name="AppBaseTheme">
 . . .

Теперь ActionBar будет виден на любых версиях андроида.

Открываем res/values/strings.xml и добавляем строки:

<string name="action_item_1">Item 1</string>
<string name="action_item_2">Item 2</string>
<string name="action_item_3">Item 3</string>

Открываем res/menus/main.xml и пишем там:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:sabd="http://schemas.android.com/apk/res-auto" >
    <item
        android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:icon="@android:drawable/ic_menu_add"
        sabd:showAsAction="always" />
    <item
        android:id="@+id/action_item_1"
        android:title="@string/action_item_1"
        android:icon="@android:drawable/ic_menu_delete"
        sabd:showAsAction="ifRoom" />
    <item
        android:id="@+id/action_item_2"
        android:title="@string/action_item_2"
        android:icon="@android:drawable/ic_menu_add"
        sabd:showAsAction="ifRoom|withText" />
    <item
        android:id="@+id/action_item_3"
        android:title="@string/action_item_3"
        android:icon="@android:drawable/ic_menu_add"
        sabd:showAsAction="never" />
</menu>

Обратите внимание на атрибуты xmlns:sabd=«schemas.android.com/apk/res-auto» и sabd:showAsAction – без них ActionBar будет неправильно работать. Иконки случайные, значения не имеют. Я всё-таки напишу, что означает атрибут sabd:showAsAction:

  • always — элемент всегда будет виден, если места не хватает, заголовок будет показан не полностью
  • ifRoom — элемент будет виден, только если для него есть место
  • never — элемент никогда не будет виден, для его показа нужно нажать кнопку Меню на устройстве или кнопку Overflow на ActionBar при отсутствии первой
  • withText -элемент будет показываться только с его заголовком
  • collapseActionView — элемент может сворачиваться в кнопку или разворачиваться на всю ширину Actionbar по нажатию, далее я приведу его пример

Теперь приступаем непосредственно к написанию кода. Первым делом в коде Activity нужно заменить extends Activity на extends ActionBarActivity и добавить её в импорт. В последних версиях ADT при создании Activity автоматически создаётся метод onCreateOptionsMenu(Menu menu), в котором мы и создаём меню:

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;

public class MainActivity extends ActionBarActivity {

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		Log.d("MENU", "Cliced MenuItem is " + item.getTitle());
		return super.onOptionsItemSelected(item);
	}
}

Запускаем приложение и видим наш ActionBar:



Item 2 не отобразился, так как для него не хватило места. Item 3 ни при каких условиях не будет виден, потому что мы выставили атрибут sabd:showAsAction=«never». Добраться до них можно с помощью кнопки «Меню» на устройстве.

Обрабатывать нажатия на элементы меню можно там же, где и раньше — в методе onOptionsItemSelected(MenuItem item). Обрабатывать нажатия на иконку приложения можно в этом же методе, она имеет ID android.R.id.home. Чтобы добавить на ActionBar кнопку «Вверх» («Up Button»), нужно использовать метод ActionBar.setDisplayHomeAsUpEnabled(boolean showHomeAsUp):

Поиск

Иногда нужно сделать поиск, например, как в Google Play. На помощь приходит ActionView. Открываем res/menu/main.xml и удаляем 3 последних элемента — они нам не нужны, а место занимать будут. Вместо них добавляем один новый:

<item android:id="@+id/action_search"
    	android:title="@string/action_search"
        android:icon="@android:drawable/ic_menu_search"
        sabd:showAsAction="always|collapseActionView"
        sabd:actionViewClass="android.support.v7.widget.SearchView" />

атрибут sabd:actionViewClass=«android.support.v7.widget.SearchView» обозначает, какой View будет использован вместо обычного. Текст collapseActionView в атрибуте sabd:showAsAction говорит о том, что ActionView может быть сворачиваться в кнопку или разворачиваться на всю ширину по нажатию. Чтобы использовать его в Activity, изменим код MainActivity:

import android.os.Bundle;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.SearchView.OnQueryTextListener;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends ActionBarActivity implements OnQueryTextListener {

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

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.main, menu);

		MenuItem searchItem = menu.findItem(R.id.action_search);
	    SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
	    searchView.setQueryHint("Поиск");
	    searchView.setOnQueryTextListener(this);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		Log.d("MENU", "Cliced MenuItem is " + item.getTitle());
		return super.onOptionsItemSelected(item);
	}

	public boolean onQueryTextChange(String text_new) {
		Log.d("QUERY", "New text is " + text_new);
		return true;
	}

	public boolean onQueryTextSubmit(String text) {
		Log.d("QUERY", "Search text is " + text);
		return true;
	}
}

Запускаем приложение и видим:

Нажимаем на кнопку поиска:

Кстати, клавиатура появляется автоматически. Выйти из режима поиска можно, нажав на кнопку Вверх в левой части ActionBar или нажав на аппаратную кнопку назад.

В методе onQueryTextChange() мы получаем текст из поля ввода, когда пользователь набирает очередную букву. В методе onQueryTextSubmit(String text) нам даётся текст, который пользователь ищет. У SearchView есть такие полезные методы:

setQuery(CharSequence query, boolean submit) — изменяет текст в поле ввода на тот, который ему передают, опционально делает его конечным (начинает поиск)

getQuery() — возвращает текст, который сейчас есть в поле ввода

Способы навигации

У 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);
	}

setQueryHint(CharSequence hint) — изменяет подсказку на ту, которую ему передают

getQueryHint() — возвращает подсказку

setSuggestionsAdapter(CursorAdapter adapter) — добавляет выпадающий список, как у AutoCompleteTextView

getSuggestionsAdapter() — возвращает адаптер этого списка

setOnCloseListener(SearchView.OnCloseListener listener) — ставит на него обработчик закрытия

setOnSuggestionListener(SearchView.OnSuggestionListener listener) — ставит на него обработчик нажатия на элемент выпадающего списка.

Способы навигации

У 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);
	}

ED98

Теги: рубрика Android
  • Похожие статьи
  • Предыдущие из рубрики