mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-07 19:14:12 +08:00
refactor: decoupling menuMaxHeight and boxes in DropdownMenu
This commit is contained in:
@@ -26,53 +26,39 @@ package com.highcapable.flexiui.component
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.UiComposable
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.node.Ref
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.highcapable.flexiui.interaction.rippleClickable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal actual fun DropdownListBox(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier,
|
||||
properties: DropdownListProperties,
|
||||
menuHeightPx: (Int) -> Unit,
|
||||
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||
internal actual fun DropdownMenuMeasureBox(
|
||||
menuMaxHeight: (Dp) -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val view = LocalView.current
|
||||
val coordinates = remember { Ref<LayoutCoordinates>() }
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.dropdownList(
|
||||
properties = properties,
|
||||
modifier = modifier.rippleClickable(
|
||||
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) }
|
||||
}
|
||||
),
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
coordinates.value = it
|
||||
updateHeight(view.rootView, coordinates.value) { newHeight -> menuMaxHeight(with(density) { newHeight.toDp() }) }
|
||||
},
|
||||
content = content
|
||||
)
|
||||
DisposableEffect(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() }
|
||||
}
|
||||
|
@@ -41,6 +41,8 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
@@ -59,6 +61,7 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -66,7 +69,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.UiComposable
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
@@ -167,7 +169,6 @@ fun DropdownList(
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
var menuHeightPx by remember { mutableStateOf(0) }
|
||||
val startPadding = style.startPadding.orElse() ?: style.padding
|
||||
val endPadding = style.endPadding.orElse() ?: style.padding
|
||||
val animatedEndIconTint by animateColorAsState(when {
|
||||
@@ -187,22 +188,25 @@ fun DropdownList(
|
||||
focused || hovered -> style.borderInactive
|
||||
else -> style.borderInactive
|
||||
}.copy(animatedBorderWidth, SolidColor(animatedBorderColor))
|
||||
DropdownListBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
modifier = modifier,
|
||||
properties = DropdownListProperties(
|
||||
DropdownMenuBox(
|
||||
modifier = Modifier.dropdownList(
|
||||
colors = colors,
|
||||
style = style,
|
||||
border = border,
|
||||
enabled = enabled,
|
||||
focusRequester = focusRequester,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
menuHeightPx = { menuHeightPx = it }
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier.rippleClickable(
|
||||
enabled = enabled,
|
||||
role = Role.DropdownList,
|
||||
interactionSource = interactionSource
|
||||
) {
|
||||
focusRequester.requestFocus()
|
||||
onExpandedChange(!expanded)
|
||||
}
|
||||
)
|
||||
) {
|
||||
val menuWidth = maxWidth + startPadding + endPadding
|
||||
val menuHeight = with(LocalDensity.current) { menuHeightPx.toDp() }
|
||||
val menuMaxWidth = maxWidth + startPadding + endPadding
|
||||
// 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.
|
||||
val needInflatable = minWidth > 0.dp
|
||||
@@ -223,7 +227,7 @@ fun DropdownList(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { onExpandedChange(false) },
|
||||
offset = DefaultDropdownListMenuOffset,
|
||||
modifier = Modifier.width(menuWidth).heightIn(max = menuHeight),
|
||||
modifier = Modifier.width(menuMaxWidth).heightIn(max = menuMaxHeight),
|
||||
colors = menuColors,
|
||||
style = menuStyle,
|
||||
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
|
||||
fun DropdownMenuItem(
|
||||
onClick: () -> Unit,
|
||||
@@ -368,44 +412,29 @@ private fun DropdownMenuContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal expect fun DropdownListBox(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier,
|
||||
properties: DropdownListProperties,
|
||||
menuHeightPx: (Int) -> Unit,
|
||||
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||
)
|
||||
|
||||
internal fun Modifier.dropdownList(
|
||||
properties: DropdownListProperties,
|
||||
private fun Modifier.dropdownList(
|
||||
colors: DropdownListColors,
|
||||
style: DropdownListStyle,
|
||||
border: BorderStroke,
|
||||
enabled: Boolean,
|
||||
focusRequester: FocusRequester,
|
||||
interactionSource: MutableInteractionSource,
|
||||
modifier: Modifier
|
||||
) = status(properties.enabled)
|
||||
.focusRequester(properties.focusRequester)
|
||||
.focusable(properties.enabled, properties.interactionSource)
|
||||
.hoverable(properties.interactionSource, properties.enabled)
|
||||
.clip(properties.style.shape)
|
||||
.background(properties.colors.backgroundColor, properties.style.shape)
|
||||
.borderOrNot(properties.border, properties.style.shape)
|
||||
) = status(enabled)
|
||||
.focusRequester(focusRequester)
|
||||
.focusable(enabled, interactionSource)
|
||||
.hoverable(interactionSource, enabled)
|
||||
.clip(style.shape)
|
||||
.background(colors.backgroundColor, style.shape)
|
||||
.borderOrNot(border, style.shape)
|
||||
.then(modifier)
|
||||
.padding(
|
||||
top = properties.style.topPadding.orElse() ?: properties.style.padding,
|
||||
start = properties.style.startPadding.orElse() ?: properties.style.padding,
|
||||
bottom = properties.style.bottomPadding.orElse() ?: properties.style.padding,
|
||||
end = properties.style.endPadding.orElse() ?: properties.style.padding
|
||||
top = style.topPadding.orElse() ?: style.padding,
|
||||
start = style.startPadding.orElse() ?: style.padding,
|
||||
bottom = style.bottomPadding.orElse() ?: 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 {
|
||||
val pivotX = when {
|
||||
menuBounds.left >= parentBounds.right -> 0f
|
||||
|
@@ -23,47 +23,33 @@
|
||||
|
||||
package com.highcapable.flexiui.component
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.UiComposable
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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 com.highcapable.flexiui.interaction.rippleClickable
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal actual fun DropdownListBox(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier,
|
||||
properties: DropdownListProperties,
|
||||
menuHeightPx: (Int) -> Unit,
|
||||
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||
internal actual fun DropdownMenuMeasureBox(
|
||||
menuMaxHeight: (Dp) -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val windowInfo = LocalWindowInfo.current
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.dropdownList(
|
||||
properties = properties,
|
||||
modifier = modifier.rippleClickable(
|
||||
enabled = properties.enabled,
|
||||
role = Role.DropdownList,
|
||||
interactionSource = properties.interactionSource
|
||||
) {
|
||||
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())
|
||||
}
|
||||
),
|
||||
Box(
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
val boundsInWindow = it.boundsInWindow()
|
||||
val visibleWindowBounds = windowInfo.containerSize.toIntRect()
|
||||
val heightAbove = boundsInWindow.top - visibleWindowBounds.top
|
||||
val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
|
||||
menuMaxHeight(with(density) { max(heightAbove, heightBelow).toDp() })
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
@@ -23,47 +23,33 @@
|
||||
|
||||
package com.highcapable.flexiui.component
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.UiComposable
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
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 com.highcapable.flexiui.interaction.rippleClickable
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal actual fun DropdownListBox(
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier,
|
||||
properties: DropdownListProperties,
|
||||
menuHeightPx: (Int) -> Unit,
|
||||
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||
internal actual fun DropdownMenuMeasureBox(
|
||||
menuMaxHeight: (Dp) -> Unit,
|
||||
content: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val windowInfo = LocalWindowInfo.current
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.dropdownList(
|
||||
properties = properties,
|
||||
modifier = modifier.rippleClickable(
|
||||
enabled = properties.enabled,
|
||||
role = Role.DropdownList,
|
||||
interactionSource = properties.interactionSource
|
||||
) {
|
||||
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())
|
||||
}
|
||||
),
|
||||
Box(
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
val boundsInWindow = it.boundsInWindow()
|
||||
val visibleWindowBounds = windowInfo.containerSize.toIntRect()
|
||||
val heightAbove = boundsInWindow.top - visibleWindowBounds.top
|
||||
val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom
|
||||
menuMaxHeight(with(density) { max(heightAbove, heightBelow).toDp() })
|
||||
},
|
||||
content = content
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user