Jetpack Compose 为'点击页面条目切换到新页面' 添加动画

42 阅读1分钟

一. 实现原理

  1. 在点击条目时获取我们点击到的ListItem的控件位置
  2. 使用一个新的Box标签覆盖住我们点击的ListItem
  3. 利用动画API,让覆盖在ListItem上的Box逐渐变大,直到覆盖整个页面。

二. API介绍

  • Modifier.onGloballyPositioned(onGloballyPositioned: (androidx.compose.ui.layout.LayoutCoordinates) -> kotlin.Unit)

    该扩展函数用于获取可组合项在屏幕中的位置,在测量完成后,lambda将会被调用。

    注意:为了能正常覆盖我们的控件,Rect的获取需要使用boundsInRoot,而不是boundsInWindow,以避免状态栏高度带来的控件偏移。

  • LocalDensity.current

    该组合项会返回一个Density对象。利用此对象,我们可以快捷的换算dppx的关系。

三. 源码和注释

 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 = "")
                                 }
                             })
                         }
                     }
                 }
             }
         }
     }
 }

四. 运行结果

gif.75team.com.gif