如何阻止AnimationDrawable在Compose重组时重复播放?
解决AnimationDrawable在Compose中重复播放问题及逐帧动画优化方案
一、修复AnimationDrawable仅播放一次的问题
1. 优化ViewModel状态,跟踪动画触发需求
首先在ViewModel中增加状态,标记是否需要播放动画,确保动画仅在用户交互或Drawable切换时触发:
private val _onDrawable = R.drawable.anim_on private val _offDrawable = R.drawable.anim_off data class HomeScreenState( val mainUnitConnected: Boolean = false, val mainUnitDrawableId: Int = _offDrawable, val shouldPlayAnimation: Boolean = false // 控制动画是否需要播放 ) class HomeViewModel: ViewModel(){ private val _uiState = MutableStateFlow(HomeScreenState()) val uiState: StateFlow<HomeScreenState> = _uiState.asStateFlow() fun updateMainUnitConnected(connected: Boolean){ _uiState.update { it.copy( mainUnitConnected = connected, shouldPlayAnimation = true // 切换状态时触发动画 ) } updateMainUnitDrawable(connected) } private fun updateMainUnitDrawable(connected: Boolean){ _uiState.update { it.copy( mainUnitDrawableId = if(connected) _onDrawable else _offDrawable ) } } // 动画结束后重置播放标记 fun markAnimationPlayed(){ _uiState.update { it.copy(shouldPlayAnimation = false) } } }
2. 在Composable中缓存Drawable并控制播放逻辑
通过remember缓存Drawable实例,避免重组时创建新实例导致重复播放;用LaunchedEffect精准控制动画触发时机,并监听动画结束:
@Composable fun MainUnitCard(viewModel: HomeViewModel = viewModel()){ val homeUiState by viewModel.uiState.collectAsState() val context = LocalContext.current // 缓存Drawable实例,仅当DrawableId变化时重新创建 val drawable = remember(homeUiState.mainUnitDrawableId) { AppCompatResources.getDrawable(context, homeUiState.mainUnitDrawableId) as AnimationDrawable } // 计算动画总时长,用于监听结束 val totalAnimDuration = remember(drawable) { var duration = 0 for (i in 0 until drawable.numberOfFrames) { duration += drawable.getDuration(i) } duration.toLong() } // 仅当需要播放或Drawable切换时触发动画 LaunchedEffect(homeUiState.shouldPlayAnimation, homeUiState.mainUnitDrawableId) { if (homeUiState.shouldPlayAnimation) { drawable.stop() // 先停止避免重复触发 drawable.start() // 等待动画结束后标记状态,停在最后一帧 delay(totalAnimDuration) viewModel.markAnimationPlayed() drawable.stop() } } val painter = rememberDrawablePainter(drawable = drawable, contentDescription = "主设备状态") Card( onClick = { viewModel.updateMainUnitConnected(!homeUiState.mainUnitConnected) } ) { Column { Image(painter = painter, contentDescription = "主设备状态") } } }
核心问题解决点
- stop()无效原因:之前每次重组都会创建新的
AnimationDrawable实例,调用stop()的是旧实例,新实例附着到View时会自动播放。用remember缓存实例后,操作的是同一个对象,stop()才会生效。 - 布尔值状态丢失:Composable局部变量会在重组时重新初始化,需用
remember/rememberSaveable或ViewModel存储状态,确保配置变化(如旋转)时状态不丢失。
二、Compose中逐帧动画的更优方案
若不想依赖View体系的AnimationDrawable,可使用Compose原生实现,完全贴合Compose生命周期,状态更可控:
@Composable fun MainUnitCard(viewModel: HomeViewModel = viewModel()){ val homeUiState by viewModel.uiState.collectAsState() // 根据状态切换帧序列(示例:on状态对应39帧,off状态对应另一组帧) val frameList = remember(homeUiState.mainUnitConnected) { if (homeUiState.mainUnitConnected) { (0..39).map { R.drawable.frame_on_${it} } // 替换为实际帧资源id } else { (0..39).map { R.drawable.frame_off_${it} } } } // 控制动画是否播放 val shouldPlay = remember { mutableStateOf(false) } // 帧索引动画:从第0帧过渡到最后一帧 val frameIndex by animateIntAsState( targetValue = if (shouldPlay.value) frameList.size - 1 else 0, animationSpec = tween( durationMillis = frameList.size * 100, // 假设每帧100ms,可根据实际调整 easing = LinearEasing ), finishedListener = { shouldPlay.value = false // 动画结束后停在最后一帧 } ) // 状态切换时触发动画 LaunchedEffect(homeUiState.mainUnitConnected) { shouldPlay.value = true } Card( onClick = { viewModel.updateMainUnitConnected(!homeUiState.mainUnitConnected) } ) { Column { Image( painter = painterResource(id = frameList[frameIndex]), contentDescription = "主设备状态" ) } } }
原生方案优势
- 完全基于Compose体系,生命周期更适配,不会出现View体系的状态丢失问题
- 动画状态完全可控,支持暂停、继续、自定义曲线等扩展逻辑
- 重组时不会自动重复播放,仅在主动触发时运行
内容的提问来源于stack exchange,提问作者Jan Börschlein
相关产品推荐
相关产品推荐

