文章目录

  • 20.1 创建 SoundPool
  • 20.2 访问 assets
  • 20.3 加载音频文件
  • 20.4 播放音频
  • 20.5 单元测试
  • 20.6 创建测试类
  • 20.7 配置测试类
  • 20.8 编写测试函数
    • 20.8.1 测试对象交互
  • 20.9 数据绑定回调
  • 20.10 释放音频
  • 20.11 整合测试

20.1 创建 SoundPool

通常用 SoundPool 类的高级 API 开发,其封装度很高,可加载一批声音资源到内存中,并能控制同时播放的音频文件的个数,新建 SoundPool 的代码如下:

private const val TAG = "BeatBox"
private const val SOUNDS_FOLDER = "sample_sounds"
private const val MAX_SOUNDS = 5class BeatBox(private val assets: AssetManager) {val sounds: List<Sound>private val soundPool = SoundPool.Builder().setMaxStreams(MAX_SOUNDS).build()init {sounds = loadSounds()}private fun loadSounds(): List<Sound> {val soundNames: Array<String>try {soundNames = assets.list(SOUNDS_FOLDER)!!} catch (e: Exception) {Log.e(TAG, "Could not list assets", e)return emptyList()}val sounds = mutableListOf<Sound>()soundNames.forEach { filename ->val assetPath = "$SOUNDS_FOLDER/$filename"val sound = Sound(assetPath)sounds.add(sound)}return sounds}
}

20.2 访问 assets

需要通过 assetManager 访问各资源。

  • 访问 InputStream 数据流,方式如下:
val assetPath = sound.assetPath
val assetManager = context.assets
val soundData = assetManager.open(assetPath)
  • 访问 FileDescriptor,方式如下:
val assetPath = sound.assetPath
val assetManager = context.assets
val assetFileDescriptor = assetManager.openFd(assetPath)
val fileDescriptor = assetFileDescriptor.fileDescriptor

20.3 加载音频文件

必须先加载音频文件,再播放音频文件。

SoundPool 加载的音频文件都有自己的 Integer 型 ID。代码如下:为管理这些 ID,在 Sound 类中添加 soundId 属性。

class Sound(val assetPath: String, var soundId: Int? = null) {val name = assetPath.split("/").last().removeSuffix(WAV)
}

在 BeatBox 中添加 load(Sound) 函数载入音频,代码如下:

package com.bignerdranch.android.beatboximport android.content.res.AssetFileDescriptor
import android.content.res.AssetManager
import android.media.SoundPool
import android.util.Log
import java.io.IOExceptionprivate const val TAG = "BeatBox"
private const val SOUNDS_FOLDER = "sample_sounds"
private const val MAX_SOUNDS = 5class BeatBox(private val assets: AssetManager) {val sounds: List<Sound>private val soundPool = SoundPool.Builder().setMaxStreams(MAX_SOUNDS).build()init {sounds = loadSounds()}private fun loadSounds(): List<Sound> {val soundNames: Array<String>try {soundNames = assets.list(SOUNDS_FOLDER)!!} catch (e: Exception) {Log.e(TAG, "Could not list assets", e)return emptyList()}val sounds = mutableListOf<Sound>()soundNames.forEach { filename ->val assetPath = "$SOUNDS_FOLDER/$filename"val sound = Sound(assetPath)try {load(sound)sounds.add(sound)} catch (ioe: IOException) {Log.e(TAG, "could not load sound $filename", ioe)}}return sounds}private fun load(sound: Sound) {val afd: AssetFileDescriptor = assets.openFd(sound.assetPath)val soundId = soundPool.load(afd, 1)sound.soundId = soundId}
}

20.4 播放音频

在 BeatBox 中添加 play(Sound) 函数即可播放音频,其中 SoundPool.play() 函数的参数分别是 音频ID、左音量、右音量、优先级、是否循环、播放速率。代码如下:

package com.bignerdranch.android.beatboximport android.content.res.AssetFileDescriptor
import android.content.res.AssetManager
import android.media.SoundPool
import android.util.Log
import java.io.IOExceptionprivate const val TAG = "BeatBox"
private const val SOUNDS_FOLDER = "sample_sounds"
private const val MAX_SOUNDS = 5class BeatBox(private val assets: AssetManager) {val sounds: List<Sound>private val soundPool = SoundPool.Builder().setMaxStreams(MAX_SOUNDS).build()init {sounds = loadSounds()}private fun loadSounds(): List<Sound> {val soundNames: Array<String>try {soundNames = assets.list(SOUNDS_FOLDER)!!} catch (e: Exception) {Log.e(TAG, "Could not list assets", e)return emptyList()}val sounds = mutableListOf<Sound>()soundNames.forEach { filename ->val assetPath = "$SOUNDS_FOLDER/$filename"val sound = Sound(assetPath)try {load(sound)sounds.add(sound)} catch (ioe: IOException) {Log.e(TAG, "could not load sound $filename", ioe)}}return sounds}private fun load(sound: Sound) {val afd: AssetFileDescriptor = assets.openFd(sound.assetPath)val soundId = soundPool.load(afd, 1)sound.soundId = soundId}fun play(sound: Sound) {sound.soundId?.let {soundPool.play(it, 1.0f, 1.0f,1,0,1.0f)}}
}

