refactor: decoupling menuMaxHeight and boxes in DropdownMenu

This commit is contained in:
2023-11-21 05:32:38 +08:00
parent d70d3d24d6
commit bac92adff5
4 changed files with 119 additions and 132 deletions

View File

@@ -26,53 +26,39 @@ package com.highcapable.flexiui.component
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.node.Ref import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp
import com.highcapable.flexiui.interaction.rippleClickable
import kotlin.math.max import kotlin.math.max
@Composable @Composable
internal actual fun DropdownListBox( internal actual fun DropdownMenuMeasureBox(
expanded: Boolean, menuMaxHeight: (Dp) -> Unit,
onExpandedChange: (Boolean) -> Unit, content: @Composable BoxScope.() -> Unit
modifier: Modifier,
properties: DropdownListProperties,
menuHeightPx: (Int) -> Unit,
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) { ) {
val density = LocalDensity.current
val view = LocalView.current val view = LocalView.current
val coordinates = remember { Ref<LayoutCoordinates>() } val coordinates = remember { Ref<LayoutCoordinates>() }
BoxWithConstraints( BoxWithConstraints(
modifier = Modifier.dropdownList( modifier = Modifier.onGloballyPositioned {
properties = properties, coordinates.value = it
modifier = modifier.rippleClickable( updateHeight(view.rootView, coordinates.value) { newHeight -> menuMaxHeight(with(density) { newHeight.toDp() }) }
enabled = properties.enabled, },
role = Role.DropdownList,
interactionSource = properties.interactionSource
) {
properties.focusRequester.requestFocus()
onExpandedChange(!expanded)
}.onGloballyPositioned {
coordinates.value = it
updateHeight(view.rootView, coordinates.value) { newHeight -> menuHeightPx(newHeight) }
}
),
content = content content = content
) )
DisposableEffect(view) { DisposableEffect(view) {
val listener = OnGlobalLayoutListener(view) { val listener = OnGlobalLayoutListener(view) {
updateHeight(view.rootView, coordinates.value) { newHeight -> menuHeightPx(newHeight) } updateHeight(view.rootView, coordinates.value) { newHeight -> menuMaxHeight(with(density) { newHeight.toDp() }) }
} }
onDispose { listener.dispose() } onDispose { listener.dispose() }
} }

View File

@@ -41,6 +41,8 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
@@ -59,6 +61,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -66,7 +69,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.FocusManager
@@ -167,7 +169,6 @@ fun DropdownList(
val focused by interactionSource.collectIsFocusedAsState() val focused by interactionSource.collectIsFocusedAsState()
val hovered by interactionSource.collectIsHoveredAsState() val hovered by interactionSource.collectIsHoveredAsState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var menuHeightPx by remember { mutableStateOf(0) }
val startPadding = style.startPadding.orElse() ?: style.padding val startPadding = style.startPadding.orElse() ?: style.padding
val endPadding = style.endPadding.orElse() ?: style.padding val endPadding = style.endPadding.orElse() ?: style.padding
val animatedEndIconTint by animateColorAsState(when { val animatedEndIconTint by animateColorAsState(when {
@@ -187,22 +188,25 @@ fun DropdownList(
focused || hovered -> style.borderInactive focused || hovered -> style.borderInactive
else -> style.borderInactive else -> style.borderInactive
}.copy(animatedBorderWidth, SolidColor(animatedBorderColor)) }.copy(animatedBorderWidth, SolidColor(animatedBorderColor))
DropdownListBox( DropdownMenuBox(
expanded = expanded, modifier = Modifier.dropdownList(
onExpandedChange = onExpandedChange,
modifier = modifier,
properties = DropdownListProperties(
colors = colors, colors = colors,
style = style, style = style,
border = border, border = border,
enabled = enabled, enabled = enabled,
focusRequester = focusRequester, focusRequester = focusRequester,
interactionSource = interactionSource interactionSource = interactionSource,
), modifier = modifier.rippleClickable(
menuHeightPx = { menuHeightPx = it } enabled = enabled,
role = Role.DropdownList,
interactionSource = interactionSource
) {
focusRequester.requestFocus()
onExpandedChange(!expanded)
}
)
) { ) {
val menuWidth = maxWidth + startPadding + endPadding val menuMaxWidth = maxWidth + startPadding + endPadding
val menuHeight = with(LocalDensity.current) { menuHeightPx.toDp() }
// Note: If minWidth is not 0, a constant width is currently set. // Note: If minWidth is not 0, a constant width is currently set.
// At this time, the child layout must be completely filled into the parent layout. // At this time, the child layout must be completely filled into the parent layout.
val needInflatable = minWidth > 0.dp val needInflatable = minWidth > 0.dp
@@ -223,7 +227,7 @@ fun DropdownList(
expanded = expanded, expanded = expanded,
onDismissRequest = { onExpandedChange(false) }, onDismissRequest = { onExpandedChange(false) },
offset = DefaultDropdownListMenuOffset, offset = DefaultDropdownListMenuOffset,
modifier = Modifier.width(menuWidth).heightIn(max = menuHeight), modifier = Modifier.width(menuMaxWidth).heightIn(max = menuMaxHeight),
colors = menuColors, colors = menuColors,
style = menuStyle, style = menuStyle,
scrollState = scrollState, scrollState = scrollState,
@@ -276,6 +280,46 @@ fun DropdownMenu(
} }
} }
@Composable
fun DropdownMenuBox(
modifier: Modifier = Modifier,
content: @Composable DropdownMenuBoxScope.() -> Unit
) {
var menuMaxHeight by remember { mutableStateOf(Dp.Unspecified) }
DropdownMenuMeasureBox(menuMaxHeight = { menuMaxHeight = it }) {
BoxWithConstraints(modifier = modifier) {
val currentConstraints = constraints
val currentMaxHeight = maxHeight
val currentMaxWidth = maxWidth
val currentMinHeight = minHeight
val currentMinWidth = minWidth
fun Modifier.currentAlign(alignment: Alignment) = align(alignment).then(modifier)
fun Modifier.currentMatchParentSize() = matchParentSize().then(modifier)
object : DropdownMenuBoxScope {
override val menuMaxHeight = menuMaxHeight
override val constraints get() = currentConstraints
override val maxHeight get() = currentMaxHeight
override val maxWidth get() = currentMaxWidth
override val minHeight get() = currentMinHeight
override val minWidth get() = currentMinWidth
override fun Modifier.align(alignment: Alignment) = currentAlign(alignment)
override fun Modifier.matchParentSize() = currentMatchParentSize()
}.content()
}
}
}
@Composable
internal expect fun DropdownMenuMeasureBox(
menuMaxHeight: (Dp) -> Unit,
content: @Composable BoxScope.() -> Unit
)
@Stable
interface DropdownMenuBoxScope : BoxWithConstraintsScope {
val menuMaxHeight: Dp
}
@Composable @Composable
fun DropdownMenuItem( fun DropdownMenuItem(
onClick: () -> Unit, onClick: () -> Unit,
@@ -368,44 +412,29 @@ private fun DropdownMenuContent(
} }
} }
@Composable private fun Modifier.dropdownList(
internal expect fun DropdownListBox( colors: DropdownListColors,
expanded: Boolean, style: DropdownListStyle,
onExpandedChange: (Boolean) -> Unit, border: BorderStroke,
modifier: Modifier, enabled: Boolean,
properties: DropdownListProperties, focusRequester: FocusRequester,
menuHeightPx: (Int) -> Unit, interactionSource: MutableInteractionSource,
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
)
internal fun Modifier.dropdownList(
properties: DropdownListProperties,
modifier: Modifier modifier: Modifier
) = status(properties.enabled) ) = status(enabled)
.focusRequester(properties.focusRequester) .focusRequester(focusRequester)
.focusable(properties.enabled, properties.interactionSource) .focusable(enabled, interactionSource)
.hoverable(properties.interactionSource, properties.enabled) .hoverable(interactionSource, enabled)
.clip(properties.style.shape) .clip(style.shape)
.background(properties.colors.backgroundColor, properties.style.shape) .background(colors.backgroundColor, style.shape)
.borderOrNot(properties.border, properties.style.shape) .borderOrNot(border, style.shape)
.then(modifier) .then(modifier)
.padding( .padding(
top = properties.style.topPadding.orElse() ?: properties.style.padding, top = style.topPadding.orElse() ?: style.padding,
start = properties.style.startPadding.orElse() ?: properties.style.padding, start = style.startPadding.orElse() ?: style.padding,
bottom = properties.style.bottomPadding.orElse() ?: properties.style.padding, bottom = style.bottomPadding.orElse() ?: style.padding,
end = properties.style.endPadding.orElse() ?: properties.style.padding end = style.endPadding.orElse() ?: style.padding
) )
@Immutable
internal data class DropdownListProperties(
val colors: DropdownListColors,
val style: DropdownListStyle,
val border: BorderStroke,
val enabled: Boolean,
val focusRequester: FocusRequester,
val interactionSource: MutableInteractionSource
)
private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin {
val pivotX = when { val pivotX = when {
menuBounds.left >= parentBounds.right -> 0f menuBounds.left >= parentBounds.right -> 0f

View File

@@ -23,47 +23,33 @@
package com.highcapable.flexiui.component package com.highcapable.flexiui.component
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.toIntRect import androidx.compose.ui.unit.toIntRect
import com.highcapable.flexiui.interaction.rippleClickable
import kotlin.math.max import kotlin.math.max
@Composable @Composable
internal actual fun DropdownListBox( internal actual fun DropdownMenuMeasureBox(
expanded: Boolean, menuMaxHeight: (Dp) -> Unit,
onExpandedChange: (Boolean) -> Unit, content: @Composable BoxScope.() -> Unit
modifier: Modifier,
properties: DropdownListProperties,
menuHeightPx: (Int) -> Unit,
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) { ) {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current val windowInfo = LocalWindowInfo.current
BoxWithConstraints( Box(
modifier = Modifier.dropdownList( modifier = Modifier.onGloballyPositioned {
properties = properties, val boundsInWindow = it.boundsInWindow()
modifier = modifier.rippleClickable( val visibleWindowBounds = windowInfo.containerSize.toIntRect()
enabled = properties.enabled, val heightAbove = boundsInWindow.top - visibleWindowBounds.top
role = Role.DropdownList, val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
interactionSource = properties.interactionSource menuMaxHeight(with(density) { max(heightAbove, heightBelow).toDp() })
) { },
properties.focusRequester.requestFocus()
onExpandedChange(!expanded)
}.onGloballyPositioned {
val boundsInWindow = it.boundsInWindow()
val visibleWindowBounds = windowInfo.containerSize.toIntRect()
val heightAbove = boundsInWindow.top - visibleWindowBounds.top
val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
menuHeightPx(max(heightAbove, heightBelow).toInt())
}
),
content = content content = content
) )
} }

View File

@@ -23,47 +23,33 @@
package com.highcapable.flexiui.component package com.highcapable.flexiui.component
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.toIntRect import androidx.compose.ui.unit.toIntRect
import com.highcapable.flexiui.interaction.rippleClickable
import kotlin.math.max import kotlin.math.max
@Composable @Composable
internal actual fun DropdownListBox( internal actual fun DropdownMenuMeasureBox(
expanded: Boolean, menuMaxHeight: (Dp) -> Unit,
onExpandedChange: (Boolean) -> Unit, content: @Composable BoxScope.() -> Unit
modifier: Modifier,
properties: DropdownListProperties,
menuHeightPx: (Int) -> Unit,
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) { ) {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current val windowInfo = LocalWindowInfo.current
BoxWithConstraints( Box(
modifier = Modifier.dropdownList( modifier = Modifier.onGloballyPositioned {
properties = properties, val boundsInWindow = it.boundsInWindow()
modifier = modifier.rippleClickable( val visibleWindowBounds = windowInfo.containerSize.toIntRect()
enabled = properties.enabled, val heightAbove = boundsInWindow.top - visibleWindowBounds.top
role = Role.DropdownList, val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
interactionSource = properties.interactionSource menuMaxHeight(with(density) { max(heightAbove, heightBelow).toDp() })
) { },
properties.focusRequester.requestFocus()
onExpandedChange(!expanded)
}.onGloballyPositioned {
val boundsInWindow = it.boundsInWindow()
val visibleWindowBounds = windowInfo.containerSize.toIntRect()
val heightAbove = boundsInWindow.top - visibleWindowBounds.top
val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
menuHeightPx(max(heightAbove, heightBelow).toInt())
}
),
content = content content = content
) )
} }