Освой 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().

Запустим программу и выясним, так кто-же сказал Мяу?

SoundPool

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


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;
    }
});

При воспроизведении звука мы получаем его идентификатор, используемый для остановки воспроизведения.

Дополнительное чтение

Теория. Класс SoundPool

Исходники на Гитхабе (Java)

Обсуждение статьи на форуме.

Реклама