Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Напишем программу, которая поможет определить, кто-же сказал Мяу? Меня всегда интересовал данный вопрос.
Подготовим заранее картинки различных животных и вставим их в папку res/drawable. Создадим макет приложения. Можно использовать ImageView или ImageButton. Я использовал оба варианта для Java и Kotlin.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/activity_color"
android:orientation="vertical"
tools:ignore="ContentDescription">
<ImageView
android:id="@+id/image_cat"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@+id/image_sheep"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/image_cow"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/cat" />
<ImageView
android:id="@+id/image_duck"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toTopOf="@+id/image_dog"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintTop_toBottomOf="@+id/image_chicken"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/duck" />
<ImageView
android:id="@+id/image_sheep"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/sheep" />
<ImageView
android:id="@+id/image_dog"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:scaleType="centerInside"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/dog" />
<ImageView
android:id="@+id/image_cow"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:scaleType="centerInside"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/cow" />
<ImageView
android:id="@+id/image_chicken"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:scaleType="centerInside"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.3"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5"
app:srcCompat="@drawable/chicken" />
</androidx.constraintlayout.widget.ConstraintLayout>
Положим подготовленные аудио-файлы с голосами животных в директорию assets. По умолчанию в проекте такой папки нет. Выбираем File | New | Folder | Assets Folder. В диалоговом окне оставляем всё без изменений и нажимаем кнопку Finish. Файлы, лежащие в этой папке, считайте тоже ресурсами. Но они имеют свои особенности, в частности вы можете создавать свою структуру подпапок.
Переходим к программной части. Нам надо создать объект SoundPool, загрузить в него аудио-файлы из папки assets методом load().
Зададим максимальное количество одновременно проигрываемых потоков - 3.
При нажатии на кнопку (или картинку) будем проигрывать нужный звук. В варианте для Java остался устаревший код, в Kotlin-коде оставил только правильный вариант. Но не стал делать защиту от поворота, просто смотрите на Java-код и доработайте самостоятельно.
// Kotlin
// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
// http://developer.alexanderklimov.ru/android/
package ru.alexanderklimov.meow
import android.content.res.AssetFileDescriptor
import android.content.res.AssetManager
import android.media.AudioAttributes
import android.media.SoundPool
import android.os.Bundle
import android.util.Log
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var soundPool: SoundPool
private lateinit var assetManager: AssetManager
private var catSound: Int = 0
private var chickenSound: Int = 0
private var cowSound: Int = 0
private var dogSound: Int = 0
private var duckSound: Int = 0
private var sheepSound: Int = 0
private var streamID = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val catImage: ImageView = findViewById(R.id.image_cat)
val chickenImage: ImageView = findViewById(R.id.image_chicken)
val cowImage: ImageView = findViewById(R.id.image_cow)
val dogImage: ImageView = findViewById(R.id.image_dog)
val duckImage: ImageView = findViewById(R.id.image_duck)
val sheepImage: ImageView = findViewById(R.id.image_sheep)
val attributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
soundPool = SoundPool.Builder()
.setAudioAttributes(attributes)
.build()
assetManager = assets
catSound = loadSound("cat.ogg")
chickenSound = loadSound("chicken.ogg")
cowSound = loadSound("cow.ogg")
dogSound = loadSound("dog.ogg")
duckSound = loadSound("duck.ogg")
sheepSound = loadSound("sheep.ogg")
catImage.setOnClickListener { playSound(catSound) }
chickenImage.setOnClickListener { playSound(chickenSound) }
cowImage.setOnClickListener { playSound(cowSound) }
dogImage.setOnClickListener { playSound(dogSound) }
duckImage.setOnClickListener { playSound(duckSound) }
sheepImage.setOnClickListener { playSound(sheepSound) }
}
override fun onPause() {
super.onPause()
soundPool.release()
}
private fun playSound(sound: Int): Int {
if (sound > 0) {
streamID = soundPool.play(sound, 1F, 1F, 1, 0, 1F)
}
return streamID
}
private fun loadSound(fileName: String): Int {
val afd: AssetFileDescriptor = try {
application.assets.openFd(fileName)
} catch (e: IOException) {
e.printStackTrace()
Log.d("Meow", "Не могу загрузить файл $fileName")
return -1
}
return soundPool.load(afd, 1)
}
}
// Java
package ru.alexanderklimov.saymeow;
import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private SoundPool mSoundPool;
private AssetManager mAssetManager;
private int mCatSound, mChickenSound, mCowSound, mDogSound, mDuckSound, mSheepSound;
private int mStreamID;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// // Для устройств до Android 5
// createOldSoundPool();
// } else {
// // Для новых устройств
// createNewSoundPool();
// }
// mAssetManager = getAssets();
//
// // получим идентификаторы
// mCatSound = loadSound("cat.ogg");
// mChickenSound = loadSound("chicken.ogg");
// mCowSound = loadSound("cow.ogg");
// mDogSound = loadSound("dog.ogg");
// mDuckSound = loadSound("duck.ogg");
// mSheepSound = loadSound("sheep.ogg");
ImageButton cowImageButton = findViewById(R.id.imageButtonCow);
// cowImageButton.setOnClickListener(onClickListener);
ImageButton chickenImageButton = findViewById(R.id.imageButtonChicken);
chickenImageButton.setOnClickListener(onClickListener);
ImageButton catImageButton = findViewById(R.id.imageButtonCat);
catImageButton.setOnClickListener(onClickListener);
ImageButton duckImageButton = findViewById(R.id.imageButtonDuck);
duckImageButton.setOnClickListener(onClickListener);
ImageButton sheepImageButton = findViewById(R.id.imageButtonSheep);
sheepImageButton.setOnClickListener(onClickListener);
ImageButton dogImageButton = findViewById(R.id.imageButtonDog);
dogImageButton.setOnClickListener(onClickListener);
cowImageButton.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
int eventAction = event.getAction();
if (eventAction == MotionEvent.ACTION_UP) {
// Отпускаем палец
if (mStreamID > 0)
mSoundPool.stop(mStreamID);
}
if (eventAction == MotionEvent.ACTION_DOWN) {
// Нажимаем на кнопку
mStreamID = playSound(mCowSound);
}
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
mSoundPool.stop(mStreamID);
}
return true;
}
});
}
View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.imageButtonCow:
playSound(mCowSound);
break;
case R.id.imageButtonChicken:
playSound(mChickenSound);
break;
case R.id.imageButtonCat:
playSound(mCatSound);
break;
case R.id.imageButtonDuck:
playSound(mDuckSound);
break;
case R.id.imageButtonSheep:
playSound(mSheepSound);
break;
case R.id.imageButtonDog:
playSound(mDogSound);
break;
}
}
};
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void createNewSoundPool() {
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
mSoundPool = new SoundPool.Builder()
.setAudioAttributes(attributes)
.build();
}
@SuppressWarnings("deprecation")
private void createOldSoundPool() {
mSoundPool = new SoundPool(3, AudioManager.STREAM_MUSIC, 0);
}
private int playSound(int sound) {
if (sound > 0) {
mStreamID = mSoundPool.play(sound, 1, 1, 1, 0, 1);
}
return mStreamID;
}
private int loadSound(String fileName) {
AssetFileDescriptor afd;
try {
afd = mAssetManager.openFd(fileName);
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getApplicationContext(), "Не могу загрузить файл " + fileName,
Toast.LENGTH_SHORT).show();
return -1;
}
return mSoundPool.load(afd, 1);
}
@Override
protected void onResume() {
super.onResume();
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Для устройств до Android 5
createOldSoundPool();
} else {
// Для новых устройств
createNewSoundPool();
}
mAssetManager = getAssets();
// получим идентификаторы
mCatSound = loadSound("cat.ogg");
mChickenSound = loadSound("chicken.ogg");
mCowSound = loadSound("cow.ogg");
mDogSound = loadSound("dog.ogg");
mDuckSound = loadSound("duck.ogg");
mSheepSound = loadSound("sheep.ogg");
}
@Override
protected void onPause() {
super.onPause();
mSoundPool.release();
mSoundPool = null;
}
}
При загрузке файлов метод load() возвращает идентификатор soundID, который сохраняем для дальнейшего использования. Объявим для каждого звука отдельную переменную, если же звуков много лучше завести для этого ассоциативный массив.
Файловый дескриптор AssetFileDescriptor для файла из директории assets получаем с помощью метода openFd(), принимающего в качестве параметра имя файла. Если файл не найден или не может быть открыт, то выводим сообщение и в качестве soundID возвращаем -1.
По нажатию кнопки вызываем метод playSound(), передавая ему нужный идентификатор звука. В методе проверяем этот идентификатор. Если файл не был найден, то метод loadSound() возвращает -1, а если метод load() класса SoundPool не смог загрузить файл, то soundID будет равен 0, поэтому проверяем, что SoundID > 0, что означает, что файл был успешно загружен. Если же все хорошо, то вызываем метод play().
В версии Android 5.0 конструктор класса SoundPool является устаревшим. В коде использовано условие if с проверкой версии системы на устройстве, а также использованы аннотации, чтобы студия не ругалась на устаревший метод. Про аннотации мы поговорим в другой статье, пока воспринимайте их как подсказку-предупреждение при написании кода, чтобы выбрать правильный вариант.
Программа держит загруженные звуки в памяти. Если они вам не нужны, то нужно освободить ресурсы. Я сделал это в методе onPause(), соответственно загрузку пришлось перенести в onResume().
Запустим программу и выясним, так кто-же сказал Мяу?
Один из читателей захотел выводить звук не через щелчок, а нажатие на кнопку. А когда палец открывается от экрана, то звук должен прекращаться. Получился интересный эффект, который мы нашли сообща. Код для кнопки с коровой (предыдущий код лучше убрать):
cowImageButton.setOnTouchListener(new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
int eventAction = event.getAction();
if (eventAction == MotionEvent.ACTION_UP) {
// Отпускаем палец
if (mStreamID > 0)
mSoundPool.stop(mStreamID);
}
if (eventAction == MotionEvent.ACTION_DOWN) {
// Нажимаем на кнопку
mStreamID = playSound(mCowSound);
}
if (event.getAction() == MotionEvent.ACTION_CANCEL) {
mSoundPool.stop(mStreamID);
}
return true;
}
});
При воспроизведении звука мы получаем его идентификатор, используемый для остановки воспроизведения.
Обсуждение статьи на форуме.