20.5 单元测试

要编写测试代码,首先需要添加两个测试工具:Mockito和Hamcrest。

Mockito是一个方便创建模拟对象的Java框架。有了模拟对象,就可以单独测试SoundViewModel,不用担心会因代码关联关系测到其他对象。

Hamcrest是个规则匹配器工具库。匹配器可以方便地在代码里模拟匹配条件。如果不能按预期匹配条件定义,测试就通不过。这可以验证代码是否按预期工作。

JUnit库里已经自带Hamcrest。而且,在创建项目时,Android Studio已经自动添加了JUnit依赖。所以,我们只需要手动添加Mockito依赖就可以了。在build.gradle文件,添加Mockito依赖,代码如下:

dependencies {testImplementation 'org.mockito:mockito-core:2.25.0'testImplementation 'org.mockito:mockito-inline:2.25.0'
}

20.6 创建测试类

打开 SoundViewModel.kt 文件,鼠标选中 SoundViewModel 类,用 Command+Shift+T 快捷键,为此类创建一个单测类,示例如下:


将测试放到 test 目录下,如下图:

20.7 配置测试类

创建 SoundViewModelTest.kt 文件,代码如下:

package com.bignerdranch.android.beatboximport org.junit.Beforeinternal class SoundViewModelTest {private lateinit var sound: Soundprivate lateinit var subject: SoundViewModel@Beforefun setUp() {sound = Sound("assetPath")subject = SoundViewModel()subject.sound = sound}
}

其中测试对象通常命名为 subject,这样可读性强,方便迁移代码。

20.8 编写测试函数

在测试类里写一个以 @Test 注解的测试函数即可,代码如下:

package com.bignerdranch.android.beatboximport org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Testinternal class SoundViewModelTest {private lateinit var sound: Soundprivate lateinit var subject: SoundViewModel@Beforefun setUp() {sound = Sound("assetPath")subject = SoundViewModel()subject.sound = sound}@Testfun exposesSoundNameAsTitle() {assertThat(subject.title, `is`(sound.name))}
}

运行效果如下:

20.8.1 测试对象交互

在 SoundViewModel 类里写 onButtonClicked() 函数去调用 BeatBox.play(Sound) 函数,代码如下:

package com.bignerdranch.android.beatboximport org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Testinternal class SoundViewModelTest {private lateinit var sound: Soundprivate lateinit var subject: SoundViewModel@Beforefun setUp() {sound = Sound("assetPath")subject = SoundViewModel()subject.sound = sound}@Testfun exposesSoundNameAsTitle() {assertThat(subject.title, `is`(sound.name))}@Testfun callsBeatBoxPlayOnButtonClicked() {subject.onButtonClicked()}
}

通过 mock(Class) 可模拟需测试的类,并通过调用verify(Object)函数,确认onButtonClicked()函数调用了BeatBox.play(Sound)函数,代码如下:

package com.bignerdranch.android.beatboximport org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.verifyinternal class SoundViewModelTest {private lateinit var beatBox: BeatBoxprivate lateinit var sound: Soundprivate lateinit var subject: SoundViewModel@Beforefun setUp() {beatBox = mock(BeatBox::class.java)sound = Sound("assetPath")subject = SoundViewModel()subject.sound = sound}@Testfun exposesSoundNameAsTitle() {assertThat(subject.title, `is`(sound.name))}@Testfun callsBeatBoxPlayOnButtonClicked() {subject.onButtonClicked()verify(beatBox).play(sound)}
}

现在直接运行单测会报错。我们按如下修改:

在 SoundViewModel.kt 中 修改 onButtonClicked() 函数,代码如下:

package com.bignerdranch.android.beatboximport androidx.databinding.BaseObservableclass SoundViewModel(private val beatBox: BeatBox) : BaseObservable() {fun onButtonClicked() {sound?.let { beatBox.play(it) }}var sound: Sound? = nullset(sound) {field = soundnotifyChange()}val title: String?get() = sound?.name
}

在 MainActivity.kt 中传入 beatBox参数 到 SoundViewModel 中,代码如下:

