Глава 10. Аргументы фрагментов
Получение аргументов
Когда фрагменту требуется получить доступ к его аргументам, он вызывает метод getArguments()
класса
Fragment
, а затем один из get
-методов
Bundle для конкрет- ного типа.
В методе
CrimeFragment.onCreate(…)
замените код упрощенного решения выборкой
UUID из аргументов фрагмента.
Листинг 10.7. Получение идентификатора преступления из аргументов (CrimeFragment.java)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UUID crimeId = (UUID)getActivity().getIntent()
.getSerializableExtra(EXTRA_CRIME_ID);
UUID crimeId = (UUID)getArguments().getSerializable(EXTRA_CRIME_ID);
mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
}
Запустите приложение CriminalIntent. Оно работает точно так же, но архитектура с независимостью
CrimeFragment должна вызвать у вас приятные чувства. Кроме того, она хорошо подготовит нас к следующей главе, в которой мы реализуем более сложную систему навигации в CriminalIntent.
Перезагрузка списка
Осталась еще одна подробность, которой нужно уделить внимание. Запустите приложение CriminalIntent, щелкните на элементе списка и внесите изменения в подробную информацию о преступлении. Эти изменения сохраняются в модели, но при возвращении к списку представление остается неизменным.
Мы должны сообщить адаптеру спискового представления, что набор данных из- менился (или мог измениться), чтобы тот мог заново получить данные и повторно загрузить список. Работая со стеком возврата
ActivityManager
, можно перезагрузить список в нужный момент.
Когда
CrimeListFragment запускает экземпляр
CrimeActivity
, последний помещается на вершину стека. При этом экземпляр
CrimeActivity
, который до этого находился на вершине, приостанавливается и останавливается.
Когда пользователь нажимает кнопку
Back для возвращения к списку, экземпляр
CrimeActivity извлекается из стека и уничтожается. В этот момент
CrimeListAc- tivity запускается и продолжает выполнение.
Когда экземпляр
CrimeListActivity продолжает выполнение, он получает вызов onResume()
от ОС. При получении этого вызова
CrimeListActivity его экземпляр
FragmentManager вызывает onResume()
для фрагментов, хостом которых в настоящее
Запуск
активности из фрагмента209время является активность. В нашем случае это единственный фрагмент
CrimeList-
Fragment
Нажатие кнопки Back
Нажатие на строке
Рис. 10.4. Стек возврата CriminalIntent
В классе
CrimeListFragment переопределите onResume()
для перезагрузки списка.
Листинг 10.8. Перезагрузка списка в onResume() (CrimeListFragment.java)
@Overridepublic void onResume() { super.onResume(); ((CrimeAdapter)getListAdapter()).notifyDataSetChanged();}Почему для обновления спискового представления переопределяется метод onResume()
, а не onStart()
? Мы не можем предполагать, что активность останав- ливается при нахождении перед ней другой активности. Если другая активность прозрачна, ваша активность может быть только приостановлена. Если же ваша активность приостановлена, а код обновления находится в onStart()
, список не будет перезагружаться. В общем случае самым безопасным местом для выпол- нения действий, связанных с обновлением представления фрагмента, является метод onResume()
Запустите приложение CriminalIntent. Выберите преступление в списке и измените его подробную информацию. Вернувшись к списку, вы немедленно увидите свои изменения.
За последние две главы приложение CriminalIntent серьезно продвинулось вперед.
Давайте взглянем на обновленную диаграмму объектов.
210
Глава 10. Аргументы фрагментов
Контроллер
Модель
Представ- ление
Рис. 10.5. Обновленная диаграмма объектов CriminalIntent
Получение результата
с использованием фрагментов
В этой главе нам не требовалось, чтобы запущенная активность возвращала резуль- тат. А если бы это было нужно? Код выглядел бы почти так же, как в приложении
GeoQuiz. Вместо метода startActivityForResult(…)
класса
Activity использовался бы метод
Fragment.startActivityForResult(…)
. Вместо
Activity.onActivityRe- sult(…)
мы переопределим
Fragment.onActivityResult(…)
public class CrimeListFragment extends ListFragment {
private static final int REQUEST_CRIME = 1;
public void onListItemClick(ListView l, View v, int position, long id) {
// Получение объекта Crime от адаптера
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
Log.d(TAG, c.getTitle() + " was clicked");
// Запуск CrimeActivity
Intent i = new Intent(getActivity(), CrimeActivity.class);
startActivityForResult(i, REQUEST_CRIME);
}
Получение результата с использованием фрагментов
211
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CRIME) {
// Обработка результата
}
}
}
Метод
Fragment.startActivityForResult(Intent,int)
похож на одноименный метод класса
Activity
. Он включает дополнительный код передачи результата вашему фрагменту от активности-хоста.
Возвращение результата от фрагмента выглядит немного иначе. Фрагмент может получить результат от активности, но не может вернуть собственный результат — на это способны только активности. Таким образом, хотя
Fragment содержит методы startActivityForResult(…)
и onActivityResult(…)
, у него нет методов setResult(…)
Вместо этого вы приказываете активности-хосту вернуть значение; вот как это делается:
public class CrimeFragment extends Fragment {
public void returnResult() {
getActivity().setResult(Activity.RESULT_OK, null);
}
}
Возможность возвращения результата активности из фрагмента для приложения
CriminalIntent продемонстрирована в главе 20.
ViewPagerВ
этой главе мы создадим новую активность, которая станет хостом для
Crime-
Fragment
. Макет активности будет состоять из экземпляра
ViewPager
. Включение виджета
ViewPager в пользовательский интерфейс позволяет «листать» элементы списка, проводя пальцем по экрану.
Справа налево
Слева направо
Рис. 11.1. Листание страниц
На рис. 11.2 представлена обновленная диаграмма CriminalIntent. Новая актив- ность с именем
CrimePagerActivity займет место
CrimeActivity
. Ее макет состоит из экземпляра
ViewPager
Все новые объекты, которые необходимо создать, находятся в пунктирном пря- моугольнике на приведенной диаграмме. Для реализации листания страничных представлений в CriminalIntent ничего другого менять не придется. В частности, класс
CrimeFragment останется неизменным благодаря той работе по обеспечению независимости
CrimeFragment
, которую мы проделали в главе 10.
11
Создание CrimePagerActivity
213
Представ- ление
Модель
Контроллер
Рис. 11.2. Диаграмма макета CrimePagerActivity
В этой главе нам предстоит:
создать класс
CrimePagerActivity
;
определить иерархию представлений, состоящую из
ViewPager
;
связать экземпляр
ViewPager с его адаптером
CrimePagerActivity
;
изменить метод
CrimeListFragment.onListItemClick(…)
так, чтобы он запускал
CrimePagerActivity вместо
CrimeActivity
Создание CrimePagerActivity
Класс
CrimePagerActivity будет субклассом
FragmentActivity
. Он создает и управ- ляет экземпляром
ViewPager
Создайте новый класс с именем
CrimePagerActivity
. Назначьте его суперклассом
FragmentActivity
. В методе onCreate(Bundle)
просто включите сквозной вызов суперкласса — создание экземпляра
ViewPager будет рассмотрено чуть позднее.
Листинг 11.1. Создание ViewPager (CrimePagerActivity.java)
public class CrimePagerActivity extends FragmentActivity {
private ViewPager mViewPager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
214Глава 11. ViewPager
Формирование макетов представлений в кодеВ этой книге макеты представлений определяются исключительно в XML-файлах макетов. Обычно это хорошая идея, но в Android нет ничего, что бы заставляло вас определять макеты именно таким образом. Иерархия представлений в этой главе проста — она состоит из единственного представления. По этой причине вместо файла XML мы определим иерархию в программном коде. Так как в
макете исполь- зуется всего одно представление, эта задача будет решаться относительно просто.
Создание представления не требует никакого волшебства; просто вызовите его конструктор. К сожалению, полностью изолироваться от XML вам не удастся.
Для некоторых компонентов все равно понадобятся идентификаторы ресурсов;
ViewPager входит в их число.
FragmentManager требует, чтобы любое представление, использованное как контейнер фрагмента, обязательно имело идентификатор.
ViewPager является контейнером фрагмента, поэтому ему необходимо присвоить идентификатор.
С учетом этого мы должны:
создать идентификатор ресурса для
ViewPager
;
создать экземпляр
ViewPager и присвоить его mViewPager
;
настроить его, присвоив ему идентификатор ресурса;
назначить
ViewPager представлением содержимого активности.
Автономные идентификаторы ресурсовОпределение автономного идентификатора ресурса отчасти напоминает опреде- ление строкового ресурса: вы создаете элемент в файле XML из каталога res/values
Создайте новый файл ресурсов Android в формате XML с именем res/values/ids.xml для хранения идентификаторов и добавьте в него идентификатор с именем viewPager
Листинг 11.2. Создание автономного идентификатора ресурса (res/values/ids.xml)
После того как идентификатор будет создан, можно переходить к созданию и ото- бражению
ViewPager
. Создайте экземпляр
ViewPager и назначьте его представлением содержимого.
Листинг 11.3. Программное создание представления содержимого (CrimePagerActivity.java)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewPager); setContentView(mViewPager);}
Формирование макетов представлений в коде
215
Класс
ViewPager находится в библиотеке поддержки. В отличие от
Fragment класс
ViewPager доступен только в библиотеке поддержки; в последующих SDK не су- ществует «стандартного» класса
ViewPager
ViewPager и PagerAdapter
Класс
ViewPager в чем-то похож на
AdapterView
(суперкласс
ListView
). Чтобы класс
AdapterView мог выдавать представления, ему необходим экземпляр
Adapter
. Классу
ViewPager необходим адаптер
PagerAdapter
Однако взаимодействие между
ViewPager и
PagerAdapter намного сложнее вза- имодействия между
AdapterView и
Adapter
. К счастью, мы можем использовать
FragmentStatePagerAdapter
— субкласс
PagerAdapter
, который берет на себя многие технические подробности.
FragmentStatePagerAdapter сводит взаимодействие к двум простым методам: get-
Count()
и getItem(int)
. При вызове метода getItem(int)
для позиции в массиве преступлений следует вернуть объект
CrimeFragment
, настроенный для вывода информации объекта в заданной позиции.
В классе
CrimePagerActivity добавьте следующий код для назначения
PagerAdapter класса
ViewPager и реализации его методов getCount()
и getItem(int)
Листинг 11.4. Назначение PagerAdapter (CrimePagerActivity.java)
public class CrimePagerActivity extends FragmentActivity {
private ViewPager mViewPager;
private ArrayList mCrimes;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewPager = new ViewPager(this);
mViewPager.setId(R.id.viewPager);
setContentView(mViewPager);
mCrimes = CrimeLab.get(this).getCrimes();
FragmentManager fm = getSupportFragmentManager();
mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) {
@Override
public int getCount() {
return mCrimes.size();
}
@Override
public Fragment getItem(int pos) {
Crime crime = mCrimes.get(pos);
return CrimeFragment.newInstance(crime.getId());
}
});
}
}
216Глава 11. ViewPager
Пройдемся по этому коду. В первой строке мы получаем от
CrimeLab набор дан- ных — контейнер
ArrayList объектов
Crime
. Затем мы получаем экземпляр
Frag- mentManager для активности.
На следующем шаге адаптером назначается безымянный экземпляр
Fragment-
StatePagerAdapter
. Для создания
FragmentStatePagerAdapter необходим объект
FragmentManager
. Не забывайте, что
FragmentStatePagerAdapter
— ваш агент, управляющий взаимодействием с
ViewPager
. Чтобы ваш агент мог выполнить свою работу с фрагментами, возвращаемыми в getItem(int)
, он должен быть способен добавить их в активность. Вот почему ему необходим экземпляр
Frag- mentManager
(Что именно делает агент? Вкратце он добавляет возвращаемые фрагменты в ак- тивность и помогает
ViewPager идентифицировать представления фрагментов для их правильного размещения. Более подробная информация приведена в разделе
«Для любознательных» в конце главы).
Два метода
PagerAdapter весьма просты. Метод getCount()
возвращает текущее ко- личество элементов в списке. Все существенное происходит в методе getItem(int)
Он получает экземпляр
Crime для заданной позиции в наборе данных, после чего использует его идентификатор для создания и возвращения правильно настроен- ного экземпляра
CrimeFragment
Интеграция CrimePagerActivityТеперь можно переходить к устранению класса
CrimeActivity и замене его классом
CrimePagerActivity
Для начала щелчок на элементе списка в
CrimeListFragment должен запускать экземпляр
CrimePagerActivity вместо
CrimeActivity
Вернитесь к файлу
CrimeListFragment.java и измените метод onListItemClick(…)
так, чтобы он запускал
CrimePagerActivity
Листинг 11.5. Запуск активности (CrimeListFragment.java)
@Override public void onListItemClick(ListView l, View v, int position, long id) {
// Получение объекта Crime от адаптера
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
// Запуск CrimeActivity Intent i = new Intent(getActivity(), CrimeActivity.class); // Запуск CrimePagerActivity с объектом Сrime Intent i = new Intent(getActivity(), CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId());
startActivity(i);
}
Также необходимо добавить
CrimePagerActivity в манифест, чтобы ОС могла за- пустить эту активность.
Пока манифест будет открыт, заодно удалите объявление
CrimeActivity
Интеграция CrimePagerActivity
217Листинг 11.6. Добавление CrimePagerActivity в манифест (AndroidManifest.xml)
android:name=".CrimeActivity"
android:label="@string/app_name" >
android:label="@string/app_name">
Наконец, чтобы не загромождать проект, удалите
CrimeActivity.java на панели
Package
Explorer
Запустите приложение CriminalIntent. Нажмите на строке
Crime
#0
, чтобы про- смотреть подробную информацию. Проведите по экрану влево или вправо, чтобы просмотреть другие элементы списка. Обратите внимание: переключение страниц происходит плавно и без задержек. По умолчанию
ViewPager загружает элемент, находящийся на экране, а также по одному соседнему элементу в каждом направле- нии, чтобы отклик на жест прокрутки был немедленным. Количество загружаемых соседних страниц можно настроить вызовом setOffscreenPageLimit(int)
Однако с
ViewPager еще не все идеально. Вернитесь к списку при помощи кнопки
Back и нажмите на другом элементе. Вы снова увидите информацию первого эле- мента — вместо того, который был запрошен.
По умолчанию
ViewPager отображает в своем экземпляре
PagerAdapter первый элемент. Чтобы вместо него отображался элемент, выбранный пользователем, на- значьте текущим элементом
ViewPager элемент с указанным индексом.
В конце
CrimePagerActivity.onCreate(…)
найдите индекс отображаемого преступле- ния; для этого переберите и проверьте идентификаторы всех преступлений. Когда вы найдете экземпляр
Crime
, у которого поле mId совпадает с crimeId в дополнении интента, измените текущий элемент по индексу найденного объекта
Crime
Листинг 11.7. Назначение исходного элемента (CrimePagerActivity.java)
public class CrimePagerActivity extends FragmentActivity {
@Override public void onCreate(Bundle savedInstanceState) {
FragmentManager fm = getSupportFragmentManager();
mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) {
});
продолжение
218Глава 11. ViewPager
Листинг 11.7 (продолжение)
UUID crimeId = (UUID)getIntent() .getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID); for (int i = 0; i < mCrimes.size(); i++) { if (mCrimes.get(i).getId().equals(crimeId)) { mViewPager.setCurrentItem(i); break; } } }
}
Запустите приложение CriminalIntent. При выборе любого элемента списка должна отображаться подробная информация правильного объекта
Crime
Также в приложение можно добавить еще одну функцию:
заменить заголовок активности, выводимый на панели действий (строки заголовка на старых устрой- ствах), кратким описанием текущего объекта
Crime
. Для этого следует реализовать интерфейс
ViewPager.OnPageChangeListener
Листинг 11.8. Добавление OnPageListener (CrimePagerActivity.java)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewPager = new ViewPager(this);
mViewPager.setId(R.id.viewPager);
setContentView(mViewPager);
mCrimes = CrimeLab.get(this).getCrimes();
FragmentManager fm = getSupportFragmentManager();
mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) {
});
mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { public void onPageScrollStateChanged(int state) { } public void onPageScrolled(int pos, float posOffset, int posOffsetPixels) { } public void onPageSelected(int pos) { Crime crime = mCrimes.get(pos); if (crime.getTitle() != null) { setTitle(crime.getTitle()); } } });}
Метод onPageChangeListener используется для обнаружения изменений в странице, которая в настоящий момент отображается экземпляром
ViewPager
. При изменении страницы заголовку
CrimePagerActivity задается краткое описание
Crime
Нас интересует лишь то, какая страница является текущей, поэтому мы реализуем метод onPageSelected(…)
. Метод onPageScrolled(…)
сообщает, где будет находиться
Интеграция CrimePagerActivity
219страница, а метод onPageScrollStateChanged(…)
— находится ли анимация стра- ницы в процессе активного перетаскивания, перехода в устойчивое состояние или в простое.
Запустите приложение CriminalIntent и убедитесь в том, что при каждом жесте прокрутки заголовок активности заполняется значением поля mTitle текущего объекта
Crime
. Вот и все! Теперь наш экземпляр
ViewPager полностью готов к работе.
FragmentStatePagerAdapter и FragmentPagerAdapterСуществует еще один тип
PagerAdapter
, который можно использовать в приложе- ниях; он называется
FragmentPagerAdapter
FragmentPagerAdapter используется точно так же, как
FragmentStatePagerAdapter
, и отличается от него только способом выгрузки неиспользуемых фрагментов.
Fragment для Item1текущий фрагмент
Переход на страницу вправо
Представ- ление
Представ- ление
Представ- ление
Представ- ление
Текущий фрагмент
Fragment для Item2Fragment для Item3Fragment для Item1Fragment для Item2Fragment для Item3Представ- ление
Рис. 11.3. Управление фрагментами FragmentStatePagerAdapter
При использовании класса
FragmentStatePagerAdapter неиспользуемый фрагмент уничтожается. Происходит закрепление транзакции для полного удаления фраг- мента из объекта
FragmentManager активности. Наличие «состояния» у
Fragment-
StatePagerAdapter определяется тем фактом, что экземпляр при уничтожении сохраняет объект
Bundle вашего фрагмента в методе onSaveInstanceState(Bundle)
Когда пользователь возвращается обратно, новый фрагмент восстанавливается по состоянию этого экземпляра.
С другой стороны,
FragmentPagerAdapter ничего подобного не делает.
Когда фрагмент становится ненужным,
FragmentPagerAdapter вызывает для транзакции detach(Fragment)
вместо remove(Fragment)
. Представление фрагмента при этом