Журнал LinuxFormat - перейти на главную

LXF151:Android: Музыка на марше

Материал из Linuxformat
(перенаправлено с «LXF151:tut7»)
Перейти к: навигация, поиск
Наш эксперт

У Джульетты Кемп гораздо больше MP3-файлов, чем она не поленилась загрузить в эмулятор Android.

Часть 1: В первой из двух статей цикла Джульетта Кемп обращает свой взгляд на MediaPlayer и создает и запускает простой MP3-плейер.

Если вы программист, то, безусловно, оцените одну из лучших возможностей Android – удобный, хорошо задокументированный обширный API: огромное количество классов и интерфейсов, которыми можно воспользоваться с минимумом усилий. И если у вас нет веской причины этого не делать, лучше обойтись им, чем разворачивать собственные классы, отчасти ради экономии времени и усилий, а отчасти потому, что классы и интерфейсы API стабильно работают с устройствами Android.

На двух следующих уроках мы коснемся API для работы со звуком/мультимедиа, который позволяет проигрывать файлы MP3 и других форматов. На этом уроке мы проиграем файлы, расположенные на SD-карте, а в следующем займемся потоковым воспроизведением. Мы предполагаем, что вы хотя бы отдаленно знакомы со средой разработки и развертывания программ для Android, и употребим командную строку вместо Eclipse – но если вы предпочтете Eclipse, он прекрасно подойдет.

Скорая помощь
Помните, что пример кода на DVD не компилируется «как есть»; нужно создать его либо как мы объяснили, либо с помощью Eclipse, чтобы компилятор знал, где найти локальную установку SDK.

Начальные установки: приступаем

Я компилировала в версии 10, и она нужна по меньшей мере одному примененному здесь классу, но все базовые классы должны быть доступны и в более ранних версиях:

android create project --target android-10 --name mp3 \\

--path ~/android/mp3 --activity AndroidMP3 --package com.example.androidmp3

Для первой версии нашего проекта мы просто возьмем список MP3-файлов, имеющихся на SD-карте. Сначала создайте list.xml в res/layout, чтобы настроить представление [View], которое будет использовать программа:

<?xml version=”1.0” encoding=”UTF-8”?>

<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”

android:orientation=”vertical”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”>

<ListView android:id=”@id/android:list”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”

android:layout_weight=”1”

android:drawSelectorOnTop=”false”/>

<TextView android:id=”@id/android:empty”

android:layout_width=”fill_parent”

android:layout_height=”fill_parent”

android:text=”@string/emptylist”/>

</LinearLayout>

Обратите внимание на автоматический компонент android:

empty – он задает текст, отображаемый, если список пуст (текст задайте в res/values/strings.xml). Нам также понадобится файл item.xml, определяющий, как будут отображаться элементы списка:

<?xml version=”1.0” encoding=”utf-8”?>

<TextView android:id=”@+id/title” xmlns:android=”http://schemas.android.com/apk/res/android”

android:layout_width=”wrap_content”

android:layout_height=”wrap_content”/>

Вернемся к файлу AndroidMP3.java. Базовый список MP3-файлов должен наследовать ListActivity. Создавая действие, зададим его представление и обновим список доступных MP3-файлов:

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.list);

updateMP3List();

}

Мы уже создали R.layout.list; теперь запишем updateMP3List():

private static final String MEDIA_PATH = new String(“/sdcard/”);

private List<String> mp3list = new ArrayList<String>();

[ ... ]

public void updateMP3List() {

File home = new File(MEDIA_PATH);

if ( home.listFiles( new Mp3Filter() ).length > 0 ) {

for ( File file : home.listFiles( new Mp3Filter() ) ) {

mp3list.add(file.getName());

}

ArrayAdapter<String> mp3ListAdapter =

new ArrayAdapter<String>(this, R.layout.item, mp3list);

setListAdapter(mp3ListAdapter);

}

else {

// No files here; ‘empty’ string will be seen

}

}

Строка MEDIA_PATH задается всему классу, а затем применяется для создания нового объекта File. Метод listFiles() возвращает массив файлов в заданный каталог, соответствующий фильтру. Стало быть, нужно также создать Mp3Filter – легко реализовав его как вспомогательный класс внутри главного:

class Mp3Filter implements FilenameFilter {

public boolean accept(File dir, String name) {

return (name.endsWith(“.mp3”));

}

}

Создание ArrayAdapter

Вернемся к updateMP3List(). Оператор if проверяет, есть ли в каталоге на SD-карте MP3-файлы. Если их нет, будет использоваться строка «empty». Хотя блок else пуст, стоит задокументировать, что это сделано сознательно – проще будет сопровождать код потом.

Если файлы есть, мы по очереди проходим их, добавляя в список. Затем мы должны объявить ArrayAdapter, чтобы работать со списком mp3List как с массивом, который Android может отобразить, пользуясь TextView из файла item.xml для каждого элемента. Это еще одна часть стандартного API Android, и мы будем применять ее часто. Она выступает как интерфейс между TextView и массивом объектов любого типа. (Ее можно использовать и как интерфейс для более сложных типов представлений, перегрузив ее метод getView(); здесь нам достаточно TextView.)

Раздобыв список MP3-файлов, нужно позаботиться, чтобы при щелчке пользователя на элементе списка что-то произошло – а именно, воспроизвелся данный MP3-файл. Следующий фрагмент кода использует стандартный метод onListItemClick:

private int currentPosition = 0;

[ ... ]

protected void onListItemClick(ListView list, View view, int position, long id) {

currentPosition = position;

playMp3(MEDIA_PATH + mp3List.get(position));

}

Мы записали текущее положение в currentPosition, чтобы знать, где мы находимся в списке, и вызвали метод playMp3() для воспроизведения нужного файла:

private MediaPlayer mp = new MediaPlayer();

[ ... ]

private void playMp3(String mp3Path) {

try {

mp.reset();

mp.setDataSource(mp3Path);

mp.prepare();

mp.start();

} catch (IOException e) {

Log.v(TAG, e.getMessage());

}

}

В начале класса устанавливается MediaPlayer, а затем почти всю работу выполняет предоставляемый Android API MediaPlayer, благодаря чему метод довольно ясен. Код сбрасывает MediaPlayer на случай, если кто-то еще одновременно проигрывает файл, или, если возникла ошибка, устанавливает источник данных (путь до файла, на котором щелкнул пользователь MP3); подготавливает проигрыватель и начинает воспроизведение MP3-файла. Вот и все, не считая блока catch (в нем мы просто выводим ошибку в файл журнала). Во врезке более подробно рассматриваются цикл жизни MediaPlayer и возможные состояния проигрывателя.

Но что делать проигрывателю, когда песня закончилась? Как насчет того, чтобы сразу перейти к следующей? Добавьте следующий код в конец блока try:

mp.setOnCompletionListener(new OnCompletionListener() {

public void onCompletion(MediaPlayer mplayer) {

nextMp3();

}

});

Нет SD-карты?

Если в системе нет SD-карты (т. е. нельзя не только «получить список MP3-файлов», но и «получить содержимое каталога»), вы получите NullPointerException. К сожалению, этого нельзя избежать проверками home.exists() или home.isDirectory(), так как каталог SD-карты виден и отображается как каталог, даже если на самом деле его не существует. Ошибка возникнет только при попытке получить список файлов. Вместо этого нужно поместить оператор if/else в блок try/catch:

try {
if (home.listFiles [ ... ] ) { [ ... ] }
else { [ /// ] }
} catch (NullPointerException e) {
return;
}

Код тихо завершится, и TextView использует строку «empty» с ID «empty». Пожалуй, можно слегка улучшить этот пример, показав в TextView сообщение о том, что SD-карта не найдена.

(thumbnail)
Приложение, запущенное с темой ‘light’ — некоторым пользователям она может показаться нагляднее.

Опять же, API делает львиную долю работы, предоставляя метод setOnCompletionListener и класс OnCompletionListener. Нам остается написать метод nextMp3():

private void nextMp3() {

if (++currentPosition >= mp3List.size()) {

// No more songs! Reset currentPosition

currentPosition = 0;

} else {

playMp3(MEDIA_PATH + mp3List.get(currentPosition));

}

}

Типы звуковых файлов

Кроме MP3, Android поддерживает семь различных форматов звуковых файлов, в том числе AAC (.3gp, .mp4, и .m4a; сам .aac поддерживается только в Android 3.1+), FLAC (.flac) и Ogg Vorbis (.ogg). Наше приложение находит только MP3-файлы, что может не вполне удовлетворять вашим запросам.

Для получения списка всех доступных файлов без перечисления всех возможных расширений, можно посмотреть на класс MediaStore.Audio, и в частности, на контент-провайдер MediaStore.Audio.Media.EXTERNAL_CONTENT_URI. Android автоматически сканирует внешние SD-карты на наличие файлов мультимедиа, и для доступа к ним и для получения информации о них можно воспользоваться этим контент-провайдером.

Мы воспользуемся этим подходом в следующей статье, улучшая наш проигрыватель. Пока переменные и методы в этом классе названы так, чтобы было ясно, что мы берем с SD-карты только MP3-файлы.

Если MP3-файлы кончились, сверьтесь с размером списка и сбросьте currentPosition (и больше ничего не делайте; если вы хотите зациклить воспроизведение, можно перейти к проигрыванию первого MP3-файла). Иначе – вернитесь к методу playMP3 с путем до следующего MP3-файла.

Заметьте, что в операторе if мы употребили предварительный инкремент – это означает, что currentPosition увеличивается на единицу до проверки значения логического выражения. (В конструкции currentPosition++ >= mp3List.size() переменная увеличилась бы после этого, а нам такого не надо: мы проверяем, есть ли в списке следующая песня). Кроме того, когда мы дойдем до блока else, currentPosition уже увеличена на единицу. Важно четко понимать разницу между операторами пре- и постинкремента в Java.

Если на вашей SD-карте есть MP3-файлы, можно быстро проверить наш код – настроить устройство на USB-тестирование и установить программу прямо на него. Программу можно запустить и на эмуляторе, но тогда прежде всего понадобится установить SD-карту. Перейдите в каталог, где хранится карта, и скомандуйте

mksdcard -l 512MCard 512M mysdcard.img

Параметр -l необязателен – он задает метку диска для карты; размер и имя файла (с расширением .img) обязательны. После создания SD-карты запустите Android SDK/AVD Manager (android), выберите подходящее AVD (Android Virtual Device – виртуальное устройство Android) и измените его параметры, указав путь до SD-карты. Затем запустите AVD.

Ну, сейчас на карте нет MP3-файлов, и попробовав запустить программу, вы получите сообщение «На карте нет MP3-файлов [No MP3s on SD card]». Чтобы записать их на карту, воспользуйтесь командой adb push:

adb push getready.mp3 /sdcard/

(Чтобы эта команда сработала, эмулятор должен быть запущен.) Файлы можно добавить и через обозреватель файлов DDMS, но, по-моему, из командной строки это делается быстрее и удобнее.

Еще одно замечание по проверке: выбирайте MP3-файлы, запускаемые легко и быстро, для уверенности в том, что все работает должным образом.

ВРЕЗКА Скорая помощь
Для быстрого изменения стиля вашего приложения воспользуйтесь атрибутом android:theme элемента application в AndroidManifest.xml. Полный список доступных по умолчанию стилей можно найти на странице документации R.style.html.

Создаем несколько кнопок

На данном этапе нельзя понять, которая песня играет в данный момент; нет и кнопок паузы и перемещения между песнями. Давайте создадим три кнопки (воспроизведение/пауза, следующий трек, предыдущий трек) и текстовое поле для отображения песни, проигрываемой в данный момент.

Сейчас для упрощения работы переключим в list.xml представление расположения элементов с LinearLayout на RelativeLayout; прочие элементы оставим без изменений. Просто добавьте три кнопки и TextView:

<Button android:id=”@+id/playpause” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_alignParentBottom=”true” android:text=”@string/playpause_button” />

<Button android:id=”@+id/next” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_alignParentBottom=”true” android:layout_toRightOf=”@id/playpause” android:text=”@string/next_button” />

<Button android:id=”@+id/prev” android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:layout_alignParentBottom=”true” android:layout_toRightOf=”@id/next” android:text=”@string/prev_button” />

<TextView android:id=”@+id/currentmp3”

android:layout_width=”fill_parent”

android:layout_height=”wrap_content”

android:layout_above=”@id/playpause”

android:text=”” />

Состояния MediaPlayer

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

Цикл начинается с состояния Idle (либо после его создания, либо после вызова reset()), а заканчивается, после вызова release(), состоянием End. Обратите внимание, что хотя из состояния Idle нельзя вызывать довольно много методов (включая prepare() и start()), сообщения об ошибках, которые вы получите, если MediaPlayer был сброшен методом reset(), будут немного отличаться от аналогичных сообщений в случае, если MediaPlayer только что был создан.

По окончании работы с MediaPlayer важно вызвать release(), чтобы освободить все ресурсы.

Все кнопки выровнены по нижней части экрана (атрибут layout_alignParentBottom) и расположены одна за другой (атрибут layout_toRightOf). TextView находится над кнопками, но объявлять его нужно последним, иначе будет ошибка из-за ссылки на @id/playpause: ее нужно сперва объявить, а потом уж на нее ссылаться.

Вернемся к коду. Метод onCreate() вызывает метод setupButtons() – так код будет опрятнее. Все три кнопки настраиваются весьма похоже, в методе setupButtons(), и здесь я приведу код только для одной из них (код для других см. на DVD):

playpauseButton = (Button)this.findViewById(R.id.playpause);

playpauseButton.setOnClickListener(new OnClickListener() {

@Override

public void onClick(View v) {

if (mp.isPlaying()) {

pauseMp3();

} else {

playMp3(MEDIA_PATH + mp3List.get(currentPosition));

}

}

});

Опять же, большая часть работы с кнопками выполняется интерфейсом. Мы приостанавливаем или воспроизводим MP3-файл в зависимости от того, проигрывает ли MediaPlayer файл в данный момент или нет. Эту логику можно бы перенести в собственный метод playpause, но лучше разделять методы, особенно если вы захотите вызывать метод приостановки/воспроизведения из других частей кода.

У нас уже есть метод playMp3, но давайте отобразим в нем название песни, проигрываемой в данный момент, добавив следующую строку перед mp.start():

currentmp3.setText(mp3List.get(currentPosition));

Учтите, что она отображает имя файла песни, а не тэг MP3. Как получить метаданные файла, рассказано во врезке вверху справа.

Android и метаданные аудиофайлов

Для получения метаданных уже существует специальный класс MediaMetadataRetriever. Однако он доступен только в API начиная с уровня 10, т. е. 2.3.3/2.3.4, которым мы пользуемся в этом проекте. Если вы хотите отправить свою программу в свободное плавание, то захотите убедиться, что существует альтернатива для более ранних версий Android. Заменим строку currentmp3.setText() в playMp3() на

setTrackInfo(mp3Path);

и создадим следующий метод setTrackInfo():

private void setTrackInfo(String mp3Path) {
TextView currentmp3 = (TextView)this.findViewById(R.id.currentmp3);
MediaMetadataRetriever metadata = new
MediaMetadataRetriever();
metadata.setDataSource(mp3Path);
String artist = metadata.extractMetadata
(MediaMetadataRetriever.METADATA_KEY_ARTIST);
String title = metadata.extractMetadata
(MediaMetadataRetriever.METADATA_KEY_TITLE);
currentmp3.setText(title + “ - “ + artist);
metadata.release();
}

Опять же, все весьма очевидно. Мы создаем свой класс MediaMetadataRetriever, передаем ему путь к файлу (MediaMetadataRetriever также может принимать источники данных других типов, таких как FileDescriptor или Uri) и забираем исполнителя и название песни из метаданных файла, чтобы записать их в TextView. Затем мы освобождаем созданный класс, чтобы зря не тратить ресурсы (помните, что мы создаем новый класс для каждого нового трека; можно было бы создать один на все время жизни приложения и освобождать его в методе onStop(), но нужды в этом нет – класс довольно легкий, и мы пользуемся им только в данном методе). Конечно, если в файле нет корректных метаданных, произойдет ошибка (будет возвращен null), и нужно предусмотреть отход на случай, если и «исполнитель», и «трек» возвратят null (очевидный вариант – «название трека»). Также можно применить метод getDuration() из MediaPlayer для получения длительности файла. Обратите внимание, что она выдается в миллисекундах, и вам придется преобразовать ее в минуты и секунды. Тут поможет java.util.concurrent.TimeUnit, но увы – он не сможет преобразовать время в форму минуты:секунды, вместо этого округлив все до ближайшей минуты. Попробуйте такой вариант:

long durationMillis = mp.getDuration();
long durationSecs = TimeUnit.SECONDS.convert(durationMillis, TimeUnit.MILLISECONDS);
long durationMins = TimeUnit.MINUTES.convert(durationMillis, TimeUnit.MILLISECONDS);
String duration = durationMins + “:” + 
(durationSecs - durationMins*60);
currentmp3.setText(title + “ - “ + artist + “ 
(“ + duration + “)”);

Заметьте, что mp.getDuration() вызвать можно, поскольку setTrackInfo() вызывается уже после подготовки MediaPlayer – иначе появилось бы сообщение об ошибке.

Метод pauseMp3 до смешного прост:

private void pauseMp3() {

mp.pause();

}

Методы nextMp3() и prevMp3() также довольно просты – это вызовы playMp3() с новым текущим положением. Перекомпилируйте и запустите программу; теперь в ней должно быть текстовое поле с текущей песней (пока вы ничего не запустили, оно будет пустым) и три кнопки для перемещения взад-вперед по списку (а также выбора песни с помощью сенсорного экрана).

Одна из проблем в том, что если нажать паузу, а затем воспроизведение, файл MP3 будет проигрываться с самого начала, а не с места остановки. Это легко исправить, добавив приватную булевскую переменную pause. Измените начало блока try в playMp3() следующим образом:

if (pause == false) {

mp.reset();

mp.setDataSource(mp3Path);

mp.prepare();

currentmp3.setText(mp3List.get(currentPosition));

}

mp.start();

pause = false;

и добавьте строку pause = true в pauseMp3(). Вам также понадобится добавить строку pause = false в prevMp3() и nextMp3(), иначе при нажатии кнопок «предыдущий/следующий трек» в состоянии паузы вместо запуска следующей песни продолжит проигрываться текущая. Вот и все. (При желании можете добавить кнопку «стоп» с методом mp.stop().)

Другая проблема – если выйти из приложения, песня продолжит играть. Если приложение закрыто осознанно, нужно обработать эту ситуацию, применив сервис и передав нужные параметры службе фонового управления процессами. Мы займемся этим через месяц, а пока реализуем onStop(), чтобы корректно освободить ресурсы. Это особенно важно при работе с таким ресурсоемким классом как MediaPlayer, иначе может привести к недостатку памяти и/или ресурсов процессора и «падению» системы. К счастью, API помогает нам прибраться за собой:

protected void onStop() {

mp.release();

mp = null;

super.onStop();

}

(thumbnail)
Теперь мы добавили кнопки, информацию о треке и его продолжительность!

Учтите, что без вызова метода суперкласса вы получите ошибку времени выполнения. Этот метод освобождает MediaPlayer, и для перезапуска приложения придется создавать новый, в методе onResume():

protected void onResume() {

mp = new MediaPlayer();

updateMP3List();

super.onResume();

}

Обновлять список MP3-файлов не обязательно, но благодаря этому к списку прибавятся все новые файлы, появившиеся с момента последнего запуска приложения. Вздумав попробовать это сейчас, вы обнаружите, что при каждом перезапуске приложения список удваивается, так как он не очищается перед обновлением. Поэтому добавим строку в начало метода updateMP3List():

mp3List.clear();

Теперь наше приложение умеет проигрывать файлы, приостанавливать воспроизведение и перемещаться между песнями, и корректно освобождает ресурсы при выходе. На следующем уроке мы освоим запуск проигрывателя в качестве сервиса – его можно будет перевести в фоновый режим; мы узнаем, как с помощью MediaStore получить медиа-файлы и информацию о них, как установить листенер для обработки ошибок MediaPlayer и как работать с потоковым воспроизведением.

Персональные инструменты
купить
подписаться
Яндекс.Метрика