I'm writing a button that similar to the FAB that locates at the bottom right, it's supposed to work like:
Click the Button -> Popup a DropdownMenu with an "info" icon -> Click the icon to show a Tooltip beside it.
But the tooltip is not aligned to the "info" icon, why is that?
The code:
class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Scaffold {
// Full fill the entire screen
Box(modifier = Modifier.fillMaxSize()) {
// This Box is for aligning the content at the bottom end,
// and DropdownMenu requires a Box container.
Box(
modifier = Modifier.align(Alignment.BottomEnd)
) {
var dropdownExpanded by remember { mutableStateOf(false) }
Button(
onClick = { dropdownExpanded = !dropdownExpanded },
) {
Text("dropdown")
}
DropdownMenu(
expanded = dropdownExpanded,
onDismissRequest = {},
) {
TooltipImage() // the only menu item
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipImage() {
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
TooltipBox(
positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(),
tooltip = {
RichTooltip { Text("tooltip content") }
},
state = tooltipState
) {
Image(
painter = painterResource(id = android.R.drawable.ic_menu_info_details),
contentDescription = "",
modifier = Modifier
.clickable {
scope.launch { tooltipState.show() }
}
)
}
}
Edited:
The answer is correct, however, when I add a TextField on that Dropdown and when it's focused, the input caret is also mis-aligned at the top. The TextField doesn't seem to support the position provicer, I ended up using popup dialog instead of dropdown menu.
The reason this happens is when you open a DropDownMenu
which is a PopUp
you create a new window with anchor positioned in (0,22) on my emulator, you can check how a DropDown and Tooltip is positioned in this answer.
And if you debug TooltipDefaults.rememberRichTooltipPositionProvider()
you can see anchorBounds are not where they supposed to be on screen.
What you can do get position of DropDownMenu
on screen
with
Modifier
.onGloballyPositioned {
offset = it.positionOnScreen()
}
write your own PopupPositionProvider
with this offset for TooltipBox as
@Composable
fun rememberPositionProvider(
spacingBetweenTooltipAndAnchor: Dp = SpacingBetweenTooltipAndAnchor,
userOffset: IntOffset = IntOffset.Zero
): PopupPositionProvider {
val tooltipAnchorSpacing = with(LocalDensity.current) {
spacingBetweenTooltipAndAnchor.roundToPx()
}
return remember(tooltipAnchorSpacing, userOffset) {
object : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
val newBounds = anchorBounds.translate(userOffset)
var x = newBounds.right
// Try to shift it to the left of the anchor
// if the tooltip would collide with the right side of the screen
if (x + popupContentSize.width > windowSize.width) {
x = newBounds.left - popupContentSize.width
// Center if it'll also collide with the left side of the screen
if (x < 0)
x = newBounds.left +
(newBounds.width - popupContentSize.width) / 2
}
// 🔥 This is a line i added you might check for right side
// overflowing as well
x += popupContentSize.width / 2 + anchorBounds.width / 2
// Tooltip prefers to be above the anchor,
// but if this causes the tooltip to overlap with the anchor
// then we place it below the anchor
var y = newBounds.top - popupContentSize.height - tooltipAnchorSpacing
if (y < 0)
y = newBounds.bottom + tooltipAnchorSpacing
return IntOffset(x, y)
}
}
}
}
internal val SpacingBetweenTooltipAndAnchor = 4.dp
And apply it as
@Preview
@Composable
fun TooltipTest() {
Scaffold {
// Full fill the entire screen
Box(modifier = Modifier.fillMaxSize()) {
// This Box is for aligning the content at the bottom end,
// and DropdownMenu requires a Box container.
Box(
modifier = Modifier.align(Alignment.BottomEnd)
) {
var dropdownExpanded by remember { mutableStateOf(false) }
Button(
onClick = { dropdownExpanded = !dropdownExpanded },
) {
Text("dropdown")
}
var offset by remember {
mutableStateOf(Offset.Zero)
}
DropdownMenu(
modifier = Modifier
.onGloballyPositioned {
offset = it.positionOnScreen()
},
expanded = dropdownExpanded,
onDismissRequest = {},
) {
TooltipImage(
offset = offset.round()
) // the only menu item
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TooltipImage(
offset: IntOffset
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val scope = rememberCoroutineScope()
TooltipBox(
positionProvider = rememberPositionProvider(userOffset = offset),
tooltip = {
RichTooltip { Text("tooltip content") }
},
state = tooltipState
) {
Image(
painter = painterResource(id = android.R.drawable.ic_menu_info_details),
contentDescription = "",
modifier = Modifier
.clickable {
scope.launch { tooltipState.show() }
}
)
}
}
Result