Jetpack Compose中视频淡入动画失效问题排查与解决
解决Compose中视频淡入动画失效与切换卡顿问题
问题描述
开发混合渲染图片与视频的Demo时,图片的淡入淡出动画正常工作,但视频仅支持正常淡出,淡入动画完全失效,且切换到视频时画面存在明显卡顿。
原因分析
- 视频初始化延迟:每次加载视频时,
ExoPlayer的创建、prepare()调用需要一定时间,当AnimatedVisibility触发淡入动画时,视频还未完成渲染,导致动画无视觉效果,同时初始化过程带来性能开销引发卡顿。 - Player实例频繁重建:每次切换到视频都会创建新的
ExoPlayer实例,频繁的创建与释放操作会占用大量资源,加剧卡顿。 - 动画与View不同步:
AnimatedVisibility直接作用于AndroidView时,原生View的绘制流程与Compose动画的帧更新不同步,导致淡入动画无法正确作用到视频内容上。
修复方案
1. 缓存视频Player实例
对相同URL的视频复用ExoPlayer实例,避免重复创建带来的性能损耗。
2. 提前初始化视频Player
在视频即将显示前完成Player的初始化和准备工作,确保淡入动画启动时视频已可渲染。
3. 调整动画作用范围
将淡入淡出动画作用在包裹AndroidView的容器上,确保动画能正确覆盖视频内容。
4. 优化Player生命周期管理
视频不可见时暂停播放,重新显示时恢复播放,仅在确定不再使用时释放Player资源。
修复后的完整代码
import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import androidx.media3.common.Player import coil.compose.AsyncImage import coil.compose.ContentScale import kotlinx.coroutines.delay import kotlinx.coroutines.launch enum class MediaType { IMAGE, VIDEO } data class Media( val url: String, val type: MediaType, val delay: Long ) val media = listOf( Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000), Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000 ), Media("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", MediaType.VIDEO, 16000), Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000), Media("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", MediaType.VIDEO, 16000), Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000), Media("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", MediaType.VIDEO, 16000), Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000), Media("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", MediaType.VIDEO, 16000), Media("https://img.freepik.com/free-vector/flat-sale-landing-page-template_23-2149053848.jpg", MediaType.IMAGE, 5000), Media("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4", MediaType.VIDEO, 16000), ) @Composable fun ContentPlayer() { var currentIndex by rememberSaveable { mutableIntStateOf(0) } val coroutineScope = rememberCoroutineScope() // 缓存视频Player实例,key为视频URL val videoPlayerCache = remember { mutableMapOf<String, ExoPlayer>() } fun switchToNextMedia() { currentIndex = if (currentIndex == media.size - 1) 0 else currentIndex + 1 } // 预加载下一个视频(如果是视频类型) LaunchedEffect(currentIndex) { val nextIndex = if (currentIndex == media.size - 1) 0 else currentIndex + 1 val nextMedia = media[nextIndex] if (nextMedia.type == MediaType.VIDEO && !videoPlayerCache.containsKey(nextMedia.url)) { val context = LocalContext.current val player = ExoPlayer.Builder(context).build().apply { setMediaItem(MediaItem.fromUri(nextMedia.url.toUri())) prepare() playWhenReady = false // 预加载但不播放,等显示时再启动 } videoPlayerCache[nextMedia.url] = player } } LaunchedEffect(currentIndex) { coroutineScope.launch { val currentMedia = media[currentIndex] delay(currentMedia.delay) switchToNextMedia() } } // 处理当前媒体的Player状态 LaunchedEffect(currentIndex) { val currentMedia = media[currentIndex] if (currentMedia.type == MediaType.VIDEO) { videoPlayerCache[currentMedia.url]?.let { player -> player.playWhenReady = true player.seekTo(0) // 每次显示时回到开头 } } // 暂停其他视频的播放 videoPlayerCache.filter { it.key != currentMedia.url }.values.forEach { player -> player.playWhenReady = false } } // 释放不再需要的Player资源 DisposableEffect(Unit) { onDispose { videoPlayerCache.values.forEach { it.release() } videoPlayerCache.clear() } } Box(modifier = Modifier.fillMaxSize()) { media.forEachIndexed { index, mediaItem -> AnimatedVisibility( visible = index == currentIndex, enter = fadeIn(tween(3000)), exit = fadeOut(tween(3000)), ) { Box(modifier = Modifier.fillMaxSize()) { // 动画作用在容器上 if (mediaItem.type == MediaType.IMAGE) { AsyncImage( contentScale = ContentScale.FillBounds, model = mediaItem.url, contentDescription = null, modifier = Modifier.fillMaxSize() ) } else { VideoContentPlayer( url = mediaItem.url, player = videoPlayerCache[mediaItem.url] ) } } } } } } @Composable fun VideoContentPlayer(url: String, player: ExoPlayer?) { val context = LocalContext.current val exoPlayer = player ?: remember(context, url) { ExoPlayer.Builder(context).build().apply { setMediaItem(MediaItem.fromUri(url.toUri())) prepare() playWhenReady = true } } DisposableEffect(exoPlayer) { onDispose { // 这里不直接释放,交给缓存统一管理 exoPlayer.playWhenReady = false } } AndroidView(factory = { PlayerView(context).apply { this.player = exoPlayer useController = false setShowBuffering(PlayerView.SHOW_BUFFERING_ALWAYS) // 显示缓冲状态,提升用户体验 } }) }
关键优化点说明
- Player缓存机制:通过
videoPlayerCache复用相同URL的ExoPlayer实例,避免重复初始化带来的性能损耗。 - 预加载逻辑:在当前媒体播放时,提前初始化下一个视频的Player,确保切换时视频已准备就绪,解决淡入动画失效问题。
- 动画容器调整:将
AnimatedVisibility作用在包裹媒体内容的Box上,确保动画能正确覆盖视频内容。 - 生命周期优化:视频不可见时暂停播放,重新显示时恢复并回到开头,仅在全局销毁时统一释放所有Player资源。
内容的提问来源于stack exchange,提问作者Jalson1982
相关产品推荐
相关产品推荐

