前言
在使用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