package com.bignerdranch.android.beatboximport android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bignerdranch.android.beatbox.databinding.ActivityMainBinding
import com.bignerdranch.android.beatbox.databinding.ListItemSoundBindingclass MainActivity : AppCompatActivity() {private lateinit var beatBox: BeatBoxoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)beatBox = BeatBox(assets)val binding: ActivityMainBinding =DataBindingUtil.setContentView(this, R.layout.activity_main)binding.recyclerView.apply {layoutManager = GridLayoutManager(context, 3)adapter = SoundAdapter(beatBox.sounds)}}private inner class SoundHolder(private val binding: ListItemSoundBinding) :RecyclerView.ViewHolder(binding.root) {init {binding.viewModel = SoundViewModel(beatBox)}fun bind(sound: Sound) {binding.apply {viewModel?.sound = soundexecutePendingBindings()}}}private inner class SoundAdapter(private val sounds: List<Sound>) :RecyclerView.Adapter<SoundHolder>() {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoundHolder {val binding = DataBindingUtil.inflate<ListItemSoundBinding>(layoutInflater, R.layout.list_item_sound, parent, false)return SoundHolder(binding)}override fun onBindViewHolder(holder: SoundHolder, position: Int) {val sound = sounds[position]holder.bind(sound)}override fun getItemCount() = sounds.size}
}

再次运行单测,即可通过,效果如下:

20.9 数据绑定回调

按钮要响应事件还差最后一步:关联 按钮对象 和 onButtonClicked() 函数。

和前面使用数据绑定关联数据和UI一样,也可以使用lambda表达式,让数据绑定帮忙关联按钮和点击监听器。

在布局文件里,添加数据绑定lambda表达式,让按钮对象和 SoundViewModel.onButtonClicked() 函数关联起来,代码如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><data><variablename="viewModel"type="com.bignerdranch.android.beatbox.SoundViewModel"/></data><Buttonandroid:layout_width="match_parent"android:layout_height="120dp"android:onClick="@{ () -> viewModel.onButtonClicked() }"android:text="@{viewModel.title}"tools:text="Sound name"/>
</layout>

当点击按钮时即会发出声音,效果如下:

日志如下:

I/OMXClient: IOmx service obtained
I/OMXClient: IOmx service obtained
W/System: A resource failed to call close.
I/chatty: uid=10143(com.bignerdranch.android.beatbox) FinalizerDaemon identical 20 lines
W/System: A resource failed to call close.
I/AudioTrack: createTrack_l(0): AUDIO_OUTPUT_FLAG_FAST successful; frameCount 0 -> 13239
I/AudioTrack: createTrack_l(0): AUDIO_OUTPUT_FLAG_FAST successful; frameCount 0 -> 11773
I/AudioTrack: createTrack_l(9): AUDIO_OUTPUT_FLAG_FAST successful; frameCount 0 -> 9792
I/AudioTrack: createTrack_l(10): AUDIO_OUTPUT_FLAG_FAST successful; frameCount 0 -> 11773

20.10 释放音频

音频播放完毕,应调用 SoundPool.release() 函数释放 SoundPool,代码如下:

class BeatBox(private val assets: AssetManager) {fun release() {soundPool.release()}
}

并在 MainActivity.kt 中调用,代码如下:

class MainActivity : AppCompatActivity() {fun release() {soundPool.release()}
}

再次运行应用,当旋转设备,或按 Back 键时,即会停止播放声音。

20.11 整合测试

  • 在单元测试里,受测对象是单个类。
  • 在整合测试里,受测对象是应用的一部分,包括协同工作的众多对象。

在Android平台上,整合测试通常还是指UI级别的测试(和UI部件交互,验证它们的行为表现是否符合预期)。例如,在MainActivity启动后,可以验证用户界面上的第一个按钮显示了音频库里的第一个文件名:65_cjipie。

整合测试通常以 instrumentation 测试来实施。

Espresso是Google开发的一个UI测试框架,可用来测试Android应用。Android Studio 默认已在 build.gradle 中导入了 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'的依赖。


代码如下:

package com.bignerdranch.android.beatboximport androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class)
class MainActivityTest {@get:Ruleval activityRule = ActivityTestRule(MainActivity::class.java)@Testfun showsFirstFileName() {onView(withText("65_cjipie")).check(matches(isDisplayed()))}
}

首先看其中的注解。@RunWith(AndroidJUnit4.class)表明,这是一个Android工具测试,需要activity和其他Android运行时环境支持。之后,activityRule上的@get:Rule注解告诉JUnit,运行测试前,要启动一个MainActivity实例。

准备工作做完,接下来就可以在测试函数里对MainActivity做断定测试了。在showsFirstFileName()函数里,onView(withText(“65_cjipie”))这行代码会找到显示“65_cjipie”的视图,对其执行测试。

check(matches(isDisplayed()))用来判定视图在屏幕上看得见。如果没有,则测试失败。相较于JUnit的assertThat(…)断言函数,check(…)函数是Espresso版的断言函数。

与视图交互时,Espresso会等待应用闲置再执行下一个测试。Espresso有一套探测UI是否已更新完毕的方法。如果需要,可使用一个IdlingResource子类告诉Espresso:多等一会儿,应用还在忙。

