《Android编程权威指南》第 19 章第二篇,补充完 BeatBox 应用啦。
第一篇地址:
https://juejin.cn/post/7032485144078319653
六、导入 assets
创建 BeatBox 类,AssetManager 类可以访问 assets。
class BeatBox(private val assets: AssetManager) {
fun loadSounds(): List<String> {
try {
val soundNames = assets.list(SOUNDS_FOLDER)!!
Log.d(TAG, "Found ${soundNames.size} sounds")
return soundNames.asList()
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "Could not list assets", e)
return emptyList()
}
}
}
AssetManager.list(String) 能列出指定目录下的所有文件名,传入声音资源所在的目录,就能看到其中的所有.wav文件。
在 MainActivity 中创建 BeatBox 实例,并调用 loadSounds() 函数。
beatBox = BeatBox(assets)
beatBox.loadSounds()
运行结果如下,可以看到已经读到 assets 里的文件。
七、使用 assets
- 创建 Sound 管理类,使用 String.split(String).last() 分离出文件名,再使用 String.removeSuffix(String) 删除.wav后缀。
private const val WAV = ".wav"
class Sound(val assetPath: String) {
val name = assetPath.split("/").last().removeSuffix(WAV)
}
- 在 BeatBox.loadSounds() 中创建 Sound 对象集合。
class BeatBox(private val assets: AssetManager) {
private val sounds: List<Sound>
init {
sounds = loadSounds()
}
fun loadSounds(): List<Sound> {
val soundNames: Array<String>
try {
soundNames = assets.list(SOUNDS_FOLDER)!!
} catch (e: Exception) {
e.printStackTrace()
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
}
}
- 绑定 Sound 对象集合
private inner class SoundAdapter(private val sounds:List<Sound>):RecyclerView.Adapter<SoundHolder>(){
...
override fun getItemCount() = sounds.size
}
- 传入声音资源(MainActivity.kt)
adapter = SoundAdapter(beatBox.sounds)
运行结果:
八、绑定数据
- 创建 SoundViewModel 类并添加绑定函数。
class SoundViewModel {
var sound: Sound? = null
set(sound) {
field = sound
}
val title: String?
get() = sound?.name
}
- 绑定至视图模型
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.pyn.beatbox.SoundViewModel" />
</data>
<Button
android:layout_width="match_parent"
android:layout_height="120dp"
android:text="@{viewModel.title}"
tools:text="Sound name" />
</layout>
- 关联使用视图模型
private inner class SoundHolder(private val binding:ListItemSoundBinding):RecyclerView.ViewHolder(binding.root){
init {
binding.viewModel = SoundViewModel()
}
fun bind(sound:Sound){
binding.apply {
viewModel?.sound = sound
executePendingBindings()
}
}
}
...
override fun onBindViewHolder(holder: SoundHolder, position: Int) {
val sound = sounds[position]
holder.bind(sound)
}
- 绑定数据观察
class SoundViewModel : BaseObservable() {
var sound: Sound? = null
set(sound) {
field = sound
notifyChange()
}
@get:Bindable
val title: String?
get() = sound?.name
}
调用 notifyChange(),就是通知绑定类,视图模型对象上所有可绑定属性都已更新。
运行结果:
九、深入学习:数据绑定再探
有关数据绑定(DataBinding)库更加深入的介绍请参考:
https://developer.android.com/topic/libraries/data-binding
lambda 表达式「布局里面也可以使用 lambda 表达式写短回调」
比如给 item 中的 button 添加点击时间可以写成:
android:onClick="@{() -> viewModel.onButtonClick()}"
数据绑定还有一些方便的语法可用。最方便的一个是使用单引号代替双引号,它还有 null 自动处理机制。
数据绑定默认会把绑定表达式解读为属性函数调用。
比如要定义一个 app:isGone 属性,基于某个布尔值来设置所有 View 的可见性,可以这么做:
@BindingAdapter("app:isGone")
fun bindIsGone(view: View, isGone: Boolean) {
view.visibility = if (isGone) View.GONE else View.VISIBLE
}
TextViewBindingAdapter 就为 TextView 提供了一些特别的属性操作。你可以在Android Studio 里看看它们的源码。当然也有搜到 AutoCompleteTextViewBindingAdapter、CheckedTextViewBindingAdapter 这些类,可自行查查看看。
十、深入学习:LiveData和数据绑定
class SoundViewModel{
val title :MutableLiveData<String?> = MutableLiveData()
var sound: Sound? = null
set(sound) {
field = sound
title.postValue(sound?.name)
}
}
private inner class SoundAdapter(private val sounds:List<Sound>):RecyclerView.Adapter<SoundHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
SoundHolder{
...
binding.lifecycleOwner = this@MainActivity
return SoundHolder(binding)
}
...
}
其他
BeatBox 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/BeatBox