Всем привет! Меня зовут Юрий Дорофеев, я Android-разработчик и преподаватель в Mail.ru Group. Если вы когда-нибудь записывали аудиосообщения, то видели, как анимируется интерфейс в зависимости от громкости вашего голоса. Давайте повторим этот эффект:
Чтобы начать запись внутри Android-приложения, нужно сначала дать ему доступ к этой функциональности. Создадим в Android-манифесте тег
Также нам придётся запросить его в runtime’е при помощи
Для записи звука создадим класс
Записывать аудио будем во временную директорию:
После окончания записи нужно применить к медиарекордеру
Чтобы работать с состоянием, нужно его каким-то образом получать. Для этого сделаем функцию, которая будет говорить, ведётся ли сейчас запись:
Теперь сделаем кнопку записи. Создадим новое
По клику на кнопку будем выполнять методы
Если сейчас запустим приложение, то кнопка будет кликаться и даже будет работать запись аудио, но визуально это никак не отображается.
Теперь давайте сделаем отслеживание громкости записи. Добавим соответствующую функцию:
Данна функция возвращает максимальное значение со времени последнего вызова.
При начале записи активируем таймер для опрашивания громкости. Пусть он каждые 100 мс забирает данные:
Теперь решим следующую задачу: при нажатии кнопки визуально непонятно, идёт ли запись и насколько громко. Создадим метод
Насколько нужно увеличивать кнопку?
Интересно, что эта анимация работает автоматически: если мы несколько раз в цикле запустим
Для более живой анимации воспользуемся
Всё оказалось достаточно просто. Можно вместо изменения размера рисовать гистограмму громкости или ещё что-нибудь, что придёт в голову вам или вашему дизайнеру ?
Доступ к микрофону
Чтобы начать запись внутри Android-приложения, нужно сначала дать ему доступ к этой функциональности. Создадим в Android-манифесте тег
uses-permission и укажем разрешение RECORD_AUDIO:<uses-permission android:name="android.permission.RECORD_AUDIO" />
Также нам придётся запросить его в runtime’е при помощи
ActivityCompat.requestPermission:ActivityCompat.requestPermissions( this, arrayOf(android.Manifest.permission.RECORD_AUDIO), 777, )
Запись звука
Для записи звука создадим класс
RecordController. У него должны быть два основных метода: start и stop. Для записи голоса хорошо подходит кодек ААС:fun start() { Log.d(TAG, "Start") audioRecorder = MediaRecorder().apply{ setAudioSource(MediaRecorder.AudioSource.MIC) setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS) setAudioEncoder(MediaRecorder.AudioEncoder.AAC) setOutputFile(getAudioPath()) prepare() start() } }
Записывать аудио будем во временную директорию:
private fun getAudioPath(): String { return "${context.cacheDir.absolutePath}${File.pathSeparator}${System.currentTimeMillis()}.wav" }
После окончания записи нужно применить к медиарекордеру
stop и release:fun stop() { audioRecorder?.let { Log.d(TAG, "Stop") it.stop() it.release() } audioRecorder = null }
Чтобы работать с состоянием, нужно его каким-то образом получать. Для этого сделаем функцию, которая будет говорить, ведётся ли сейчас запись:
fun isAudioRecording() = audioRecorder != null
Кнопка записи
Теперь сделаем кнопку записи. Создадим новое
View и расположим его в центре экрана. Почему это не кнопка, а View? Потому что мы сейчас будем её анимировать и нам не нужны стандартные визуальные эффекты нажатия, тени и прочего.<?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" tools:context=".MainActivity"> <View android:id="@+id/start_button" android:layout_width="100dp" android:layout_height="100dp" android:background="@drawable/oval" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
По клику на кнопку будем выполнять методы
start или stop. При первом клике мы начинаем запись, а при втором останавливаем, поэтому берём из рекордера состояние, в зависимости от котор��го применяем нужную логику:private fun onButtonClicked() { if (recordController.isAudioRecording()) { recordController.stop() } else { recordController.start() } }
Если сейчас запустим приложение, то кнопка будет кликаться и даже будет работать запись аудио, но визуально это никак не отображается.
Громкость
Теперь давайте сделаем отслеживание громкости записи. Добавим соответствующую функцию:
fun getVolume() = audioRecorder?.maxAmplitude ?: 0
Данна функция возвращает максимальное значение со времени последнего вызова.
При начале записи активируем таймер для опрашивания громкости. Пусть он каждые 100 мс забирает данные:
private fun onButtonClicked() { if (recordController.isAudioRecording()) { recordController.stop() countDownTimer?.cancel() countDownTimer = null } else { recordController.start() countDownTimer = object : CountDownTimer(60_000, 100) { override fun onTick(p0: Long) { val volume = recordController.getVolume() Log.d(TAG, "Volume = $volume") handleVolume(volume) } override fun onFinish() { } }.apply{ start() } } }
Анимация
Теперь решим следующую задачу: при нажатии кнопки визуально непонятно, идёт ли запись и насколько громко. Создадим метод
handleVolume, реагирующий на громкость и меняющий размер кнопки. У View есть множество способов анимирования, самый простой — это animate, который позволяет очень удобно задавать простые анимации.Насколько нужно увеличивать кнопку?
MediaRecorder возвращает значение громкости в виде 16-ти битного int с максимальным значением 32767. Давайте рассчитаем, насколько далеко мы находимся от этого предела, чтобы пропорционально увеличить кнопку:private fun handleVolume(volume: Int) { val scale = min(8.0, volume / MAX_RECORD_AMPLITUDE + 1.0).toFloat() Log.d(TAG, "Scale = $scale") audioButton.animate() .scaleX(scale) .scaleY(scale) .setInterpolator(interpolator) .duration= VOLUME_UPDATE_DURATION }
Интересно, что эта анимация работает автоматически: если мы несколько раз в цикле запустим
animate, то наложения не произойдёт, каждая новая анимация будет завершать предыдущую. Только надо не забыть завершать запись и анимацию в методах onDestroy или onPause на случай поворота экрана или других событий, связанных с Activity.Для более живой анимации воспользуемся
OvershootInterpolator'ом, он позволяет выходить за границы доступного диапазона: кнопка будет словно пульсировать, кратковременно выходя за верхнюю границу:Всё оказалось достаточно просто. Можно вместо изменения размера рисовать гистограмму громкости или ещё что-нибудь, что придёт в голову вам или вашему дизайнеру ?
