Глава 13. Воспроизведение звука и MediaPlayer Откройте файл res/values-11/styles.xml и замените значение атрибута parent у AppBas- eTheme на android:Theme.Holo . Эта тема должна использоваться на всех устройствах с API 11 и выше, поэтому каталог res/values-14/ только мешает. Удалите его из про- екта HelloMoon. Сохраните файлы и снова просмотрите макет. Теперь он должен иметь темный фон, который хорошо сочетается с графическим файлом. Создание класса HelloMoonFragment Создайте новый класс с именем HelloMoonFragment и назначьте его суперклассом android.support.v4.app.Fragment Переопределите метод HelloMoonFragment.onCreateView(…) , чтобы заполнить только что определенный макет и получить ссылки на кнопки. Листинг 13.3. Исходная версия HelloMoonFragment (HelloMoonFragment.java) public class HelloMoonFragment extends Fragment { private Button mPlayButton; private Button mStopButton; @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_hello_moon, parent, false); mPlayButton = (Button)v.findViewById(R.id.hellomoon_playButton); mStopButton = (Button)v.findViewById(R.id.hellomoon_stopButton); return v; } } Использование фрагмента макета В приложении CriminalIntent хостинг фрагментов осуществлялся добавлением их в код активности. В приложении HelloMoon вместо этого будет использоваться фрагмент макета, при использовании которого разработчик задает класс фрагмента в элементе fragment Откройте файл activity_hello_moon.xml и замените его содержимое элементом frag- ment , приведенным в листинге 13.4. Листинг 13.4. Создание фрагмента макета (activity_hello_moon.xml)
android:id="@+id/helloMoonFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:name="com.bignerdranch.android.hellomoon.HelloMoonFragment">
Определение макета HelloMoonFragment 245Прежде чем вы сможете запустить код, необходимо внести еще одно изменение в код HelloMoonActivity . Измените суперкласс HelloMoonActivity — им должен быть класс FragmentActivity : Листинг 13.5. Преобразование HelloMoonActivity в FragmentActivity (HelloMoonActivity.java) public class HelloMoonActivity extends Activity FragmentActivity { /** Вызывается при исходном создании активности. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_hello_moon); } } Запустите приложение HelloMoon. На этот раз HelloMoonActivity становится хостом представления HelloMoonFragment Вот и все, что необходимо для хостинга фрагмента макета. Мы указали имя класса фрагмента в макете, он был добавлен в активность и выведен на экран. А вот что при этом происходило «за кулисами»: когда класс вызвал метод .setContentView(…) и заполнил макет activity_hello_moon.xml , он обнаружил элемент fragment . В этой точке FragmentManager создал экземпляр HelloMoonFragment и добавил его в список. Затем он вызвал для HelloMoonFragment метод onCreateView(…) и поместил представление, возвращенное этим методом, в место, подготовленное тегом fragment Выполнение Приостановка Остановка Создание (активность/фрагмент возвращается на передний план) (активность/фрагмент снова становится видимой) Уничтожение Запуск (завершение активности) (все методы вызываются в setContentView() для фрагментов макетов) Рис. 13.5. Жизненный цикл фрагмента макета Чем приходится расплачиваться за эту простоту? Вы теряете гибкость и широту возможностей, характерные для прямой работы с FragmentManager : 246 Глава 13. Воспроизведение звука и MediaPlayer Вы можете переопределять методы жизненного цикла фрагмента, чтобы реаги- ровать на события, но не можете управлять тем, когда эти методы вызываются. Вы не можете закреплять транзакции, которые удаляют, заменяют или отсо- единяют фрагмент макета. Приходится довольствоваться тем, что было сделано при создании активности. К фрагментам макетов нельзя присоединять аргументы. Присоединение аргу- ментов должно осуществляться после создания фрагмента и до его включения в FragmentManager . С фрагментами макетов все эти события вам недоступны. Однако в простом приложении или в статической части сложного приложения использование фрагмента макета может быть вполне разумным. Теперь, когда мы организовали хостинг HelloMoonFragment , обратимся к воспро- изведению аудио. Воспроизведение аудио Создайте в пакете com.bignerdranch.android.hellomoon новый класс с именем AudioPlayer . Оставьте его суперклассом java.lang.Object В файле AudioPlayer.java добавьте поле для хранения экземпляра MediaPlayer и методы для остановки и воспроизведения этого экземпляра. Листинг 13.6. Простой код воспроизведения аудио с использованием MediaPlayer (AudioPlayer.java) public class AudioPlayer { private MediaPlayer mPlayer; public void stop() { if (mPlayer != null) { mPlayer.release(); mPlayer = null; } } public void play(Context c) { mPlayer = MediaPlayer.create(c, R.raw.one_small_step); mPlayer.start(); } } В методе play(Context) вызывается метод MediaPlayer.create(Context, int) Объект Context нужен MediaPlayer для опознания идентификатора ресурса аудио- файла. (Также существуют другие методы MediaPlayer.create(…) , используемые при получении аудио из других источников, например из Интернета или по ло- кальному URI.) В методе AudioPlayer.stop() экземпляр MediaPlayer освобождается, а полю mPlayer присваивается null . Вызов MediaPlayer.release() уничтожает экземпляр.
Воспроизведение аудио 247 Уничтожение кажется слишком агрессивной интерпретацией «остановки», но для этого есть веские причины. Класс MediaPlayer удерживает аудиооборудование и другие системные ресурсы до вызова release() . Эти ресурсы совместно ис- пользуются всеми приложениями. Класс MediaPlayer включает метод stop() для перевода экземпляра MediaPlayer в остановленное состояние, из которого он может быть перезапущен. Однако при простом воспроизведении аудиоданных корректнее уничтожить экземпляр вызовом release() , а потом создать его заново. Простое правило: удерживайте ровно один экземпляр MediaPlayer и только на то время, в котором он что-то воспроизводит. Для соблюдения этого правила мы внесем пару изменений в play(Context) . Добавьте исходный вызов stop() и заставьте слушателя вызывать stop() при завершении воспроизведения. Листинг 13.7. Только один экземпляр (AudioPlayer.java) public void play(Context c) { stop(); mPlayer = MediaPlayer.create(c, R.raw.one_small_step); mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { public void onCompletion(MediaPlayer mp) { stop(); } }); mPlayer.start(); } } Вызов stop() в начале play(Context) предотвращает возможное создание несколь- ких экземпляров MediaPlayer , если пользователь щелкнет на кнопке Play повторно. Вызов stop() при завершении воспроизведения файла освобождает экземпляр MediaPlayer , как только он перестает использоваться. Также вызов AudioPlayer.stop() необходимо включить в HelloMoonFragment , чтобы экземпляр MediaPlayer не продолжал воспроизведение после уничтожения фраг- мента. В классе HelloMoonFragment переопределите метод onDestroy() и включите в него вызов AudioPlayer.stop() Листинг 13.8. Переопределение onDestroy() (HelloMoonFragment.java) @Override public void onDestroy() { super.onDestroy(); mPlayer.stop(); } }
248 Глава 13. Воспроизведение звука и MediaPlayer Класс MediaPlayer может продолжить воспроизведение после уничтожения Hel- loMoonFragment , потому что MediaPlayer работает в другом программном потоке (thread). Сейчас мы намеренно игнорируем этот многопоточный аспект HelloMoon. Управление потоками более подробно рассматривается в главе 26. Подключение кнопок воспроизведения и остановки Вернитесь к файлу HelloMoonFragment.java . Пора заняться воспроизведением аудио: создайте экземпляр класса AudioPlayer и назначьте слушателей для кнопок вос- произведения и остановки. Листинг 13.9. Подключение кнопки Play (HelloMoonFragment.java) public class HelloMoonFragment extends Fragment { private AudioPlayer mPlayer = new AudioPlayer(); private Button mPlayButton; private Button mStopButton; @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_hello_moon, parent, false); mPlayButton = (Button)v.findViewById(R.id.hellomoon_playButton); mPlayButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { mPlayer.play(getActivity()); } }); mStopButton = (Button)v.findViewById(R.id.hellomoon_stopButton); mStopButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { mPlayer.stop(); } }); return v; } } Запустите приложение HelloMoon, нажмите кнопку Play и насладитесь живой историей. В этой главе мы едва затронули тему использования MediaPlayer . За информацией о возможностях MediaPlayer обращайтесь к руководству Android «MediaPlayer Developer Guide» по адресу https://developer.android.com/guide/topics/media/mediaplayer.html Упражнение. Приостановка воспроизведения Предоставьте пользователю возможность приостановить воспроизведение аудио. Описания необходимых методов можно найти в справочном описании класса Me- diaPlayer
Упражнение. Воспроизведение видео в HelloMoon 249 Для любознательных: воспроизведение видео В том, что касается воспроизведения видео, можно выбирать из нескольких вари- антов. Можно использовать класс MediaPlayer , как мы только что сделали. Для этого достаточно подключить место для воспроизведения. Часто обновляемые изображения (как видео) в Android отображаются на виджете SurfaceView . Точнее, они отображаются на виджете Surface , хостом которого явля- ется SurfaceView . Чтобы получить доступ к Surface , следует получить экземпляр SurfaceHolder для SurfaceView . Эта тема более подробно рассматривается в главе 19. А пока достаточно знать, что подключение SurfaceHolder к MediaPlayer осущест- вляется вызовом MediaPlayer.setDisplay(SurfaceHolder) Часто для воспроизведения видео проще использовать экземпляр VideoView Класс VideoView не взаимодействует с MediaPlayer , как SurfaceView . Однако он взаимодействует с MediaController , что позволяет легко организовать интерфейс воспроизведения. Единственный нюанс с использованием VideoView заключается в том, что класс не принимает идентификаторы ресурсов — только пути к файлам или объекты Uri . Чтобы создать объект Uri , ссылающийся на ресурс Android, используйте код следующего вида: Uri resourceUri = Uri.parse("android.resource://" + "com.bignerdranch.android.hellomoon/raw/apollo_17_stroll"); Создайте URI со схемой android.resource , именем вашего пакета вместо хоста, ти- пом и именем вашего ресурса вместо пути. Результат может быть передан VideoView Упражнение. Воспроизведение видео в HelloMoon Измените программу HelloMoon так, чтобы она также позволяла воспроизводить видеоролики. Если вы не загрузили файл apollo_17_stroll.mpg ранее, вернитесь к файлу решений и скопируйте его из проекта главы в каталог res/raw . Затем воспроизведите его одним из описанных способов.
Сохранение фрагментов В настоящее время приложение HelloMoon некорректно обрабатывает повороты. Запустите его, включите воспроизведение и поверните устройство. Воспроизве- дение прерывается. Дело в том, что при повороте HelloMoonActivity уничтожается. Когда это происхо- дит, FragmentManager отвечает за уничтожение HelloMoonFragment FragmentManager вызывает методы угасающего жизненного цикла фрагмента: onPause() , onStop() и onDestroy() . В HelloMoonFragment.onDestroy() освобождается экземпляр Media- Player , что приводит к остановке воспроизведения. В главе 3 мы решили проблему обработки поворотов в приложении Geo- Quiz переопределением Activity.onSaveInstanceState(Bundle) . Данные со- хранялись, а новая активность загружала их. Класс Fragment содержит метод onSaveInstanceState(Bundle) , который работает аналогичным образом. Однако сохранение состояния объекта MediaPlayer и его последующее восстановление все равно прерывает воспроизведение и раздражает пользователей. Сохранение фрагмента К счастью, у фрагментов имеется механизм, благодаря которому экземпляр Me- diaPlayer может «пережить» изменение конфигурации. Переопределите метод HelloMoonFragment.onCreate(…) и задайте свойство фрагмента. 14
Повороты и сохраненные фрагменты 251 Листинг 14.1. Вызов setRetainInstance(true) (HelloMoonFragment.java) private Button mPlayButton; private Button mStopButton; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { По умолчанию свойство retainInstance фрагмента содержит false . Это означает, что при поворотах фрагмент не сохраняется, а уничтожается и создается заново вместе с активностью-хостом. Вызов setRetainInstance(true) сохраняет фрагмент, который не уничтожается вместе с активностью, а передается новой активности в неизменном виде. При сохранении фрагмента можно рассчитывать на то, что все его поля (включая mPlayButton , mPlayer и mStopButton ) сохранят прежние значения. Вы к ним обра- щаетесь, а они просто находятся на своем месте. Снова запустите HelloMoon. Включите воспроизведение, поверните устройство и убедитесь в том, что файл воспроизводится без прерывания. Повороты и сохраненные фрагменты Давайте поближе познакомимся с тем, как работают сохраненные фрагменты. Они используют то обстоятельство, что представление фрагмента может уничтожаться и создаваться заново без необходимости уничтожать сам фрагмент. При изменении конфигурации FragmentManager сначала уничтожает представле- ния фрагментов в своем списке. Представления фрагментов всегда уничтожаются и создаются заново по тем же причинам, по которым уничтожаются и создаются заново представления активности: в новой конфигурации могут потребоваться новые ресурсы. На случай, если для нового варианта существуют более подходящие ресурсы, представление строится «с нуля». Затем FragmentManager проверяет свойство retainInstance каждого фрагмента. Если оно равно false (по умолчанию), FragmentManager уничтожает экземпляр фрагмента. Фрагмент и его представление будут созданы заново новым экземпля- ром FragmentManager новой активности.
252Глава 14. Сохранение фрагментов До поворота После поворота Рис. 14.1. Реализация поворота по умолчанию с UI-фрагментом С другой стороны, если значение retainInstance равно true , представление фраг- мента уничтожается, но сам фрагмент остается. При создании новой активности новый экземпляр FragmentManager находит сохраненный фрагмент и воссоздает его представление. До поворота Во время поворота После поворота Рис. 14.2. Поворот с сохраненным UI-фрагментом Сохраненный фрагмент не уничтожается, но отсоединяется (detached) от «уми- рающей» активности. В сохраненном состоянии фрагмент все еще существует, но не имеет активности-хоста. Сохранение фрагментов: действительно так хорошо? 253 Выполняется Новый Уничтоженный Сохраненный Создан Сохранен? Остановлен Приостановлен Рис. 14.3. Жизненный цикл фрагмента Переход в сохраненное состояние происходит только при выполнении двух условий: для фрагмента был вызван метод setRetainInstance(true) ; активность-хост уничтожается для изменения конфигурации (обычно поворот). Фрагмент находится в сохраненном состоянии очень недолго — от момента отсо- единения от старой активности до повторного присоединения к новой, немедленно создаваемой активности. Сохранение фрагментов: действительно так хорошо? Сохраненные фрагменты: удобно, верно? Да! Действительно удобно. На первый взгляд они решают все проблемы, связанные с уничтожением активностей и фраг- ментов при поворотах. При изменении конфигурации устройства для подбора наи- более подходящих ресурсов создается новое представление, а в вашем распоряжении имеется простой способ сохранения данных и объектов.
254Глава 14. Сохранение фрагментов Тогда почему не сохранять все фрагменты подряд и почему фрагменты не сохра- няются по умолчанию? Похоже, Android без энтузиазма относится к сохранению фрагментов в пользовательских интерфейсах. Мы не знаем, почему это так, но если группа Android ни во что не ставит эту возможность, в будущем могут возникнуть проблемы. Помните, что сохраненные фрагменты продолжают существовать только при унич- тожении активности при изменения конфигурации. Если активность уничтожается из-за того, что ОС потребовалось освободить память, то все сохраненные фрагменты также будут уничтожены. Повороты и onSaveInstanceState(Bundle)Метод onSaveInstanceState(Bundle) — еще один инструмент, используемый для обработки поворотов. Собственно, если у вашего приложения нет проблем с поворо- тами, то только благодаря работе поведения onSaveInstanceState(…) по умолчанию. Хорошим примером служит приложение CriminalIntent. Фрагмент CrimeFragment не сохраняется, но если внести изменения в краткое описание преступления или переключить флажок, новые состояния объектов View автоматически сохраняются и восстанавливаются после поворота. Метод onSaveInstanceState(…) проектиро- вался именно для решения этой задачи — сохранения и восстановления состояния пользовательского интерфейса приложения. Главное отличие между переопределением Fragment.onSaveInstanceState(…) и со- хранением фрагмента — продолжительность существования сохраненных данных. Если данные должны только пережить изменения конфигурации, сохранение фрагмента потребует существенно меньшей работы. Это особенно справедливо при сохранении объекта; разработчику не нужно беспокоиться о том, реализует объект Serializable или нет. Но если данные должны существовать дольше, сохранение фрагмента не помо- жет. Если активность уничтожается для освобождения памяти после бездействия пользователя, все сохраненные фрагменты уничтожаются так же, как и их несо- храненные родственники. Чтобы разница стала более наглядной, вспомните приложение GeoQuiz. Проблема поворота заключалась в том, что индекс вопроса обнулялся при повороте. На каком бы вопросе ни находился пользователь, при повороте устройства он возвращался к первому вопросу. Чтобы пользователь видел правильный вопрос, мы сохраняли значение индекса, а затем снова загружали его. В приложении GeoQuiz не использовались фрагменты, но представьте себе пере- работанную версию GeoQuiz с фрагментом QuizFragment , хостом которого является QuizActivity . Как быть — переопределить Fragment.onSaveInstanceState(…) для сохранения индекса или сохранить QuizFragment и оставить переменную живой? На рис. 14.4 показаны три разных жизненных цикла, с которыми вам придется работать: жизнь объекта активности (и его несохраненных фрагментов), жизнь сохраненного фрагмента и жизнь записи активности. Повороты и onSaveInstanceState(Bundle) 255Уничтожения Запись активности Объект активности Сохраненный фрагмент активности (процесс завершается) Повороты Рис. 14.4. Три жизненных цикла Срок жизни объекта активности слишком мал; это и является источником проблемы поворота. Индекс определенно должен пережить объект активности. Если сохранить QuizFragment , индекс будет существовать на протяжении срока жизни сохраненного фрагмента. Если GeoQuiz содержит только пять вопросов, сохранение QuizFragment проще реализуется и требует меньше кода. Мы инициали- зируем индекс в поле, а затем вызываем setRetainInstance(true) в QuizFragment. onCreate(…) Листинг 14.2. Сохранение гипотетического фрагмента QuizFragment public class QuizFragment extends Fragment { private int mCurrentIndex = 0; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } } |