抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

简单粗暴的写了一个粗糙的 Compose ToolTips

对于用过一段时间的 Compose 开发者来说,Compose 目前在完善性上还有一些差距,比如原生的 ToolTips,当然网上也有很多库用来支持,但我觉得有点大材小用。

毕竟,一个 ToolTips 只需要排列一下,然后一定时间内消失就行,于是自己写一下吧,不考虑边界还是灰常简单的~

拆解

主要拆解为四个部分:

一些基础属性定义:

private var basePositionMap = mutableMapOf<String, Offset>()
private var baseSizeMap = mutableMapOf<String, IntSize>()
private var tips by mutableStateOf("")
private var tipsDismissJob: Job? = null

Modifier 修饰符定义,主要是监听手势操作:

@Stable
fun Modifier.toolTips(
    onClick: () -> Unit, desc: String
) = this.then(
    Modifier
        .onGloballyPositioned {
            basePositionMap[desc] = it.positionInRoot()
            baseSizeMap[desc] = it.size
        }
        .pointerInput(Unit) {
            detectTapGestures(onTap = {
                onClick.invoke()
            }, onLongPress = {
                tips = desc
            })
        }
)

ToolTips 控件本身:

@Composable
fun ToolTips(content: @Composable () -> Unit = { DefaultToolTips() }) {
    if (tips.isNotEmpty()) {
        ToolTipsBox(Modifier) {
            content()
        }
    }
    val scope = LocalLifecycleOwner.current.lifecycleScope
    LaunchedEffect(tips) {
        tipsDismissJob?.cancel()
        tipsDismissJob = scope.launch {
            delay(2000)
            if (isActive) {
                tips = ""
            }
        }
    }
}

默认 ToolTips 样式:

@Composable
fun DefaultToolTips() {
    Box(
        modifier = Modifier
            .clip(Shapes2)
            .background(color = colorResource(id = R.color.colorOnContent))
            .padding(horizontal = 8.dp, vertical = 4.dp)
    ) {
        Text(text = tips, fontSize = 12.sp, color = colorResource(id = R.color.colorContent))
    }
}

用于布局 TipsBox 位置:

@Composable
private fun ToolTipsBox(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    val position = basePositionMap[tips] ?: return
    val size = baseSizeMap[tips] ?: return
    val y = with(LocalDensity.current) { position.y + 32.dp.toPx() }
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        layout(constraints.minWidth, constraints.minHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(x = (position.x - placeable.width / 2f + size.width / 2f).toInt(), y = y.toInt())
            }
        }
    }
}

用法:

setContent {
    Box(modifier = Modifier.fillMaxWidth()) {
        val contentDescription = "描述文字"
        Icon(
            modifier = Modifier
                //...
                .toolTips(onClick = {
                    //...
                }, contentDescription)
        )
        ToolTips()
    }
}

至于思路,就是存下了长按控件的位置、大小,然后再根据这个位置布局一下 ToolTips 的位置,最后这个 ToolTips 会在一定时间后消失,就这么简单。

但真用起来,其实问题不少,比如可能超过父布局边界,比如可能需要换个方位,就自行调整或者直接用别人设计的更具备实用性的库吧,毕竟我这就图个轻便~

更新

修整得规范了些,越界问题也处理了,用法类似,只是说 需要多传一个 rememberToolTipsState,避免离开界面了,这玩意儿还没消失。

@Composable
fun rememberToolTipsState(marginTop: Int = 0, marginHorizontal: Int = 0): ToolTipState {
    return remember {
        ToolTipState().apply {
            this.marginTop = marginTop
            this.marginHorizontal = marginHorizontal
        }
    }
}

class ToolTipState {
    val positionMap = mutableMapOf<String, Offset>()
    val sizeMap = mutableMapOf<String, IntSize>()

    var tips by mutableStateOf("")
        private set
    var tipsDismissJob: Job? = null

    var marginTop: Int = 0
    var marginHorizontal: Int = 0

    fun show(tips: String) {
        this.tips = tips
    }

    fun dismiss() {
        tips = ""
    }
}


@Stable
fun Modifier.toolTips(
    onClick: () -> Unit = {}, desc: String, state: ToolTipState
) = this.then(
    Modifier
        .onGloballyPositioned {
            state.positionMap[desc] = it.positionInRoot()
            state.sizeMap[desc] = it.size
        }
        .pointerInput(Unit) {
            detectTapGestures(onTap = {
                onClick.invoke()
            }, onLongPress = {
                state.show(desc)
            })
        }
)

@Composable
fun ToolTips(modifier: Modifier = Modifier, state: ToolTipState, content: @Composable () -> Unit = { DefaultToolTips(state) }) {
    val visible = state.tips.isNotEmpty()
    state.tipsDismissJob?.cancel()
    AnimatedVisibility(enter = fadeIn(), exit = fadeOut(), visible = visible) {
        ToolTipsBox(modifier, state) {
            content()
        }
    }
    if (visible) {
        LaunchedEffect(state.tips) {
            state.tipsDismissJob = launch {
                delay(3000)
                if (isActive) {
                    state.dismiss()
                }
            }
        }
    }
}

以上!


本站由以下主机服务商提供服务支持:

0条评论

发表评论