Kirill Grouchnikov
11/15/2020, 9:12 PMVadim Kapustin
11/16/2020, 7:43 AM@Composable
fun iconWithHint(
asset: ImageAsset,
modifier: Modifier = Modifier,
tint: Color = AmbientContentColor.current, // for example, if something wrong, we can set color to Color.Red
popupTimeout: Long = 500,
popupShowTime: Long = 3000,
hint: @Composable() () -> Unit // for example { Text(message) }
) {
val entered = remember { mutableStateOf(0) }
val popupPosition = remember { mutableStateOf(IntOffset(0, 0)) }
var enterPosition: IntOffset
Box(modifier) {
Icon(asset,
Modifier.pointerMoveFilter(
onMove = { position ->
popupPosition.value = IntOffset(position.x.toInt(), position.y.toInt())
false
},
onEnter = {
if (entered.value == 0)
GlobalScope.launch {
entered.value = 1
delay(popupTimeout/2)
enterPosition = popupPosition.value
delay(popupTimeout/2)
if (entered.value == 1 && enterPosition == popupPosition.value)
entered.value = 2
}
false
},
onExit = {
entered.value = 0
false
}
),
tint
)
if (entered.value == 2) {
GlobalScope.launch {
val count = 10
for (i in 0.until(count)) {
if (entered.value == 0) break
delay(popupShowTime/count)
}
entered.value = 0
}
Popup(
offset = popupPosition.value
) {
Surface(
modifier = Modifier.pointerMoveFilter(
onEnter = { false },
onExit = {
entered.value = 0
false
}
),
shape = RoundedCornerShape(5.dp)
) {
hint()
}
}
}
}
}
Any improvements are welcomeIgor Demin
11/16/2020, 11:40 AMHere's what happened:I rewrote this code:
import androidx.compose.desktop.Window
import androidx.compose.foundation.clickable
import androidx.compose.material.Text
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offsetPx
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Providers
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.onDispose
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.staticAmbientOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerMoveFilter
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.Duration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.inMilliseconds
import androidx.compose.ui.unit.milliseconds
import kotlinx.coroutines.delay
fun main() = Window {
HintContainer {
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text("Left text")
WithHint(hint = {
Surface(
Modifier.padding(top = 18.dp),
color = Color.LightGray,
) {
Text("Hint", Modifier.padding(4.dp))
}
}) {
Text("Right text", Modifier.clickable {
println("Click")
})
}
}
}
}
val HintsAmbient = staticAmbientOf<SnapshotStateList<@Composable () -> Unit>>()
@Composable
fun HintContainer(children: @Composable () -> Unit) {
val hints = remember<SnapshotStateList<@Composable () -> Unit>> { mutableStateListOf() }
Providers(HintsAmbient provides hints) {
children()
Box {
for (hint in hints) {
hint()
}
}
}
}
@Composable
fun WithHint(
modifier: Modifier = Modifier,
delay: Duration = 500.milliseconds,
hint: @Composable () -> Unit, // for example { Text(message) }
content: @Composable () -> Unit
) {
val hints = HintsAmbient.current
var hoverLocalPosition by remember { mutableStateOf<Offset?>(null) }
var contentGlobalPosition by remember { mutableStateOf<Offset?>(null) }
val hintContainer = remember {
@Composable {
if (contentGlobalPosition != null && hoverLocalPosition != null) {
Box(
Modifier.offsetPx(
derivedStateOf { contentGlobalPosition!!.x + hoverLocalPosition!!.x },
derivedStateOf { contentGlobalPosition!!.y + hoverLocalPosition!!.y },
)
) {
hint()
}
}
} as (@Composable () -> Unit)
}
if (hoverLocalPosition != null) {
LaunchedEffect(Unit) {
delay(delay.inMilliseconds())
hints.add(hintContainer)
}
} else {
hints.remove(hintContainer)
}
onDispose {
hints.remove(hintContainer)
}
Box(
modifier
.pointerMoveFilter(
onMove = {
hoverLocalPosition = it
false
},
onEnter = { false },
onExit = {
hoverLocalPosition = null
false
}
)
.onGloballyPositioned {
contentGlobalPosition = it.globalPosition
}
) {
content()
}
}
Vadim Kapustin
11/17/2020, 8:27 AMSurface(shape = RoundedCornerShape(5.dp)) {
hint()
}
in this case, I sometimes get NullPointerException here:
Modifier.offsetPx(
derivedStateOf { contentGlobalPosition!!.x + hoverLocalPosition!!.x },
derivedStateOf { contentGlobalPosition!!.y + hoverLocalPosition!!.y },
)
I think this is happening when mouse move fast and between checking condition (contentGlobalPosition != null && hoverLocalPosition != null) and drawing hint box, one of the States becomes null.
I make small correction:
if (contentGlobalPosition != null && hoverLocalPosition != null) {
val offset = contentGlobalPosition!! + hoverLocalPosition!!
Box(
Modifier.offsetPx(
derivedStateOf { offset.x },
derivedStateOf { offset.y },
)
) {
Surface(shape = RoundedCornerShape(5.dp)) {
hint()
}
}
}
I hope It's correct