29、View系统与Compose互操作:AndroidView中嵌入View动画、Compose中调用View动画、互操作性能考量

说实话,做Android动画开发这么多年,我一直觉得「View动画」和「Compose动画」就像两个平行世界。但现实是,我们很少能一次性把所有代码都迁移到Compose。大部分项目都是混用的——老模块用View,新模块用Compose,中间还得互相调用。

今天我们就来聊聊这个「混用」的话题。说白了,就是怎么在Compose里优雅地塞进View动画,反过来又怎么在View体系里调用Compose动画。最后再聊聊性能——嗯,这块坑不少,我踩过。

AndroidView:Compose里的「View容器」

先看第一个场景:你正在用Compose写新界面,但有个老的自定义View,里面跑着复杂的属性动画。你不想重写它,怎么办?

Compose提供了AndroidView这个组件。它就像个「容器」,能把传统的View塞进Compose的布局树里。

@Composable
fun LegacyAnimationView() {
    AndroidView(
        factory = { context ->
            LegacyView(context).apply {
                // 启动View动画
                startAnimation(rotateAnimation)
            }
        },
        modifier = Modifier.fillMaxWidth().height(200.dp)
    )
}

这里有个细节:factory只会在首次组合时调用一次。如果你需要根据状态更新动画,得用update回调。

我的习惯:在factory里只做初始化,动画的启停控制交给update。这样能避免重复创建View。

在AndroidView里嵌入View动画

假设你有一个老牌的ImageView,上面跑着RotateAnimation。你想把它嵌入到Compose的LazyColumn里。怎么做?

@Composable
fun RotatingImageInCompose() {
    var isRotating by remember { mutableStateOf(true) }

    AndroidView(
        factory = { ctx ->
            ImageView(ctx).apply {
                setImageResource(R.drawable.icon)
                scaleType = ImageView.ScaleType.CENTER_CROP
            }
        },
        update = { imageView ->
            if (isRotating) {
                val anim = RotateAnimation(
                    0f, 360f,
                    Animation.RELATIVE_TO_SELF, 0.5f,
                    Animation.RELATIVE_TO_SELF, 0.5f
                ).apply {
                    duration = 2000
                    repeatCount = Animation.INFINITE
                    interpolator = LinearInterpolator()
                }
                imageView.startAnimation(anim)
            } else {
                imageView.clearAnimation()
            }
        },
        modifier = Modifier.size(100.dp)
    )
}

嗯,这里要注意:update会在每次重组时被调用。如果你不小心,可能会重复启动动画。我建议用LaunchedEffect来控制动画的生命周期,而不是直接写在update里。

我曾经踩过的坑:在LazyColumn里用AndroidView包裹带无限循环动画的View。滑动时,Item被回收再复用,动画状态全乱了。后来我用key参数给每个Item一个唯一标识,才解决。

Compose中调用View动画:反向操作

反过来,如果你在传统的Fragment或Activity里,想调用Compose的动画怎么办?

其实思路一样——用ComposeView作为桥梁。

// 在View体系中嵌入Compose动画
class MyFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            setContent {
                // 这里跑Compose动画
                var visible by remember { mutableStateOf(false) }
                AnimatedVisibility(visible = visible) {
                    Text("Hello from Compose!")
                }
                Button(onClick = { visible = !visible }) {
                    Text("Toggle")
                }
            }
        }
    }
}

你想想看,这样就把Compose的声明式动画能力,带进了传统的View体系里。我个人觉得,这种「反向嵌入」在过渡期特别有用——你可以在老页面里逐步引入Compose组件,而不需要一次性重写整个页面。

互操作的性能考量

聊完了怎么用,咱们得聊聊性能。说实话,View和Compose互操作是有代价的。

操作类型 性能开销 主要原因
AndroidView嵌入 中等 需要创建View树,桥接布局测量
ComposeView嵌入 中等偏高 Compose需要独立的SlotTable,与View树同步
动画帧同步 两个渲染管线需要协调刷新

为什么会这样?因为Compose有自己的渲染管线,View也有自己的。当你在Compose里嵌入View动画时,每一帧都需要在两个系统之间同步状态。说白了,就是多了一层桥接。

核心建议:
  • 如果动画是高频更新的(比如60fps的旋转动画),尽量在同一个系统内完成
  • 如果必须跨系统,考虑用GraphicsLayerCanvas来减少桥接次数
  • 避免在LazyColumnRecyclerView里大量使用互操作

我总结的几条实战经验

做了几个项目后,我总结了几条经验,分享给你:

  1. 能用Compose动画就用Compose动画——性能更好,代码也更简洁
  2. 老View动画如果只是简单效果(比如淡入淡出、平移),直接重写成Compose动画,成本不高
  3. 复杂的自定义View动画(比如依赖SurfaceView或TextureView的),保留原样,用AndroidView包裹
  4. 动画的启停控制一定要用LaunchedEffectDisposableEffect,避免内存泄漏
一个小技巧:如果你需要在Compose里监听View动画的状态(比如动画结束回调),可以用AnimationListener,然后通过remember的lambda传递给Compose层。

知识体系总览

下面这张图,是我梳理的View与Compose互操作的核心逻辑。你可以看到,核心就是「桥接」两个字。

View 系统 View Animation Property Animation Drawable Animation Compose 系统 AnimatedVisibility animate*AsState Animatable / AnimationSpec 桥接层 AndroidView ComposeView 核心逻辑:通过 AndroidView / ComposeView 实现两个动画系统的互操作

嗯,总的来说,View和Compose的互操作不是银弹。它是个「过渡方案」,不是「终极方案」。我的建议是:新项目直接用Compose动画,老项目逐步迁移。互操作只是桥梁,别把它当终点。

公众号:蓝海资料掘金营,微信deep3321