【Android进阶】20、音频播放:SoundPool 类、单元测试:Espresso框架相关推荐

  1. android开发监听媒体播放器,Android开发之媒体播放工具类完整示例

    本文实例讲述了Android开发之媒体播放工具类.分享给大家供大家参考,具体如下: package com.maobang.imsdk.util; import android.media.Media ...

  2. android第三方开源音频播放器,Android第三方开源SeekBarCompat:音乐类播放器等APP进度条常用...

     Android第三方开源SeekBarCompat:音乐类播放器等APP进度条常用 Android平台原生的SeekBar设计简单,然而,比如现在流行的一些音乐播放器的播放进度控制条,如果直接使 ...

  3. android调用系统音频播放器,Android使用Service实现简单音乐播放实例

    Service翻译成中文是服务,熟悉Windows 系统的同学一定很熟悉了.Android里的Service跟Windows里的Service功能差不多,就是一个不可见的进程在后台执行. Androi ...

  4. Android Studio——简易音频播放器

    目的 设计一个具有选歌功能的音频播放器 工具及环境 使用java语言,在Android studio平台上进行开发 功能设计 界面有三个按钮选项,可以停止.播放.暂停音乐.通过选择列表的音乐,播放相应 ...

  5. iOS开发之音频播放AVAudioPlayer 类的介绍

    主要提供以下了几种播放音频的方法: 1. System Sound Services System Sound Services是最底层也是最简单的声音播放服务,调用 AudioServicesPla ...

  6. Android中的音频播放(MediaPlayer和SoundPool)

    Android中音频和视频的播放我们最先想到的就是MediaPlayer类了,该类提供了播放.暂停.停止.和重复播放等方法.该类位于android.media包下,详见API文档.其实除了这个类还有一 ...

  7. Android之MediaPlayer 音频播放

    MediaPlayer通过如下两个静态方法来加载指定的音频: 1.static  MediaPlayer  create(Context context,Uri uri):从指定的Uri来装载音频文件 ...

  8. Android进阶之光学习记录——注解与依赖注入框架ButterKnife的尝试

    ⚠️创建的模块是java模块而非Android Library,如果创建的是后者,则无法使用AbstractProcessor 按照书上讲述的,想要自己去仿写一下butterknife 最终的项目结构 ...

  9. Android MediaPlay的使用以及实现音频播放器

    一.MediaPlay状态机详解(MediaPlay的生命周期) MediaPlayer状态机如下图所示 1.Idle(闲置)状态与End(结束)状态 MediaPlayer 对象声明周期 : 从 I ...

  10. android 音频播放过程,一种Android系统中的音频播放方法与流程

    本申请涉及android系统技术,特别涉及一种android系统中的音频播放方法. 背景技术: 在android系统中,现有的使用audiotrack进行音频播放时,audiotrack应用与andr ...

最新文章

  1. Android应用程序更新并下载
  2. HQL的使用和limit的替代
  3. 分享31个非常有用的 HTML5 教程
  4. android汽车音频焦点方案,管理音频焦点  |  Android 开发者  |  Android Developers
  5. 我的QT5学习之路(目录)
  6. Java虚拟机(一)——内存管理
  7. 用汉堡包的方式评价一下自己的合作伙伴
  8. 启动后显示不了数据_90后都买不起房?统计数据显示:90后成了城市租房主力!...
  9. div+css 英文或数字自动换行
  10. CMU 15-213 Introduction to Computer Systems学习笔记(5) Machine-Level Programming-Control
  11. [裴礼文数学分析中的典型问题与方法习题参考解答]4.3.1
  12. vue下载图片到本地的方法
  13. 中文拼音排序的两种方法
  14. Python Excel xlsx,xls,csv 格式互转
  15. HDU - 6070
  16. 尊重钟南山,但请也给我们哀悼科比
  17. git clone大仓库(>1G)时速度慢并出现RPC failed断开连接错误的真正解决方法
  18. iOS GitHub上常用第三方框架
  19. 回撤率 python_最大回撤用python怎么计算
  20. 国产手机干翻苹果?原来是靠百元机和猛降价实现的

热门文章

  1. 再逼自己一把,把项目做出来....
  2. GD库的中文问题(推荐)
  3. DCB改正——关于spp
  4. 按键控制c51单片机驱动unl2003控制步进电机正反转停止及程序调速-萌新入门
  5. python绝对方向角度值_哪个选项是turtle绘图中角度坐标系的绝对0度方向?_学小易找答案...
  6. 扑克牌(ArrayList)
  7. 如何在把微信公众号生成链接
  8. javascript实现九九乘法表(四种形式)
  9. qq浏览器tv版 v1.0 官方版
  10. 【测试工具】搭建API服务