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 活动?

如何用 Jetpack Compose 开发一个页面?

如何处理 Jetpack Compose 中的导航?

Jetpack Compose 实现波浪加载效果

Jetpack Compose | 控件篇 -- SwitchCheckBoxRadioButton

Jetpack Compose | 控件篇 -- SwitchCheckBoxRadioButton