前言
在使用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容器,否则会抛异常。
结束
分享到此为止,如果有问题欢迎一起交流学习,感谢你的阅读。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22052,转载请注明出处。


评论0