You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何阻止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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 04:35:36