Jetpack Compose 如何为 LazyColumn 懒惰地获取音乐文件及其元数据
Posted
技术标签:
【中文标题】Jetpack Compose 如何为 LazyColumn 懒惰地获取音乐文件及其元数据【英文标题】:Jetpack Compose How to get music files and their metadatas lazily for LazyColumn 【发布时间】:2022-01-01 10:48:37 【问题描述】:我正在尝试使用 Jetpack Compose 编写音乐播放器应用程序。我有一个如下所示的 MusicCardModel
data class MusicCardModel(
val contentUri: Uri?,
val songId: Long?,
val cover: Bitmap?,
val songTitle: String?,
val artist: String?,
val duration: String?
)
当我启动应用程序时,我正在使用以下功能的 MediaStore 扫描所有音乐文件
@SuppressLint("Recycle")
fun Context.musicList(): MutableList<MusicCardModel>
val list = mutableListOf<MusicCardModel>()
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
else
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST
)
val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
val sortOrder = "$MediaStore.Audio.Media.DISPLAY_NAME ASC"
val query = this.contentResolver.query(
collection,
projection,
selection,
null,
sortOrder
)
query?.use cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
while (cursor.moveToNext())
val id = cursor.getLong(idColumn)
val duration = cursor.getInt(durationColumn)
val title = cursor.getString(titleColumn)
val artist = cursor.getString(artistColumn)
val albumId = cursor.getLong(albumIdColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
val bitmap = getAlbumArt(this, contentUri)
val durationString = convertMili(duration)
list.add(MusicCardModel(contentUri, id, bitmap, title, artist, durationString))
return list
fun getAlbumArt(context: Context, uri: Uri): Bitmap
val mmr = MediaMetadataRetriever()
mmr.setDataSource(context, uri)
val data = mmr.embeddedPicture
return if(data != null)
BitmapFactory.decodeByteArray(data, 0, data.size)
else
BitmapFactory.decodeResource(context.resources, R.drawable.note)
显示该列表的这部分代码
Box(modifier = Modifier
.padding(bottom = if (isPlaying.value) 80.dp else 0.dp))
LazyColumn
items(list) index ->
MusicCard(
uri = index.contentUri!!,
songId = index.songId,
artist = index.artist!!,
name = index.songTitle!!,
duration = index.duration!!,
isPlaying = isPlaying,
playingSong = playingSong
)
@Composable
fun MusicCard(
uri: Uri,
artist: String,
name: String,
cover: Bitmap?,
duration: String,
isPlaying: MutableState<Boolean>,
playingSong: MutableState<MusicCardModel>,
songId: Long?,
playState: MutableState<Boolean>
)
val context = LocalContext.current
Card(modifier = Modifier
.fillMaxWidth()
.clickable
playMusic(context, uri)
playState.value = true
isPlaying.value = true
val playingSongModel = MusicCardModel(uri, songId,
null, name, artist, duration)
playingSong.value = playingSongModel
)
Row(
modifier = Modifier.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
)
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
)
Image(
modifier = Modifier.size(70.dp),
bitmap = cover!!.asImageBitmap(),
contentDescription = "Cover Photo",
)
Column(
modifier = Modifier
.padding(horizontal = 10.dp)
)
Text(
modifier = Modifier.padding(vertical = 5.dp),
text = artist
)
Text (name, maxLines = 1)
Text(text = duration)
有两个问题。第一个是创建这个列表,专辑艺术花费了太多时间,并且等待很长时间才能在屏幕上显示该列表。没有专辑封面它非常快,但我想展示专辑封面。如何延迟加载音乐文件的元数据?第二个问题是将所有这些列表数据加载到内存中,当我切换到另一个应用程序并且应用程序停止时,会出现 TransactionTooLargeException。我该如何解决这些问题?
【问题讨论】:
【参考方案1】:我建议您将此逻辑移到视图模型中,并仅在需要的单元格出现时才加载位图。
data class MusicCardModel(
val contentUri: Uri,
val songId: Long,
val cover: Bitmap?,
val songTitle: String,
val artist: String,
val duration: String
)
class MusicListViewModel: ViewModel()
val musicCards = mutableStateListOf<MusicCardModel>()
private var initialized = false
private val backgroundScope = viewModelScope.plus(Dispatchers.Default)
fun initializeListIfNeeded(context: Context)
if (initialized) return
val collection =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
else
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST
)
val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
val sortOrder = "$MediaStore.Audio.Media.DISPLAY_NAME ASC"
val query = context.contentResolver.query(
collection,
projection,
selection,
null,
sortOrder
)
query?.use cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
while (cursor.moveToNext())
val id = cursor.getLong(idColumn)
val duration = cursor.getInt(durationColumn)
val title = cursor.getString(titleColumn)
val artist = cursor.getString(artistColumn)
val albumId = cursor.getLong(albumIdColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id
)
val durationString = convertMili(duration)
musicCards.add(MusicCardModel(contentUri, id, null, title, artist, durationString))
initialized = true
fun loadBitmapIfNeeded(context: Context, index: Int)
if (musicCards[index].cover != null) return
// if this is gonna lag during scrolling, you can move it on a background thread
backgroundScope.launch
val bitmap = getAlbumArt(context, musicCards[index].contentUri)
musicCards[index] = musicCards[index].copy(cover = bitmap)
private fun getAlbumArt(context: Context, uri: Uri): Bitmap
val mmr = MediaMetadataRetriever()
mmr.setDataSource(context, uri)
val data = mmr.embeddedPicture
return if(data != null)
BitmapFactory.decodeByteArray(data, 0, data.size)
else
BitmapFactory.decodeResource(context.resources, R.drawable.note)
像这样使用它:
@Composable
fun MusicListScreen(
viewModel: MusicListViewModel = viewModel()
)
val musicCards = viewModel.musicCards
val context = LocalContext.current
LaunchedEffect(Unit)
viewModel.initializeListIfNeeded(context)
LazyColumn
itemsIndexed(musicCards) index, card ->
LaunchedEffect(Unit)
viewModel.loadBitmapIfNeeded(context, index)
if (card.cover != null)
Image(bitmap = card.cover.asImageBitmap(), "...")
else
// some placeholder
【讨论】:
它正在延迟加载,但是当我向下滚动它的滞后时,正如你在那里写的,但当我通过滚动到达列表末尾时,我也不知道如何在后台线程上移动它,应用程序正在自行关闭。 @AhmetSaraç 我已将其移至背景,请查看。至于关闭,可能是崩溃了,你应该检查日志。 它更好但不是那么好,因为它在滚动时仍然滞后。我认为问题在于将封面位图存储在模型中。这些数据很大,需要时间来获取和展示。如果我可以先列出文本值,我认为滚动不会很慢。我有一个可组合的 MusicCard 来列出我刚刚添加到问题中的歌曲。我想我应该将封面位图与模型分开。 @AhmetSaraç 尝试使用Bitmap.createScaledBitmap(b, 120, 120, false)
将其缩放到某个合理的大小,然后再将其添加到列表中(例如,在后台协程上)。或者尝试使用 Coil rememberImagePainter
,它可能会为您完成这项工作
缩放不起作用 如果线圈不起作用,我会尝试 Coil,这意味着我认为我对数据建模的逻辑有问题 我将尝试另一种建模,但我什么都不知道关于 ViewModel。我将学习如何使用 ViewModel。我只是 android 开发的初学者,我想从 Jetpack Compose 开始。还是谢谢你以上是关于Jetpack Compose 如何为 LazyColumn 懒惰地获取音乐文件及其元数据的主要内容,如果未能解决你的问题,请参考以下文章
Jetpack Compose 和 Compose Navigation 如何处理 Android 活动?