Глава 11. ViewPager уничтожается, но экземпляр фрагмента продолжает существовать в FragmentMan- ager . Таким образом, фрагменты, созданные FragmentPagerAdapter , никогда не уничтожаются. Fragment для Item1 Текущий фрагмент Переход на страницу вправо Представ- ление Представ- ление Представ- ление Представ- ление Представ- ление Текущий фрагмент Fragment для Item2 Fragment для Item3 Fragment для Item1 Fragment для Item2 Fragment для Item3 Рис. 11.4. Управление фрагментами FragmentPagerAdapter Выбор используемого адаптера зависит от приложения. Как правило, Fragment- StatePagerAdapter более экономно расходует память. Приложение CriminalIntent выводит список, который со временем может стать достаточно длинным, причем к каждому элементу списка может прилагаться фотография. Хранить всю эту инфор- мацию в памяти нежелательно, поэтому мы используем FragmentStatePagerAdapter С другой стороны, если интерфейс содержит небольшое фиксированное количество фрагментов, использование FragmentPagerAdapter безопасно и уместно. Самый характерный пример такого рода — интерфейс с вкладками. Некоторые детализи- рованные представления не помещаются на одном экране, поэтому отображаемая информация распределяется между несколькими вкладками. Добавление ViewPager с перебором вкладок делает этот интерфейс интуитивным. Хранение фрагментов в памяти упрощает управление кодом контроллера, а поскольку этот стиль интер- фейса обычно использует всего два или три фрагмента на активность, проблемы с нехваткой памяти крайне маловероятны. Для любознательных: как работает ViewPager Классы ViewPager и PagerAdapter незаметно выполняют большую часть рутинной работы. В этом разделе приведена более подробная информация о том, что при этом происходит. Для любознательных: как работает ViewPager 221Пара предупреждений, прежде чем мы перейдем к обсуждению: во-первых, согласно документации класс ViewPager все еще находится в процессе разработки, так что интерфейсы могут измениться в будущем. Во-вторых, в большинстве случаев по- нимать все технические подробности не обязательно. Но если вы захотите реализовать интерфейс PagerAdapter самостоятельно, вы должны знать, чем отношения ViewPager - PagerAdapter отличаются от обычных отношений AdapterView - Adapter Почему ViewPager , а не AdapterView ? У AdapterView имеется субкласс Gallery с по- хожим поведением. Почему бы не использовать его? Использование AdapterView в данном случае потребует большого объема работы, потому что вы не сможете использовать существующие экземпляры Fragment Adapter ожидает, что вы сможете предоставить View мгновенно. Но когда будет создано представление вашего фрагмента, решает FragmentManager , а не вы. Таким образом, когда Gallery обратиться к Adapter за представлением вашего фрагмента, вы не сможете создать фрагмент и немедленно выдать его представление. Именно по этой причине и существует класс ViewPager . Вместо Adapter он ис- пользует класс с именем PagerAdapter . Этот класс сложнее Adapter , потому что он выполняет больший объем работы по управлению представлениями. Ниже кратко перечислены основные различия. Вместо метода getView(…) , возвращающего представление, PagerAdapter содержит следующие методы: public Object instantiateItem(ViewGroup container, int position) public void destroyItem(ViewGroup container, int position, Object object) public abstract boolean isViewFromObject(View view, Object object) Метод pagerAdapter.instantiateItem(ViewGroup, int) приказывает адаптеру создать представление элемента списка для заданной позиции и добавить его в контейнер ViewGroup ; метод destroyItem(ViewGroup, int, Object) приказывает уничтожить этот элемент. Обратите внимание: метод instantiateItem(ViewGroup, int) не приказывает создать представление немедленно. PagerAdapter может создать представление в будущем. После того как представление было создано, ViewPager в какой-то момент заме- чает его. Чтобы понять, к какому элементу списка оно относится, ViewPager вы- зывает метод isViewFromObject(View, Object) . Параметр Object содержит объект, полученный при вызове instantiateItem(ViewGroup, int) . Таким образом, если ViewPager вызывает instantiateItem(ViewGroup, 5) и получает объект A , вызов isViewFromObject(View, A) должен вернуть true , если переданный экземпляр View относится к элементу 5, и false в противном случае. Этот процесс достаточно сложен для ViewPager , но не для класса PagerAdapter , ко- торый должен уметь только создавать представления, уничтожать представления и определять, к какому объекту относится представление. Менее жесткие требо- вания позволяют реализации PagerAdapter создавать и добавлять новый фрагмент в instantiateItem(ViewGroup, int) и возвращать фрагмент как отслеживаемый экземпляр Object . При этом isViewFromObject(View, Object) выглядит примерно так: 222 Глава 11. ViewPager @Override public boolean isViewFromObject(View view, Object object) { return ((Fragment)object).getView() == view; } Реализовать переопределения PagerAdapter каждый раз, когда потребуется ис- пользовать ViewPager , было бы слишком утомительно. Хорошо, что у нас есть FragmentPagerAdapter и FragmentStatePagerAdapter
Диалоговые окна Диалоговые окна требуют внимания пользователя и ввода данных. Обычно они ис- пользуются для принятия решений или отображения важной информации. В этой главе мы добавим диалоговое окно, в котором пользователь может изменить дату преступления. При нажатии кнопки даты в CrimeFragment открывается диалоговое окно, пока- занное на рис. 12.1. Рис. 12.1. Диалоговое окно для выбора даты 12
224Глава 12. Диалоговые окна Диалоговое окно на рис. 12.1 является экземпляром AlertDialog — субкласса Dia- log . Именно этот многоцелевой субкласс Dialog вы будете чаще всего использовать в своих программах. (У AlertDialog существует субкласс DatePickerDialog , который вроде бы идеально подходит для наших целей. Однако на момент написания книги реализация Dat- ePickerDialog работала с ошибками; использовать AlertDialog проще, чем обходить эти ошибки.) Экземпляр AlertDialog на рис. 12.1 упакован в экземпляр DialogFragment — субкласса Fragment . Вообще говоря, экземпляр AlertDialog может отображаться и без DialogFragment , но Android так поступать не рекомендует. Управление диалоговым окном из FragmentManager открывает больше возможностей для его отображения. Кроме того, «минимальный» экземпляр AlertDialog исчезнет при повороте устрой- ства. С другой стороны, если экземпляр AlertDialog упакован в фрагмент, после поворота диалоговое окно будет создано заново и появится на экране. Для приложения CriminalIntent мы создадим субкласс DialogFragment с именем DatePickerFragment . В коде DatePickerFragment создается и настраивается экземпляр AlertDialog , отображающий виджет DatePicker . В качестве хоста DatePickerFrag- ment используется экземпляр CrimePagerActivity На рис. 12.2 изображена схема этих отношений. Представление Модель Контроллер Рис. 12.2. Диаграмма объектов для двух фрагментов с хостом CrimePagerActivity Создание DialogFragment 225 Наши первоочередные задачи: создание класса DatePickerFragment ; построение AlertDialog ; вывод диалогового окна на экран с использованием FragmentManager Позднее в этой главе мы подключим виджет DatePicker и организуем передачу необходимых данных между CrimeFragment и DatePickerFragment Прежде чем браться за работу, добавьте строковый ресурс (листинг 12.1). Листинг 12.1. Добавление строки для заголовка диалогового окна (values/strings.xml)
Solved? Crimes Date of crime:
Создание DialogFragment Создайте новый класс с именем DatePickerFragment и назначьте его суперклассом версию DialogFragment из библиотеки поддержки: android.support.v4.app.Dia- logFragment Класс DialogFragment содержит следующий метод: public Dialog onCreateDialog(Bundle savedInstanceState) Экземпляр FragmentManager активности-хоста вызывает этот метод в процессе вы- вода DialogFragment на экран. Добавьте в файл DatePickerFragment.java реализацию onCreateDialog(…) , которая создает AlertDialog с заголовком и одной кнопкой OK . (Виджет DatePicker мы добавим позднее.) Листинг 12.2. Создание DialogFragment (DatePickerFragment.java) public class DatePickerFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new AlertDialog.Builder(getActivity()) .setTitle(R.string.date_picker_title) .setPositiveButton(android.R.string.ok, null) .create(); } } В этой реализации используется класс AlertDialog.Builder , предоставляющий динамичный интерфейс для конструирования экземпляров AlertDialog Сначала мы передаем объект Context конструктору AlertDialog.Builder , который возвращает экземпляр AlertDialog.Builder
226 Глава 12. Диалоговые окна Затем вызываются два метода AlertDialog.Builder для настройки диалогового окна: public AlertDialog.Builder setTitle(int titleId) public AlertDialog.Builder setPositiveButton(int textId, DialogInterface.OnClickListener listener) Метод setPositiveButton(…) получает строковый ресурс и объект, реализующий DialogInterface.OnClickListener . В листинге 12.2 передается константа Android для кнопки OK и null вместо слушателя. Слушатель будет реализован позднее в этой главе. Положительная кнопка ( Positive ) нажимается пользователем для подтверждения информации в диалоговом окне. В AlertDialog также можно добавить еще две кнопки: отрицательную ( Negative ) и нейтральную ( Neutral ). Эти обозначения определяют позицию кнопок в диалоговом окне (если их несколько). На устрой- ствах Froyo и Gingerbread положительной является левая кнопка. На более новых устройствах порядок кнопок изменен, и положительной является правая кнопка). Построение диалогового окна завершается вызовом AlertDialog.Builder.create() , который возвращает настроенный экземпляр AlertDialog На этом возможности AlertDialog и AlertDialog.Builder не исчерпаны; подроб- ности достаточно хорошо изложены в документации разработчика. А пока давайте перейдем к механике вывода диалогового окна на экран. Отображение DialogFragment Как и все фрагменты, экземпляры DialogFragment находятся под управлением экземпляра FragmentManager активности-хоста. Для добавления экземпляра DialogFragment в FragmentManager и вывода его на экран используются следующие методы экземпляра фрагмента: public void show(FragmentManager manager, String tag) public void show(FragmentTransaction transaction, String tag) Строковый параметр однозначно идентифицирует DialogFragment в списка Frag- mentManager . Выбор версии (с FragmentManager или FragmentTransaction ) зависит только от вас — если передать FragmentManager , транзакция будет автоматически создана и закреплена. В нашем примере передается FragmentManager Добавьте в CrimeFragment константу для метки DatePickerFragment . Затем в методе onCreateView(…) удалите код, блокирующий кнопку даты, и назначьте слушателя View.OnClickListener , который отображает DatePickerFragment при нажатии кнопки даты. Листинг 12.3. Отображение DialogFragment (CrimeFragment.java) public class CrimeFragment extends Fragment { public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id"; private static final String DIALOG_DATE = "date";
Создание DialogFragment 227 @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { mDateButton = (Button)v.findViewById(R.id.crime_date); mDateButton.setText(mCrime.getDate().toString()); mDateButton.setEnabled(false); mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { FragmentManager fm = getActivity() .getSupportFragmentManager(); DatePickerFragment dialog = new DatePickerFragment(); dialog.show(fm, DIALOG_DATE); } }); mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved); return v; } } Запустите приложение CriminalIntent и нажмите кнопку даты, чтобы диалоговое окно появилось на экране. Кнопка OK закрывает диалоговое окно (рис. 12.3). Рис. 12.3. AlertDialog с заголовком и кнопкой 228 Глава 12. Диалоговые окна Назначение содержимого диалогового окна Далее мы включим в AlertDialog виджет DatePicker при помощи метода Alert- Dialog.Builder : public AlertDialog.Builder setView(View view) Метод настраивает диалоговое окно для отображения переданного объекта View между заголовком и кнопкой(-ами). В Package Explorer создайте новый файл макета с именем dialog_date.xml и назначьте его корневым элементом DatePicker . Макет будет состоять из одного объекта View ( DatePicker ), который мы заполним и передадим setView(…) Настройте макет DatePicker так, как показано на рис. 12.4. Рис. 12.4. Макет DatePicker (layout/dialog_date.xml) В методе DatePickerFragment.onCreateDialog(…) заполните представление и на- значьте его диалоговому окну. Листинг 12.4. Включение DatePicker в AlertDialog (DatePickerFragment.java) @Override public Dialog onCreateDialog(Bundle savedInstanceState) { View v = getActivity().getLayoutInflater() .inflate(R.layout.dialog_date, null); return new AlertDialog.Builder(getActivity()) .setView(v) .setTitle(R.string.date_picker_title) .setPositiveButton(android.R.string.ok, null) .create(); } Запустите приложение CriminalIntent. Нажмите кнопку даты и убедитесь в том, что в диалоговом окне теперь отображается DatePicker Почему мы возимся с определением и заполнением макета, когда объект DatePicker можно было бы создать в коде так, как показано ниже? @Override public Dialog onCreateDialog(Bundle savedInstanceState) { DatePicker dp = new DatePicker(getActivity()); return new AlertDialog.Builder(getActivity()) .setView(dp) .create(); }
Создание DialogFragment 229Рис. 12.5. AlertDialog с DatePicker Использование макета упрощает изменения в случае изменения его содержимого. Предположим, вы захотели, чтобы рядом с DatePicker в диалоговом окне отобра- жался виджет TimePicker . При использовании заполнения можно просто обновить файл макета, и новое представление появится на экране. Итак, наше диалоговое окно успешно отображается. В следующем разделе мы свяжем его с полем даты Crime и позаботимся о том, чтобы пользователь мог вводить данные. Передача данных между фрагментамиМы передавали данные между двумя активностями; мы передавали данные между двумя фрагментными активностями. Теперь нужно передать данные между дву- мя фрагментами, хостом которых является одна активность — CrimeFragment и DatePickerFragment (см. рис. 5.10). Чтобы передать дату преступления DatePickerFragment , мы напишем метод newInstance(Date) и сделаем объект Date аргументом фрагмента. Чтобы вернуть новую дату фрагменту CrimeFragment для обновления уровня модели и его собственного представления, мы упакуем ее как дополнение объекта Intent и передадим этот объект Intent в вызове CrimeFragment.onActivityResult(…) Вызов Fragment.onActivityResult(…) может показаться странным — с учетом того, что активность-хост не получает вызова Activity.onActivityResult(…) в этом взаимодействии. Но как будет показано позднее в этой главе, использование on- ActivityResult(…) для передачи данных от одного фрагмента к другому не только работает, но и улучшает гибкость отображения фрагмента диалогового окна. 230 Глава 12. Диалоговые окна Дата, выбранная пользователем Отображаемая дата Рис. 12.6. Взаимодействие между CrimeFragment и DatePickerFragment Рис. 12.7. Последовательность событий взаимодействия между CrimeFragment и DatePickerFragment Передача данных DatePickerFragment Чтобы получить данные в DatePickerFragment , мы сохраним дату в пакете аргумен- тов DatePickerFragment , где DatePickerFragment сможет обратиться к ней. Создание аргументов фрагмента и присваивание им значений обычно выполняет- ся в методе newInstance() , заменяющем конструктор фрагмента. Добавьте в файл DatePickerFragment.java метод newInstance(Date) Листинг 12.5. Добавление метода newInstance(Date) (DatePickerFragment.java) public class DatePickerFragment extends DialogFragment { public static final String EXTRA_DATE = "com.bignerdranch.android.criminalintent.date"; private Date mDate; public static DatePickerFragment newInstance(Date date) { Bundle args = new Bundle(); args.putSerializable(EXTRA_DATE, date);
Создание DialogFragment 231 DatePickerFragment fragment = new DatePickerFragment(); fragment.setArguments(args); return fragment; }} В классе CrimeFragment удалите вызов конструктора DatePickerFragment и замените его вызовом DatePickerFragment.newInstance(Date) Листинг 12.6. Добавление вызова newInstance() (CrimeFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { mDateButton = (Button)v.findViewById(R.id.crime_date); updateDate(); mDateButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { FragmentManager fm = getActivity() .getSupportFragmentManager(); D перейти в каталог файлов
|