一. 实现原理
- 在点击条目时获取我们点击到的
ListItem
的控件位置 - 使用一个新的
Box
标签覆盖住我们点击的ListItem
- 利用动画API,让覆盖在
ListItem
上的Box逐渐变大,直到覆盖整个页面。
二. API介绍
-
Modifier.onGloballyPositioned(onGloballyPositioned: (androidx.compose.ui.layout.LayoutCoordinates) -> kotlin.Unit)
该扩展函数用于获取可组合项在屏幕中的位置,在测量完成后,lambda将会被调用。
注意:为了能正常覆盖我们的控件,Rect的获取需要使用
boundsInRoot
,而不是boundsInWindow
,以避免状态栏高度带来的控件偏移。 -
LocalDensity.current
该组合项会返回一个
Density
对象。利用此对象,我们可以快捷的换算dp
与px
的关系。
三. 源码和注释
package com.kagg886.hellocompose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.kagg886.hellocompose.ui.theme.HelloComposeTheme
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloComposeTheme {
//根组件布局参数,提供弹出层的末态
var rootRect by remember {
mutableStateOf<Rect?>(null)
}
Box(modifier = Modifier
.fillMaxSize()
//根组件布局参数提供者
.onGloballyPositioned {
rootRect = it.boundsInRoot()
}) {
//定义点击的条目,该条目在业务中可以换成别的data
var key by remember {
mutableIntStateOf(-1)
}
//弹出层的初态布局参数
var initialRect by remember {
mutableStateOf<Rect?>(null)
}
LazyVerticalGrid(columns = GridCells.Fixed(3), contentPadding = PaddingValues(5.dp)) {
items(30) {
//记录每个卡片的布局参数
var cardOffset: Rect? = null
Card(modifier = Modifier
.height(150.dp)
.onGloballyPositioned {
//每个卡片的布局参数的提供者
cardOffset = it.boundsInRoot()
}
.padding(5.dp)
.clickable {
//提供弹出层data
key = it
//提供弹出层初态布局参数
initialRect = cardOffset
}) {
//展示布局
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "$it")
}
}
}
}
// 这里定义key为-1时不展示弹出层
if (key >= 0 && initialRect != null) {
//获取弹出层初态布局参数
val (x, y) = with(LocalDensity.current) {
(initialRect?.topLeft?.x ?: 0f).toDp() to (initialRect?.topLeft?.y ?: 0f).toDp()
}
val (width, height) = with(LocalDensity.current) {
(initialRect?.width ?: 0f).toDp() to (initialRect?.height ?: 0f).toDp()
}
//动画监听器
var complete by remember {
mutableStateOf(false)
}
//利用该动画监听器定义的初态到末态的动画
val animX by animateDpAsState(targetValue = if (complete) 0.dp else x, label = "x")
val animY by animateDpAsState(targetValue = if (complete) 0.dp else y, label = "y") {
//在任意一个动画内实现监听器,一定要在关闭动画执行后再将key置为default
if (!complete) {
key = -1
}
}
val animWidth by animateDpAsState(targetValue = if (complete) with(LocalDensity.current) { rootRect!!.width.toDp() } else width,
label = "width")
val animHeight by animateDpAsState(targetValue = if (complete) with(LocalDensity.current) { rootRect!!.height.toDp() } else height,
label = "height")
//弹出层布局
Card(
modifier = Modifier
//绑定偏移和大小,确保覆盖点击到的`Card`
.offset(x = animX, y = animY)
.size(width = animWidth, height = animHeight)
) {
//启动动画
LaunchedEffect(key1 = Unit, block = {
complete = true
})
//弹出层实际UI
TopAppBar(title = {
Text(text = "$key")
}, navigationIcon = {
IconButton(onClick = {
//关闭动画
complete = false
}) {
Icon(imageVector = Icons.Outlined.ArrowBack, contentDescription = "")
}
})
}
}
}
}
}
}
}