mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-12-12 00:33:43 +08:00
feat: add DropdownList
This commit is contained in:
@@ -0,0 +1,130 @@
|
|||||||
|
/*
|
||||||
|
* Flexi UI - A flexible and useful UI component library.
|
||||||
|
* Copyright (C) 2019-2023 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/FlexiUI
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2023/11/18.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewTreeObserver
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
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.LocalView
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import com.highcapable.flexiui.interaction.rippleClickable
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal actual fun DropdownListBox(
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
colors: DropdownListColors,
|
||||||
|
style: DropdownListStyle,
|
||||||
|
border: BorderStroke,
|
||||||
|
enabled: Boolean,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
menuHeightPx: (Int) -> Unit,
|
||||||
|
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val coordinates = remember { Ref<LayoutCoordinates>() }
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier.dropdownList(
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
border = border,
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier.rippleClickable(
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.DropdownList,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
) { onExpandedChange(!expanded) }.onGloballyPositioned {
|
||||||
|
coordinates.value = it
|
||||||
|
updateHeight(view.rootView, coordinates.value) { newHeight -> menuHeightPx(newHeight) }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
DisposableEffect(view) {
|
||||||
|
val listener = OnGlobalLayoutListener(view) {
|
||||||
|
updateHeight(view.rootView, coordinates.value) { newHeight -> menuHeightPx(newHeight) }
|
||||||
|
}
|
||||||
|
onDispose { listener.dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateHeight(view: View, coordinates: LayoutCoordinates?, onHeightUpdate: (Int) -> Unit) {
|
||||||
|
coordinates ?: return
|
||||||
|
val visibleWindowBounds = Rect().let { view.getWindowVisibleDisplayFrame(it); it }
|
||||||
|
val heightAbove = coordinates.boundsInWindow().top - visibleWindowBounds.top
|
||||||
|
val heightBelow = visibleWindowBounds.bottom - visibleWindowBounds.top - coordinates.boundsInWindow().bottom
|
||||||
|
onHeightUpdate(max(heightAbove, heightBelow).toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnGlobalLayoutListener(
|
||||||
|
private val view: View,
|
||||||
|
private val onGlobalLayoutCallback: () -> Unit
|
||||||
|
) : View.OnAttachStateChangeListener, ViewTreeObserver.OnGlobalLayoutListener {
|
||||||
|
|
||||||
|
private var isListeningToGlobalLayout = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.addOnAttachStateChangeListener(this)
|
||||||
|
registerOnGlobalLayoutListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewAttachedToWindow(p0: View) = registerOnGlobalLayoutListener()
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow(p0: View) = unregisterOnGlobalLayoutListener()
|
||||||
|
|
||||||
|
override fun onGlobalLayout() = onGlobalLayoutCallback()
|
||||||
|
|
||||||
|
private fun registerOnGlobalLayoutListener() {
|
||||||
|
if (isListeningToGlobalLayout || !view.isAttachedToWindow) return
|
||||||
|
view.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||||
|
isListeningToGlobalLayout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unregisterOnGlobalLayoutListener() {
|
||||||
|
if (!isListeningToGlobalLayout) return
|
||||||
|
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||||
|
isListeningToGlobalLayout = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
unregisterOnGlobalLayoutListener()
|
||||||
|
view.removeOnAttachStateChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,17 +23,33 @@
|
|||||||
|
|
||||||
package com.highcapable.flexiui.component
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
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.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
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -50,9 +66,13 @@ 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.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusManager
|
import androidx.compose.ui.focus.FocusManager
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.TransformOrigin
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.InputMode
|
import androidx.compose.ui.input.InputMode
|
||||||
@@ -67,6 +87,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.platform.LocalInputModeManager
|
import androidx.compose.ui.platform.LocalInputModeManager
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.IntRect
|
import androidx.compose.ui.unit.IntRect
|
||||||
@@ -79,11 +100,23 @@ import com.highcapable.flexiui.LocalColors
|
|||||||
import com.highcapable.flexiui.LocalShapes
|
import com.highcapable.flexiui.LocalShapes
|
||||||
import com.highcapable.flexiui.LocalSizes
|
import com.highcapable.flexiui.LocalSizes
|
||||||
import com.highcapable.flexiui.interaction.rippleClickable
|
import com.highcapable.flexiui.interaction.rippleClickable
|
||||||
|
import com.highcapable.flexiui.resources.Icons
|
||||||
|
import com.highcapable.flexiui.resources.icon.Dropdown
|
||||||
|
import com.highcapable.flexiui.utils.borderOrNot
|
||||||
import com.highcapable.flexiui.utils.orElse
|
import com.highcapable.flexiui.utils.orElse
|
||||||
|
import com.highcapable.flexiui.utils.solidColor
|
||||||
import com.highcapable.flexiui.utils.status
|
import com.highcapable.flexiui.utils.status
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DropdownListColors(
|
||||||
|
val endIconTint: Color,
|
||||||
|
val borderInactiveColor: Color,
|
||||||
|
val borderActiveColor: Color,
|
||||||
|
val backgroundColor: Color
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class DropdownMenuColors(
|
data class DropdownMenuColors(
|
||||||
val contentColor: Color,
|
val contentColor: Color,
|
||||||
@@ -91,6 +124,19 @@ data class DropdownMenuColors(
|
|||||||
val borderColor: Color
|
val borderColor: Color
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DropdownListStyle(
|
||||||
|
val padding: Dp,
|
||||||
|
val topPadding: Dp,
|
||||||
|
val startPadding: Dp,
|
||||||
|
val bottomPadding: Dp,
|
||||||
|
val endPadding: Dp,
|
||||||
|
val shape: Shape,
|
||||||
|
val endIconSize: Dp,
|
||||||
|
val borderInactive: BorderStroke,
|
||||||
|
val borderActive: BorderStroke
|
||||||
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class DropdownMenuStyle(
|
data class DropdownMenuStyle(
|
||||||
val inTransitionDuration: Int,
|
val inTransitionDuration: Int,
|
||||||
@@ -99,6 +145,77 @@ data class DropdownMenuStyle(
|
|||||||
val borderStyle: AreaBoxStyle
|
val borderStyle: AreaBoxStyle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DropdownList(
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
colors: DropdownListColors = DropdownList.colors,
|
||||||
|
style: DropdownListStyle = DropdownList.style,
|
||||||
|
menuColors: DropdownMenuColors = DropdownMenu.colors,
|
||||||
|
menuStyle: DropdownMenuStyle = DropdownMenu.style,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
scrollState: ScrollState = rememberScrollState(),
|
||||||
|
properties: PopupProperties = PopupProperties(focusable = true),
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
text: @Composable () -> Unit,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val hovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
var menuHeightPx by remember { mutableStateOf(0) }
|
||||||
|
val startPadding = style.startPadding.orElse() ?: style.padding
|
||||||
|
val endPadding = style.endPadding.orElse() ?: style.padding
|
||||||
|
val animatedBorderColor by animateColorAsState(when {
|
||||||
|
focused || hovered -> style.borderActive.solidColor
|
||||||
|
else -> style.borderInactive.solidColor
|
||||||
|
})
|
||||||
|
val animatedDirection by animateFloatAsState(if (expanded) 180f else 0f)
|
||||||
|
val animatedBorderWidth by animateDpAsState(when {
|
||||||
|
focused -> style.borderActive.width
|
||||||
|
else -> style.borderInactive.width
|
||||||
|
})
|
||||||
|
val border = when {
|
||||||
|
focused || hovered -> style.borderInactive
|
||||||
|
else -> style.borderInactive
|
||||||
|
}.copy(animatedBorderWidth, SolidColor(animatedBorderColor))
|
||||||
|
DropdownListBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = onExpandedChange,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
border = border,
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
menuHeightPx = { menuHeightPx = it }
|
||||||
|
) {
|
||||||
|
val menuWidth = maxWidth + startPadding + endPadding
|
||||||
|
val menuHeight = with(LocalDensity.current) { menuHeightPx.toDp() }
|
||||||
|
Row(horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) { text() }
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.graphicsLayer {
|
||||||
|
rotationZ = animatedDirection
|
||||||
|
}.size(style.endIconSize),
|
||||||
|
imageVector = Icons.Dropdown,
|
||||||
|
tint = colors.endIconTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { onExpandedChange(false) },
|
||||||
|
offset = DefaultDropdownListMenuOffset,
|
||||||
|
modifier = Modifier.width(menuWidth).heightIn(max = menuHeight),
|
||||||
|
colors = menuColors,
|
||||||
|
style = menuStyle,
|
||||||
|
scrollState = scrollState,
|
||||||
|
properties = properties,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DropdownMenu(
|
fun DropdownMenu(
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
@@ -233,6 +350,41 @@ private fun DropdownMenuContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal expect fun DropdownListBox(
|
||||||
|
expanded: Boolean,
|
||||||
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
colors: DropdownListColors,
|
||||||
|
style: DropdownListStyle,
|
||||||
|
border: BorderStroke,
|
||||||
|
enabled: Boolean,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
menuHeightPx: (Int) -> Unit,
|
||||||
|
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun Modifier.dropdownList(
|
||||||
|
colors: DropdownListColors,
|
||||||
|
style: DropdownListStyle,
|
||||||
|
border: BorderStroke,
|
||||||
|
enabled: Boolean,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
modifier: Modifier
|
||||||
|
) = status(enabled)
|
||||||
|
.focusable(enabled, interactionSource)
|
||||||
|
.hoverable(interactionSource, enabled)
|
||||||
|
.clip(style.shape)
|
||||||
|
.background(colors.backgroundColor, style.shape)
|
||||||
|
.borderOrNot(border, style.shape)
|
||||||
|
.then(modifier)
|
||||||
|
.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
|
||||||
|
)
|
||||||
|
|
||||||
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
|
||||||
@@ -312,6 +464,17 @@ private data class DropdownMenuPositionProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object DropdownList {
|
||||||
|
val colors: DropdownListColors
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultDropdownListColors()
|
||||||
|
val style: DropdownListStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = defaultDropdownListStyle()
|
||||||
|
}
|
||||||
|
|
||||||
object DropdownMenu {
|
object DropdownMenu {
|
||||||
val colors: DropdownMenuColors
|
val colors: DropdownMenuColors
|
||||||
@Composable
|
@Composable
|
||||||
@@ -329,6 +492,15 @@ private val LocalDropdownMenuActiveColor = compositionLocalOf { Color.Unspecifie
|
|||||||
|
|
||||||
private val LocalDropdownMenuContentStyle = compositionLocalOf<AreaBoxStyle?> { null }
|
private val LocalDropdownMenuContentStyle = compositionLocalOf<AreaBoxStyle?> { null }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultDropdownListColors() = DropdownListColors(
|
||||||
|
endIconTint = LocalColors.current.themeSecondary,
|
||||||
|
borderInactiveColor = LocalColors.current.themeSecondary,
|
||||||
|
borderActiveColor = LocalColors.current.themePrimary,
|
||||||
|
backgroundColor = Color.Transparent
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
private fun defaultDropdownMenuColors() = DropdownMenuColors(
|
private fun defaultDropdownMenuColors() = DropdownMenuColors(
|
||||||
@@ -337,6 +509,23 @@ private fun defaultDropdownMenuColors() = DropdownMenuColors(
|
|||||||
borderColor = AreaBox.color
|
borderColor = AreaBox.color
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultDropdownListStyle() = DropdownListStyle(
|
||||||
|
padding = LocalSizes.current.spacingSecondary,
|
||||||
|
topPadding = Dp.Unspecified,
|
||||||
|
startPadding = Dp.Unspecified,
|
||||||
|
bottomPadding = Dp.Unspecified,
|
||||||
|
endPadding = Dp.Unspecified,
|
||||||
|
shape = when (LocalInAreaBox.current) {
|
||||||
|
true -> LocalAreaBoxShape.current
|
||||||
|
else -> LocalShapes.current.secondary
|
||||||
|
},
|
||||||
|
endIconSize = LocalSizes.current.iconSizeTertiary,
|
||||||
|
borderInactive = defaultDropdownListInactiveBorder(),
|
||||||
|
borderActive = defaultDropdownListActiveBorder()
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
private fun defaultDropdownMenuStyle() = DropdownMenuStyle(
|
private fun defaultDropdownMenuStyle() = DropdownMenuStyle(
|
||||||
@@ -355,6 +544,16 @@ private fun defaultDropdownMenuStyle() = DropdownMenuStyle(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultDropdownListInactiveBorder() = BorderStroke(LocalSizes.current.borderSizeSecondary, LocalColors.current.themeSecondary)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun defaultDropdownListActiveBorder() = BorderStroke(LocalSizes.current.borderSizePrimary, LocalColors.current.themePrimary)
|
||||||
|
|
||||||
|
private val DefaultDropdownListMenuOffset = DpOffset((-10).dp, 10.dp)
|
||||||
|
|
||||||
private val DefaultMenuContentPadding = 16.dp
|
private val DefaultMenuContentPadding = 16.dp
|
||||||
|
|
||||||
private const val DefaultInTransitionDuration = 120
|
private const val DefaultInTransitionDuration = 120
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Flexi UI - A flexible and useful UI component library.
|
||||||
|
* Copyright (C) 2019-2023 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/FlexiUI
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2023/11/18.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||||
|
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.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
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,
|
||||||
|
colors: DropdownListColors,
|
||||||
|
style: DropdownListStyle,
|
||||||
|
border: BorderStroke,
|
||||||
|
enabled: Boolean,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
menuHeightPx: (Int) -> Unit,
|
||||||
|
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier.dropdownList(
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
border = border,
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier.rippleClickable(
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.DropdownList,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
) { 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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Flexi UI - A flexible and useful UI component library.
|
||||||
|
* Copyright (C) 2019-2023 HighCapable
|
||||||
|
* https://github.com/BetterAndroid/FlexiUI
|
||||||
|
*
|
||||||
|
* Apache License Version 2.0
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* This file is created by fankes on 2023/11/18.
|
||||||
|
*/
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.highcapable.flexiui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraintsScope
|
||||||
|
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.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
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,
|
||||||
|
colors: DropdownListColors,
|
||||||
|
style: DropdownListStyle,
|
||||||
|
border: BorderStroke,
|
||||||
|
enabled: Boolean,
|
||||||
|
interactionSource: MutableInteractionSource,
|
||||||
|
menuHeightPx: (Int) -> Unit,
|
||||||
|
content: @Composable @UiComposable BoxWithConstraintsScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier.dropdownList(
|
||||||
|
colors = colors,
|
||||||
|
style = style,
|
||||||
|
border = border,
|
||||||
|
enabled = enabled,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = modifier.rippleClickable(
|
||||||
|
enabled = enabled,
|
||||||
|
role = Role.DropdownList,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
) { 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
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user