Глава 12. Диалоговые окна
Листинг 12.7 (продолжение)
calendar.setTime(mDate);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
View v = getActivity().getLayoutInflater()
.inflate(R.layout.dialog_date, null);
DatePicker datePicker = (DatePicker)v.findViewById(R.id.dialog_date_datePicker);
datePicker.init(year, month, day, new OnDateChangedListener() {
public void onDateChanged(DatePicker view, int year, int month, int day) {
// Преобразование года, месяца и дня в объект Date
mDate = new GregorianCalendar(year, month, day).getTime();
// обновление аргумента для сохранения
// выбранного значения при повороте
getArguments().putSerializable(EXTRA_DATE, mDate);
}
});
}
При инициализации
DatePicker мы также назначаем ему слушателя
OnDat- eChangedListener
. Когда пользователь изменяет дату в
DatePicker
, мы обновляем
Date в соответствии с внесенными изменениями. В следующем разделе объект
Date будет возвращаться
CrimeFragment
В конце onDateChanged(…)
объект
Date записывается обратно в аргументы фраг- мента. Это делается для того, чтобы сохранить значение mDate в случае поворота.
Если в момент поворота устройства
DatePickerFragment находится на экране,
Frag- mentManager уничтожает текущий экземпляр и создает новый. При создании нового экземпляра
FragmentManager вызывает для него onCreateDialog(…)
, и экземпляр получает сохраненную дату из аргументов. Это более простой способ сохранения значения, чем сохранение состояния в onSaveInstanceState(…)
(У читателей, имеющих больше опыта работы с фрагментами, может возникнуть вопрос: почему бы не сохранить
DatePickerFragment
? Сохранение фрагментов — отличный способ обработки поворотов, который будет более подробно рассма- триваться в главе 14. К сожалению, в настоящее время реализация
DialogFragment содержит ошибку, из-за которой сохраненные экземпляры работают некорректно, так что сохранение
DatePickerFragment нельзя признать хорошим решением.)
Сейчас
CrimeFragment успешно сообщает
DatePickerFragment
, какую дату следует отобразить. Вы можете запустить приложение CriminalIntent и убедиться в том, что все работает так же, как прежде.
Возвращение данных CrimeFragment
Чтобы экземпляр
CrimeFragment получал данные от
DatePickerFragment
, нам не- обходимо каким-то образом отслеживать отношения между двумя фрагментами.
Создание DialogFragment
233
С активностями вы вызываете startActivityForResult(…)
, а
ActivityManager от- слеживает отношения между родительской и дочерней активностью. Когда дочерняя активность прекращает существование,
ActivityManager знает, какая активность должна получить результат.
Назначение целевого фрагмента
Для создания аналогичной связи можно назначить
CrimeFragment
целевым фрагмен-
том
(target fragment) для
DatePickerFragment
. Для этого вызывается следующий метод
Fragment
:
public void setTargetFragment(Fragment fragment, int requestCode)
Метод получает фрагмент, который станет целевым, и код запроса, аналогичный передаваемому startActivityForResult(…)
. По коду запроса целевой фрагмент позднее может определить, какой фрагмент возвращает информацию.
FragmentManager сохраняет целевой фрагмент и код запроса. Чтобы получить их, вызовите getTargetFragment()
и getTargetRequestCode()
для фрагмента, назна- чившего целевой фрагмент.
В файле
CrimeFragment.java создайте константу для кода запроса, а затем назначьте
CrimeFragment целевым фрагментом экземпляра
DatePickerFragment
Листинг 12.8. Назначение целевого фрагмента (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";
private static final int REQUEST_DATE = 0;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
mDateButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
FragmentManager fm = getActivity()
.getSupportFragmentManager();
DatePickerFragment dialog = DatePickerFragment
.newInstance(mCrime.getDate());
dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
dialog.show(fm, DIALOG_DATE);
}
});
return v;
}
}
234Глава 12. Диалоговые окна
Передача данных целевому фрагментуИтак, связь между
CrimeFragment и
DatePickerFragment создана, и теперь нужно вернуть дату
CrimeFragment
. Дата будет включена в объект
Intent как дополнение.
Какой метод будет использоваться для передачи интента целевому фрагменту?
Как ни странно,
DatePickerFragment передаст его при вызове
CrimeFragment.
onActivityResult(int,
int,
Intent)
Метод
Activity.onActivityResult(…)
вызывается
ActivityManager для родительской активности при уничтожении дочерней активности. При работе с активностями вы не вызываете
Activity.onActivityResult(…)
самостоятельно; это делает
Activity-
Manager
. После того как активность получит вызов, экземпляр
FragmentManager ак- тивности вызывает
Fragment.onActivityResult(…)
для соответствующего фрагмента.
Если хостом двух фрагментов является одна активность, то для возвращения дан- ных можно воспользоваться методом
Fragment.onActivityResult(…)
и вызывать его непосредственно для целевого фрагмента. Он содержит все необходимое:
код запроса, соответствующий коду, переданному setTargetFragment(…)
, по которому целевой фрагмент узнает, кто возвращает результат;
код результата для определения выполняемого действия;
экземпляр
Intent
, который может содержать дополнительные данные.
В классе
DatePickerFragment создайте закрытый метод, который создает интент,
помещает в него дату как дополнение, а затем вызывает
CrimeFragment.onActivi- tyResult(…)
. В onCreateDialog(…)
замените параметр null вызова setPositiveBut- ton(…)
реализацией
DialogInterface.OnClickListener
, которая вызывает закрытый метод и передает код результата.
Листинг 12.9. Обратный вызов целевого фрагмента (DatePickerFragment.java)
private void sendResult(int resultCode) { if (getTargetFragment() == null) return; Intent i = new Intent(); i.putExtra(EXTRA_DATE, mDate); getTargetFragment() .onActivityResult(getTargetRequestCode(), resultCode, i);}@Override public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setView(v)
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton( android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) {
Создание DialogFragment
235
sendResult(Activity.RESULT_OK);
}
})
.create();
}
В классе
CrimeFragment переопределите метод onActivityResult(…)
, чтобы он воз- вращал дополнение, задавал дату в
Crime и обновлял текст кнопки даты.
Листинг 12.10. Реакция на получение данных от диалогового окна (CrimeFragment.java)
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) return;
if (requestCode == REQUEST_DATE) {
Date date = (Date)data
.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
mCrime.setDate(date);
mDateButton.setText(mCrime.getDate().toString());
}
}
Код, задающий текст кнопки, идентичен коду из onCreateView(…)
. Чтобы избежать задания текста в двух местах, мы инкапсулируем этот код в закрытом методе up- dateDate()
, а затем вызовем его в onCreateView(…)
и onActivityResult(…)
Листинг 12.11. Выделение кода в метод updateDate() (CrimeFragment.java)
public class CrimeFragment extends Fragment {
public void updateDate() {
mDateButton.setText(mCrime.getDate().toString());
}
@Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_crime, parent, false);
mDateButton = (Button)v.findViewById(R.id.crime_date);
mDateButton.setText(mCrime.getDate().toString());
updateDate();
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) return;
if (requestCode == REQUEST_DATE) {
Date date = (Date)data
.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
mCrime.setDate(date);
mDateButton.setText(mCrime.getDate().toString());
updateDate();
}
}
}
236
Глава 12. Диалоговые окна
Круг замкнулся — данные передаются туда и обратно.
Запустите приложение CriminalIntent и убедитесь в том, что вы действительно можете управлять датой. Измените дату
Crime
; новая дата должна появиться в пред- ставлении
CrimeFragment
. Вернитесь к списку преступлений и проверьте дату
Crime и убедитесь в том, что уровень модели действительно обновлен.
Больше гибкости в представлении DialogFragment
Использование onActivityResult(…)
для возвращения данных целевому фрагменту особенно удобно, когда ваше приложение получает много данных от пользователя и нуждается в большем пространстве для их ввода. При этом приложение должно хорошо работать на телефонах и планшетах.
На экране телефона свободного места не так много, поэтому вы, скорее всего, ис- пользуете для ввода данных активность с полноэкранным фрагментом. Дочерняя активность будет запускаться вызовом startActivityForResult()
из фрагмента родительской активности. При уничтожении дочерней активности родительская активность будет получать вызов onActivityResult(…)
, который будет перенаправ- ляться фрагменту, запустившему дочернюю активность.
Фрагмент B
Фрагмент A
Фрагмент B
Фрагмент A
Рис. 12.8. Взаимодействие между активностями на телефонах
Создание DialogFragment
237На планшетах, где экранного пространства больше, часто бывает лучше отобразить
DialogFragment для ввода тех же данных. В таком случае вы задаете целевой фраг- мент и вызываете show(…)
для фрагмента диалогового окна. При закрытии фрагмент диалогового окна вызывает для своей цели onActivityResult(…)
Фрагмент B (отображается как диалоговое окно)
Фрагмент A
Фрагмент A
Фрагмент B (отображается как диалоговое окно)
Рис. 12.9. Взаимодействие между фрагментами на планшетахМетод onActivityResult(…)
фрагмента будет вызываться всегда, независимо от того, запустил ли фрагмент активность или отобразил диалоговое окно. Следова- тельно, мы можем использовать один код для разных вариантов представления информации.
Когда один код используется и для полноэкранного, и для диалогового фрагмента, для подготовки вывода в обоих случаях вместо onCreateDialog(…)
можно пере- определить
DialogFragment.onCreateView(…)
238
Глава 12. Диалоговые окна
Упражнение. Новые диалоговые окна
В качестве несложного упражнения напишите еще один диалоговый фраг- мент
TimePickerFragment для выбора времени преступления. Используйте вид- жет
TimePicker
, добавьте в
CrimeFragment еще одну кнопку для отображения
TimePickerFragment
Если вам захочется усложнить задачу, попробуйте ограничиться однокнопочным интерфейсом. Диалоговое окно, открываемое кнопкой, должно предлагать поль- зователю выбрать между изменением времени и изменением даты. После выбора на экране должно появляться второе диалоговое окно.
Воспроизведение звука
и MediaPlayer
В следующих трех главах мы оставим CriminalIntent в покое и построим другое приложение. Оно будет воспроизводить исторические аудиофайлы с использова- нием класса
MediaPlayer
Рис. 13.1. Привет, Луна!
13
240Глава 13. Воспроизведение звука и MediaPlayer
MediaPlayer
— класс Android для воспроизведения аудио- и видеоданных. Он может воспроизводить данные из разных источников (например, локальных файлов или файлов, загружаемых из Интернета) и в разных форматах (WAV, MP3, Ogg Vorbis,
MPEG-4, 3GPP и т. д.).
Создайте новый проект с именем HelloMoon. В первом диалоговом окно выберите тему приложения
Holo
Dark
Рис. 13.2. Создание приложения HelloMoon с темой Holo Dark
Щелкните на кнопке
Next
. У
приложения не будет собственного значка лаунчера, и оно использует пустой шаблон активности. Прикажите шаблону создать актив- ность с именем
HelloMoonActivity
Добавление ресурсовДля приложения HelloMoon необходимы файлы (графический и звуковой) из ар- хива решений этой книги (
https://www.bignerdranch.com/solutions/AndroidProgramming.zip
).
Найдите в архиве решений следующие файлы:
13_Audio/HelloMoon/res/drawable-mdpi/armstrong_on_moon.jpg
13_Audio/HelloMoon/res/raw/one_small_step.wav
Добавление ресурсов
241Для этого простого приложения мы создали один файл armstrong_on_moon.jpg для экранов средней плотности (160 dpi), которую Android считает минимальным общим требованием. Скопируйте файл armstrong_on_moon.jpg в каталог drawable-mdpi
Аудиофайл будет находиться в каталоге res/raw
. Каталог raw предназначен для хранения любых ресурсов, которые не требуют специальной обработки системой построения приложений Android.
Каталог res/raw не создается для проекта по умолчанию, поэтому его придется добавить вручную. (Щелкните правой кнопкой мыши на каталоге res и выберите команду
New
Folder
.) Скопируйте файл one_small_step.wav в новый каталог.
(Заодно скопируйте в каталог res/raw/
файл
13_Audio/HelloMoon/res/raw/apollo_17_stroll.
mpg
. Этот файл будет использоваться для воспроизведения видео в упражнении в конце этой главы.)
Откройте файл res/values/strings.xml и добавьте строки, необходимые для приложения
HelloMoon:
Листинг 13.1. Добавление строк (strings.xml)
HelloMoon
Hello world!
Settings
Play
Stop
Neil Armstrong stepping
onto the moon
(Почему в приложении HelloMoon вместо значков на кнопках используются строки
«Play» и «Stop»? В главе 15 мы займемся локализацией этого приложения, а со значками это будет не так интересно.)
Итак, необходимые ресурсы готовы; можно переходить к планированию общей архитектуры HelloMoon.
В приложении HelloMoon будет использоваться только одна активность
Hello-
MoonActivity
, являющаяся хостом для фрагмента
HelloMoonFragment
AudioPlayer
— класс, который мы напишем для инкапсуляции
MediaPlayer
. Вообще говоря, инкапсулировать
MediaPlayer не обязательно;
HelloMoonFragment может взаимодействие с
MediaPlayer напрямую. Тем не менее такая архитектура делает код более стройным и ослабляет логические привязки.
Но прежде чем создавать класс
AudioPlayer
, мы сначала подготовим остальные части приложения. К этому моменту вы уже должны достаточно хорошо представлять себе следующие фазы:
определение
макета фрагмента;
создание класса фрагмента;
изменение активности и ее макета для выполнения функций хоста фрагмента.
242
Глава 13. Воспроизведение звука и MediaPlayer
Модель
Контроллер
Представление
Рис. 13.3. Диаграмма объектов HelloMoon
Определение макета HelloMoonFragment
Создайте новый XML-файл макета Android с именем fragment_hello_moon.xml
. На- значьте его корневым элементом
TableLayout
Файл fragment_hello_moon.xml следует заполнять по рис. 13.4.
Рис. 13.4. Диаграмма макета приложения HelloMoon
Определение макета HelloMoonFragment
243TableLayout работает почти так же, как
LinearLayout
. Вместо вложения
Linear-
Layout для упорядочения виджетов можно воспользоваться виджетом
TableRow
Комбинация
TableLayout и
TableRow упрощает организацию представлений в ак- куратные столбцы.
Почему
ImageView не находится в
TableRow
? Потомки
TableRow рассматриваются как «ячейки» таблицы. Мы хотим, чтобы виджет
ImageView занимал весь экран.
Если бы он был потомком
TableRow
, то виджет
TableLayout постарается развернуть остальные ячейки в этом столбце на весь экран. А если сделать его прямым потомком
TableLayout
, он сможет делать все, что хочет, тогда как кнопки
Button останутся в своих столбцах равной ширины, расположенных по соседству друг с другом.
Учтите, что виджет
TableRow не обязан объявлять атрибуты ширины и высоты. Он использует ширину и высоту, а также все остальные атрибуты
TableLayout
. С другой стороны, вложенный виджет
LinearLayout способен обеспечить большую гибкость в определении внешнего вида ваших виджетов.
Взгляните на макет в графическом конструкторе. Какой цвет фона вы видите?
В мастере для приложения HelloMoon была выбрана тема Holo Dark. Однако на момент написания книги мастер игнорировал ваш выбор и всегда назначал светлую тему. Давайте посмотрим, как решить эту проблему. (Если вы видите темный фон, значит, мастер был исправлен и вам не
придется вручную сбрасывать тему при- ложения, как описано в следующем разделе.)
Сброс темы приложенияТема приложения объявляется в элементе application манифеста:
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
Атрибут android:theme не обязателен; если тема не объявлена, используется кон- фигурация устройства по умолчанию.
Мы видим, что заданное значение представляет собой ссылку на ресурс —
@style/
AppTheme
. На панели
Package
Explorer найдите и откройте файл res/values/styles.xml
В элементе style с именем
AppBaseTheme замените значение parent на android:Theme
Листинг 13.2. Изменение файла стилей по умолчанию (res/values/styles.xml)
В каталоге res также находятся два каталога values с уточненными именами; каждый содержит файл styles.xml
. Уточнения в именах каталогов обозначают уровни API.
Данные в файле res/values-11/styles.xml будут использоваться для API уровней 11–13, а значения в файле res/values-14/styles.xml
— для API уровня 14 и выше.