84、Jetpack Compose进阶:列表LazyColumn、动画、主题、与View互操作
各位同学,今天我们来啃一块硬骨头——Compose的进阶玩法。说实话,Compose入门不难,但真正要在项目里用好它,你得掌握几个核心技能:列表怎么高效渲染、动画怎么丝滑、主题怎么统一管理,以及最头疼的——怎么跟老项目里的View共存。
我个人习惯把这四个点称为「Compose四件套」。你在面试时,面试官问Compose,基本就围绕这些来问。好,我们一个一个拆开讲。
一、LazyColumn:别再用Column+ScrollState了
很多新手会犯一个错误:用Column包一堆item,外面套个ScrollState。数据少还好,一旦超过几十条,卡顿就来了。为什么?因为Column会一次性把所有item都创建出来。
LazyColumn不一样。它只渲染当前屏幕可见的item,滚动时回收不可见的item。说白了,它就是RecyclerView的Compose版本。
来看一个实际例子。我在项目中做过一个联系人列表,每个item包含头像、姓名、电话。用LazyColumn实现是这样的:
@Composable
fun ContactList(contacts: List<Contact>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = contacts,
key = { it.id } // 必须提供稳定key
) { contact ->
ContactItem(contact = contact)
}
}
}
这里有个坑:key一定要用数据本身的唯一ID,不要用index。我曾经接手过一个项目,用了index作为key,结果用户删除一条数据后,整个列表闪了一下,而且动画错乱。排查了半天,就是key的问题。
stickyHeader或item函数。LazyColumn支持多种item类型,你可以用items传列表,也可以用item插单个组件。
二、动画:从生硬到丝滑,就差这几行代码
Compose的动画系统,说实话,比View那套好用太多了。你不需要写AnimationSet、ObjectAnimator那一堆东西。用animate*AsState系列函数,几行代码搞定。
我常用的几个动画API:
- animateFloatAsState:数值变化动画,比如进度条、透明度
- animateColorAsState:颜色过渡,比如按钮按下变色
- animateContentSize:内容大小变化动画,比如展开/折叠
- AnimatedVisibility:进场/退场动画,比如弹窗、提示条
举个例子。我之前做一个点赞按钮,需要点击时图标放大再恢复。用Compose写:
@Composable
fun LikeButton(isLiked: Boolean, onToggle: () -> Unit) {
val scale by animateFloatAsState(
targetValue = if (isLiked) 1.2f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
IconButton(onClick = onToggle) {
Icon(
imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Default.FavoriteBorder,
contentDescription = "点赞",
modifier = Modifier.scale(scale),
tint = if (isLiked) Color.Red else Color.Gray
)
}
}
注意那个spring动画规格。我试过用tween,效果太硬。换成spring后,按钮按下去有回弹感,用户体验明显提升。你想想看,一个点赞动画,用户按下去感觉「duang」一下,是不是比生硬缩放舒服多了?
三、主题:别再到处写Color(0xFF...)
很多项目里,颜色、字体、形状散落在各个Composable里。改个主题色要全局搜索替换,太痛苦了。Compose的主题系统就是为了解决这个问题的。
我建议你这样做:
- 定义
Color、Typography、Shapes三个数据类 - 创建
LightColorScheme和DarkColorScheme - 用
MaterialTheme包裹整个App
看代码:
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
secondary = Color(0xFF03DAC6),
background = Color(0xFFF5F5F5),
surface = Color.White,
error = Color(0xFFB00020)
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
然后在任何Composable里,直接用MaterialTheme.colorScheme.primary获取颜色。改主题?改一个地方就行。
AppTheme放在MainActivity的setContent最外层。这样整个App都受主题控制。如果你有部分页面需要不同主题,可以用CompositionLocalProvider局部覆盖。
四、View互操作:老项目迁移的必经之路
这是最实际的问题。你不可能一夜之间把所有页面都改成Compose。大部分项目都是渐进式迁移。那么,Compose和View怎么互相嵌套?
两个方向:
- Compose里嵌入View:用
AndroidView - View里嵌入Compose:用
ComposeView
先说AndroidView。我在项目中需要嵌入一个WebView,因为Compose没有原生的WebView组件。写法如下:
@Composable
fun WebViewPage(url: String) {
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
loadUrl(url)
}
},
modifier = Modifier.fillMaxSize()
)
}
注意factory只会在首次创建时调用。如果你需要更新WebView的状态,用update参数。
反过来,在XML布局里嵌入Compose:
// 在Activity或Fragment中
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.composeView.setContent {
AppTheme {
Text("Hello from Compose!")
}
}
这里有个坑:ComposeView的生命周期需要手动管理。如果你在Fragment里用,记得在onDestroyView里调用composeView.disposeComposition()。我曾经漏掉这一步,导致内存泄漏,页面切来切去后App直接OOM。
Modifier.clickable代替setOnClickListener,保持一致性。
知识体系总览
下面这张图,是我自己总结的Compose进阶知识结构。你可以把它当作学习路线图:
这四个知识点,说白了就是Compose进阶的四个支柱。你掌握了它们,大部分项目场景都能应对。嗯,今天的内容就到这里。记住,多写多练,代码不会骗你。
公众号:蓝海资料掘金营,微信deep3321