# Jetpack Compose – UI定位 站点:答源(https://www.dandroid.cn/) 原文:https://www.dandroid.cn/archives/22052 Canonical:https://www.dandroid.cn/archives/22052 作者:admin 发布日期:2024-09-30 最后更新:2024-09-30 01:47 分类:技术方案 内容类型:post 说明:本页是原文的纯文本镜像,供大模型与检索器完整读取;不含导航、广告、付费框。引用请以上方「原文」链接为准。 ## 正文 前言 在使用Compose开发过程中,有些场景需要把UI定位到某个位置,使用compose-layer可以轻松实现UI定位功能。 场景一 类似微信会话列表的长按菜单,可以根据长按位置,智能选择有足够空间的地方来展示,我们来实现一下这个功能。 设置LayerContainer容器 首先要设置一个LayerContainer容器用来显示弹出的Layer: class SampleListMenu : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { LayerContainer { Content() } } } } } 点击坐标: @Composable private fun ListItem( modifier: Modifier = Modifier, text: String, onOffset: (IntOffset?) -> Unit, ) { val onOffsetUpdated by rememberUpdatedState(onOffset) var coordinates: LayoutCoordinates? by remember { mutableStateOf(null) } Box( modifier = modifier .fillMaxWidth() .height(50.dp) .onGloballyPositioned { coordinates = it } .pointerInput(Unit) { detectTapGestures { val offset = coordinates?.localToWindow(it)?.round() onOffsetUpdated(offset) } } ) { Text(text = text, modifier = Modifier.align(Alignment.Center)) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } } ListItem是列表Item,点击时通过LayoutCoordinates计算点击位置相对于Window的坐标,并通知回调对象。 创建TargetLayer @Composable private fun Content() { var attach by remember { mutableStateOf(false) } var offset: IntOffset? by remember { mutableStateOf(null) } LazyColumn(modifier = Modifier.fillMaxSize()) { items(100) { index -> ListItem( text = index.toString(), onOffset = { offset = it attach = true }, ) } } TargetLayer( target = LayerTarget.Offset(offset), attach = attach, onDetachRequest = { attach = false }, backgroundColor = Color.Transparent, detachOnBackPress = true, detachOnTouchBackground = true, alignment = TargetAlignment.BottomCenter, smartAlignments = SmartAliments.Default, ) { Menus { attach = false } } } 调用TargetLayer创建弹出层,弹出层的内容是Menus菜单组合项,具体代码就不展示了。TargetLayer虽然是在Content组合中创建的,但它的内容实际上是显示在LayerContainer容器中。 上述代码已经实现功能了,代码不多,但参数比较多,下面一一解释: attach变量用来控制是否显示Layer,即是否把Layer的内容添加到LayerContainer中,true添加,false移除,把它传给TargetLayer即可 offset变量用来保存点击的坐标点,即目标点,通过LayerTarget.Offset(offset)构建一个目标传给TargetLayer onDetachRequest是请求移除Layer的回调,例如按返回键或者触摸背景区域时回调onDetachRequest,在回调里把attach参数修改为false,即可移除Layer detachOnBackPress按返回键是否请求移除Layer,true请求移除;false不请求移除;null不处理返回键事件,默认值true。注意:true和false都会消费本次返回键事件,而null则完全忽略返回键事件,如果设置为null,按下返回键,会继续传播事件 detachOnTouchBackground触摸背景区域是否请求移除Layer,true请求移除;false不请求移除;null不处理事件,事件会穿透背景,默认值false。由于现在手机屏幕越来越大,可能误触背景区域导致移除Layer,所以默认值是false,可以根据实际需求配置 alignment可以设置Layer和目标的对齐位置,这里设置为BottomCenter表示显示在目标底部,水平方向和目标中心点对齐,它的默认值是Center中心点对齐 重点来啦,smartAlignments参数是智能对齐目标的意思,只有在alignment参数导致内容溢出时,才会从smartAlignments中按顺序查找内容溢出最小的位置,可以看一下这个类: @Immutable data class SmartAliments( val aliments: List, ) { constructor(vararg array: SmartAliment) : this(array.toList()) companion object { val Default = SmartAliments( SmartAliment(TargetAlignment.BottomEnd), SmartAliment(TargetAlignment.BottomStart), SmartAliment(TargetAlignment.TopEnd), SmartAliment(TargetAlignment.TopStart), ) } } @Immutable data class SmartAliment( val alignment: TargetAlignment, val transition: LayerTransition? = null, ) 可以看到SmartAliments内部实际上只是一个简单的列表,列表项是SmartAliment: SmartAliment.alignment要对齐的位置 SmartAliment.transition该位置对应的动画,默认值是null,会根据对齐位置自动选择合适的动画 SmartAliments.Default配置了4个默认位置,可以根据实际需求创建SmartAliments。 场景二 类似微信朋友圈的点赞和评论菜单,我们来实现一下这个功能。 首先需要在Item布局里面为按钮添加tag,添加之后这个tag对应的按钮就是Layer要定位的目标: @Composable private fun ListItem( modifier: Modifier = Modifier, tag: String, onClick: (String) -> Unit, ) { Box( modifier = modifier .fillMaxWidth() .heightIn(200.dp) ) { IconButton( onClick = { onClick(tag) }, modifier = Modifier.align(Alignment.BottomEnd) .layerTag(tag) ) { Icon(Icons.Default.MoreVert, "more") } HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) } } IconButton是更多按钮,通过Modifier.layerTag为它设置一个tag,在点击的时候回调tag。 创建TargetLayer: @Composable private fun Content() { var attach by remember { mutableStateOf(false) } var tag by remember { mutableStateOf("") } LazyColumn( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { detectTapGestures( onPress = { attach = false } ) }, ) { items(100) { index -> ListItem( tag = index.toString(), onClick = { tag = it attach = true }, ) } } TargetLayer( target = LayerTarget.Tag(tag), attach = attach, onDetachRequest = { attach = false }, backgroundColor = Color.Transparent, detachOnTouchBackground = null, alignment = TargetAlignment.StartCenter, ) { } } tag变量用来保存Item回调的目标,然后通过LayerTarget.Tag(tag)构建一个目标传递给LayerTarget。 微信朋友圈菜单弹出的时候,列表仍然可以滑动,并且滑动的时候会关闭菜单。可以把detachOnTouchBackground设置为null,让触摸事件就可以穿透背景区域,再通过detectTapGestures监听触摸事件移除Layer即可。 有个细节:列表滑动的时候,更多按钮已经滑走了,而Layer收回动画还在原来的位置,微信的做法是不显示收回动画,直接隐藏菜单。我们也可以通过设置动画来解决这个问题: TargetLayer( ... transition = LayerTransition.slideRightToLeft( exit = ExitTransition.None ) ) LayerTransition.slideRightToLeft会创建从右向左的enter(进入)动画,以及从左向右的exit(退出)动画,只要把exit设置为ExitTransition.None即可关闭退出动画。 @Immutable data class LayerTransition( val enter: EnterTransition, val exit: ExitTransition, ) 场景三 输入框输入一些内容后,下拉框会有建议列表,这也是比较常见的场景,我们来实现一下这个功能。 @Composable private fun Content() { var attach by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize()) { InputBar( tag = "abc", showPopMenu = { attach = it } ) } TargetLayer( target = LayerTarget.Tag("abc"), attach = attach, onDetachRequest = { attach = false }, detachOnTouchBackground = true, alignment = TargetAlignment.BottomCenter, clipBackgroundDirection = Directions.Top, ) { } } InputBar是一个输入框,给它传递一个tag为"abc"参数,其内部还是通过Modifier.layerTag来为输入框设置tag,上面已经介绍过了这里就不赘述,然后监听输入框的回调showPopMenu是否显示Layer。 最后要把"abc"这个tag也传递给TargetLayer,这样子它才能定位到输入框这个目标。 这里多了一个还没见过的参数:clipBackgroundDirection,它的意思是要裁切Layer哪些方向的背景,设置为Directions.Top表示裁切Layer顶部方向的背景,如果不裁切的话输入框会被背景所遮挡。 Directions支持上下左右4个方向,支持同时裁剪多个方向的背景,如果要同时裁切顶部和底部的话,可以设置为:Directions.Top + Directions.Bottom 有时候需要在对齐目标之后做一些位置微调,例如输入框下面弹出的建议列表往下面挪一点,可以通过以下代码实现: TargetLayer( ... alignmentOffsetY = TargetAlignmentOffset.PX(200) ) alignmentOffsetY表示Y方向偏移,alignmentOffsetX表示X方向偏移,参数类型是TargetAlignmentOffset: @Immutable sealed class TargetAlignmentOffset { data class PX(val value: Int) : TargetAlignmentOffset() data class DP(val value: Int) : TargetAlignmentOffset() data class Target(val value: Float) : TargetAlignmentOffset() } TargetAlignmentOffset.PX表示按像素偏移,支持正数和负数,以Y轴为例,大于0往下偏移,小于0往上偏移 TargetAlignmentOffset.DP表示以dp为单位计算偏移,偏移逻辑和TargetAlignmentOffset.PX一样 TargetAlignmentOffset.Target表示按目标的大小偏移,支持正数和负数字,以Y轴为例,1表示往下偏移1倍目标的高度,-1表示往上偏移1倍目标的高度 TargetAlignment 我们已经知道可以通过TargetAlignment来设置要对齐的位置,具体都有哪些位置可以看下面的效果图: TargetLayer vs Layer 这篇文章主要讲的是定位,所以用到的都是TargetLayer,实际上还有一个Layer,它没有定位功能,可以把它当作Compose Dialog来使用: @Composable fun Layer( attach: Boolean, onDetachRequest: (LayerDetach) -> Unit, debug: Boolean = false, detachOnBackPress: Boolean? = true, detachOnTouchBackground: Boolean? = false, backgroundColor: Color = Color.Black.copy(alpha = 0.3f), alignment: Alignment = Alignment.Center, transition: LayerTransition? = null, zIndex: Float = 0f, content: @Composable LayerContentScope.() -> Unit, ) 参数alignment是Compose标准类Alignment,相当于在Box中为UI设置对齐参数 参数zIndex可以设置Layer在z轴的层级,值越大显示在越上层 其他参数上面已经介绍过就不赘述了。 不管是TargetLayer还是Layer,最终它的内容都会显示在LayerContainer中,所以外层要设置LayerContainer容器,否则会抛异常。 结束 分享到此为止,如果有问题欢迎一起交流学习,感谢你的阅读。 --- 由 答源 DAN 底座 1.4.1 生成 · 2026-11-13T08:05:34+00:00