11package top.yukonga.miuix.kmp.extra
22
33import androidx.compose.foundation.Image
4+ import androidx.compose.foundation.LocalIndication
45import androidx.compose.foundation.background
56import androidx.compose.foundation.clickable
67import androidx.compose.foundation.gestures.detectTapGestures
8+ import androidx.compose.foundation.indication
79import androidx.compose.foundation.interaction.MutableInteractionSource
10+ import androidx.compose.foundation.interaction.PressInteraction
811import androidx.compose.foundation.layout.Arrangement
912import androidx.compose.foundation.layout.PaddingValues
1013import androidx.compose.foundation.layout.Row
@@ -13,10 +16,7 @@ import androidx.compose.foundation.layout.WindowInsetsSides
1316import androidx.compose.foundation.layout.asPaddingValues
1417import androidx.compose.foundation.layout.captionBar
1518import androidx.compose.foundation.layout.displayCutout
16- import androidx.compose.foundation.layout.fillMaxSize
17- import androidx.compose.foundation.layout.heightIn
1819import androidx.compose.foundation.layout.navigationBars
19- import androidx.compose.foundation.layout.offset
2020import androidx.compose.foundation.layout.only
2121import androidx.compose.foundation.layout.padding
2222import androidx.compose.foundation.layout.size
@@ -49,8 +49,9 @@ import androidx.compose.ui.graphics.ColorFilter
4949import androidx.compose.ui.graphics.TransformOrigin
5050import androidx.compose.ui.graphics.graphicsLayer
5151import androidx.compose.ui.hapticfeedback.HapticFeedbackType
52- import androidx.compose.ui.input.pointer.PointerEventType
5352import androidx.compose.ui.input.pointer.pointerInput
53+ import androidx.compose.ui.layout.Layout
54+ import androidx.compose.ui.layout.layout
5455import androidx.compose.ui.layout.onGloballyPositioned
5556import androidx.compose.ui.layout.positionInWindow
5657import androidx.compose.ui.platform.LocalDensity
@@ -80,7 +81,6 @@ import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup
8081import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.showPopup
8182import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape
8283import top.yukonga.miuix.kmp.utils.getWindowSize
83- import kotlin.math.max
8484import kotlin.math.roundToInt
8585
8686/* *
@@ -121,21 +121,23 @@ fun SuperDropdown(
121121 onSelectedIndexChange : ((Int ) -> Unit )? ,
122122) {
123123 val interactionSource = remember { MutableInteractionSource () }
124+ val isDropdownPreExpand = remember { mutableStateOf(false ) }
124125 val isDropdownExpanded = remember { mutableStateOf(false ) }
125126 val coroutineScope = rememberCoroutineScope()
126127 val held = remember { mutableStateOf<HoldDownInteraction .Hold ?>(null ) }
127128 val hapticFeedback = LocalHapticFeedback .current
128129 val actionColor = if (enabled) MiuixTheme .colorScheme.onSurfaceVariantActions else MiuixTheme .colorScheme.disabledOnSecondaryVariant
129130 var alignLeft by rememberSaveable { mutableStateOf(true ) }
130- var dropdownOffsetXPx by remember { mutableIntStateOf(0 ) }
131- var dropdownOffsetYPx by remember { mutableIntStateOf(0 ) }
132- var componentHeightPx by remember { mutableIntStateOf(0 ) }
133- var componentWidthPx by remember { mutableIntStateOf(0 ) }
131+ var componentInnerOffsetXPx by remember { mutableIntStateOf(0 ) }
132+ var componentInnerOffsetYPx by remember { mutableIntStateOf(0 ) }
133+ var componentInnerHeightPx by remember { mutableIntStateOf(0 ) }
134+ var componentInnerWidthPx by remember { mutableIntStateOf(0 ) }
134135
136+ val density = LocalDensity .current
135137 val getWindowSize = rememberUpdatedState(getWindowSize())
136138 val windowHeightPx by rememberUpdatedState(getWindowSize.value.height)
137139 val windowWidthPx by rememberUpdatedState(getWindowSize.value.width)
138- var transformOrigin by mutableStateOf(TransformOrigin .Center )
140+ var transformOrigin by remember { mutableStateOf(TransformOrigin .Center ) }
139141
140142 DisposableEffect (Unit ) {
141143 onDispose {
@@ -154,40 +156,74 @@ fun SuperDropdown(
154156
155157 BasicComponent (
156158 modifier = modifier
159+ .indication(
160+ interactionSource = interactionSource,
161+ indication = LocalIndication .current
162+ )
157163 .pointerInput(Unit ) {
158- awaitPointerEventScope {
159- while (enabled) {
160- val event = awaitPointerEvent()
161- if (event.type != PointerEventType .Move ) {
162- val eventChange = event.changes.first()
163- if (eventChange.pressed) {
164- alignLeft = eventChange.position.x < (size.width / 2 )
164+ detectTapGestures(
165+ onPress = { position ->
166+ if (enabled) {
167+ alignLeft = position.x < (size.width / 2 )
168+ isDropdownPreExpand.value = true
169+ val pressInteraction = PressInteraction .Press (position)
170+ coroutineScope.launch {
171+ interactionSource.emit(pressInteraction)
172+ }
173+ val released = tryAwaitRelease()
174+ isDropdownPreExpand.value = false
175+ if (released) {
176+ isDropdownExpanded.value = enabled
177+ hapticFeedback.performHapticFeedback(HapticFeedbackType .LongPress )
178+ coroutineScope.launch {
179+ interactionSource.emit(HoldDownInteraction .Hold ().also {
180+ held.value = it
181+ })
182+ interactionSource.emit(PressInteraction .Release (pressInteraction))
183+ }
184+ } else {
185+ coroutineScope.launch {
186+ interactionSource.emit(PressInteraction .Cancel (pressInteraction))
187+ }
165188 }
166189 }
167190 }
168- }
169- }
170- .onGloballyPositioned { coordinates ->
171- if (isDropdownExpanded.value) {
172- val positionInWindow = coordinates.positionInWindow()
173- dropdownOffsetXPx = positionInWindow.x.toInt()
174- dropdownOffsetYPx = positionInWindow.y.toInt()
175- componentHeightPx = coordinates.size.height
176- componentWidthPx = coordinates.size.width
177- val xInWindow = dropdownOffsetXPx + if (mode == DropDownMode .AlwaysOnRight || ! alignLeft) componentWidthPx else 0
178- val yInWindow = dropdownOffsetYPx + componentHeightPx / 2
179- transformOrigin = TransformOrigin (
180- xInWindow / windowWidthPx.toFloat(),
181- yInWindow / windowHeightPx.toFloat()
182- )
183- }
191+ )
184192 },
185- interactionSource = interactionSource,
186193 insideMargin = insideMargin,
187194 title = title,
188195 titleColor = titleColor,
189196 summary = summary,
190197 summaryColor = summaryColor,
198+ leftAction = {
199+ if (isDropdownPreExpand.value) {
200+ Layout (
201+ content = {},
202+ modifier = Modifier .onGloballyPositioned { childCoordinates ->
203+ val parentCoordinates =
204+ childCoordinates.parentLayoutCoordinates ? : return @onGloballyPositioned
205+ val positionInWindow = parentCoordinates.positionInWindow()
206+ componentInnerOffsetXPx = positionInWindow.x.toInt()
207+ componentInnerOffsetYPx = positionInWindow.y.toInt()
208+ componentInnerHeightPx = parentCoordinates.size.height
209+ componentInnerWidthPx = parentCoordinates.size.width
210+ with (density) {
211+ val xInWindow = componentInnerOffsetXPx + if (mode == DropDownMode .AlwaysOnRight || ! alignLeft)
212+ componentInnerWidthPx - 64 .dp.roundToPx()
213+ else
214+ 64 .dp.roundToPx()
215+ val yInWindow = componentInnerOffsetYPx + componentInnerHeightPx / 2 - 56 .dp.roundToPx()
216+ transformOrigin = TransformOrigin (
217+ xInWindow / windowWidthPx.toFloat(),
218+ yInWindow / windowHeightPx.toFloat()
219+ )
220+ }
221+ }
222+ ) { _, _ ->
223+ layout(0 , 0 ) {}
224+ }
225+ }
226+ },
191227 rightActions = {
192228 if (showValue) {
193229 Text (
@@ -210,17 +246,6 @@ fun SuperDropdown(
210246 contentDescription = null
211247 )
212248 },
213- onClick = {
214- if (enabled) {
215- isDropdownExpanded.value = enabled
216- hapticFeedback.performHapticFeedback(HapticFeedbackType .LongPress )
217- coroutineScope.launch {
218- interactionSource.emit(HoldDownInteraction .Hold ().also {
219- held.value = it
220- })
221- }
222- }
223- },
224249 enabled = enabled
225250 )
226251 if (isDropdownExpanded.value) {
@@ -231,9 +256,6 @@ fun SuperDropdown(
231256 }
232257 }
233258
234- val density = LocalDensity .current
235- var offsetXPx by remember { mutableStateOf(0 ) }
236- var offsetYPx by remember { mutableStateOf(0 ) }
237259 val textMeasurer = rememberTextMeasurer()
238260 val textStyle = remember { TextStyle (fontWeight = FontWeight .Medium , fontSize = 16 .sp) }
239261 val textWidthDp = remember(items) { items.maxOfOrNull { with (density) { textMeasurer.measure(text = it, style = textStyle).size.width.toDp() } } }
@@ -246,18 +268,9 @@ fun SuperDropdown(
246268 val captionBarPx by rememberUpdatedState(
247269 with (density) { WindowInsets .captionBar.asPaddingValues().calculateBottomPadding().toPx() }.roundToInt()
248270 )
249- val dropdownMaxHeight by rememberUpdatedState(with (density) {
250- (windowHeightPx - statusBarPx - navigationBarPx - captionBarPx).toDp()
251- })
252271 val dropdownElevation by rememberUpdatedState(with (density) {
253272 11 .dp.toPx()
254273 })
255- val insideLeftPx by rememberUpdatedState(with (density) {
256- insideMargin.calculateLeftPadding(LayoutDirection .Ltr ).toPx()
257- }.roundToInt())
258- val insideRightPx by rememberUpdatedState(with (density) {
259- insideMargin.calculateRightPadding(LayoutDirection .Ltr ).toPx()
260- }.roundToInt())
261274 val insideTopPx by rememberUpdatedState(with (density) {
262275 insideMargin.calculateTopPadding().toPx()
263276 }.roundToInt())
@@ -283,37 +296,48 @@ fun SuperDropdown(
283296 } else {
284297 popupModifier
285298 }
286- .fillMaxSize()
287299 .pointerInput(Unit ) {
288300 detectTapGestures(
289301 onTap = {
290302 dismissPopup(isDropdownExpanded)
291303 }
292304 )
293305 }
294- .offset(x = offsetXPx.dp / density.density, y = offsetYPx.dp / density.density)
295- ) {
296- LazyColumn (
297- modifier = Modifier
298- .onGloballyPositioned { coordinates ->
299- offsetXPx = if (mode == DropDownMode .AlwaysOnRight || ! alignLeft) {
300- dropdownOffsetXPx + componentWidthPx - insideRightPx - coordinates.size.width - paddingPx - if (defaultWindowInsetsPadding) displayCutoutLeftSize else 0
301- } else {
302- dropdownOffsetXPx + paddingPx + insideLeftPx - if (defaultWindowInsetsPadding) displayCutoutLeftSize else 0
303- }
304- offsetYPx = calculateOffsetYPx(
306+ .layout { measurable, constraints ->
307+ val placeable = measurable.measure(
308+ constraints.copy(
309+ minWidth = 200 .dp.roundToPx(),
310+ minHeight = 50 .dp.roundToPx(),
311+ maxHeight = windowHeightPx - statusBarPx - navigationBarPx - captionBarPx
312+ )
313+ )
314+ layout(constraints.maxWidth, constraints.maxHeight) {
315+ val xCoordinate = calculateOffsetXPx(
316+ componentInnerOffsetXPx,
317+ componentInnerWidthPx,
318+ placeable.width,
319+ paddingPx,
320+ displayCutoutLeftSize,
321+ defaultWindowInsetsPadding,
322+ (mode == DropDownMode .AlwaysOnRight || ! alignLeft)
323+ )
324+ val yCoordinate = calculateOffsetYPx(
305325 windowHeightPx,
306- dropdownOffsetYPx ,
307- coordinates.size .height,
308- componentHeightPx ,
326+ componentInnerOffsetYPx ,
327+ placeable .height,
328+ componentInnerHeightPx ,
309329 insideTopPx,
310330 insideBottomPx,
311331 statusBarPx,
312332 navigationBarPx,
313333 captionBarPx
314334 )
335+ placeable.place(xCoordinate, yCoordinate)
315336 }
316- .heightIn(50 .dp, dropdownMaxHeight)
337+ }
338+ ) {
339+ LazyColumn (
340+ modifier = Modifier
317341 .align(AbsoluteAlignment .TopLeft )
318342 .graphicsLayer(
319343 shadowElevation = dropdownElevation,
@@ -409,6 +433,34 @@ fun DropdownImpl(
409433 }
410434}
411435
436+ /* *
437+ * Calculate the offset of the dropdown.
438+ *
439+ * @param componentInnerOffsetXPx The offset of the component inside.
440+ * @param componentInnerWidthPx The width of the component inside.
441+ * @param dropdownWidthPx The width of the dropdown.
442+ * @param extraPaddingPx The extra padding of the dropdown.
443+ * @param displayCutoutLeftSizePx The size of the display cutout on the left.
444+ * @param defaultWindowInsetsPadding Whether to apply default window insets padding to the dropdown.
445+ * @param alignRight Whether to align the dropdown to the right.
446+ * @return The offset of the dropdown.
447+ */
448+ fun calculateOffsetXPx (
449+ componentInnerOffsetXPx : Int ,
450+ componentInnerWidthPx : Int ,
451+ dropdownWidthPx : Int ,
452+ extraPaddingPx : Int ,
453+ displayCutoutLeftSizePx : Int ,
454+ defaultWindowInsetsPadding : Boolean ,
455+ alignRight : Boolean
456+ ): Int {
457+ return if (alignRight) {
458+ componentInnerOffsetXPx + componentInnerWidthPx - dropdownWidthPx - extraPaddingPx - if (defaultWindowInsetsPadding) displayCutoutLeftSizePx else 0
459+ } else {
460+ componentInnerOffsetXPx + extraPaddingPx - if (defaultWindowInsetsPadding) displayCutoutLeftSizePx else 0
461+ }
462+ }
463+
412464/* *
413465 * Calculate the offset of the dropdown.
414466 *
@@ -434,24 +486,16 @@ fun calculateOffsetYPx(
434486 navigationBarPx : Int ,
435487 captionBarPx : Int
436488): Int {
437- return if (windowHeightPx - captionBarPx - navigationBarPx - dropdownOffsetPx - componentHeightPx > dropdownHeightPx) {
489+ return ( if (windowHeightPx - captionBarPx - navigationBarPx - dropdownOffsetPx - componentHeightPx > dropdownHeightPx) {
438490 // Show below
439- dropdownOffsetPx + componentHeightPx - insideBottomHeightPx / 2
491+ dropdownOffsetPx + componentHeightPx + insideBottomHeightPx / 2
440492 } else if (dropdownOffsetPx - statusBarPx > dropdownHeightPx) {
441493 // Show above
442- dropdownOffsetPx - dropdownHeightPx + insideTopHeightPx / 2
443- } else if (windowHeightPx - statusBarPx - captionBarPx - navigationBarPx <= dropdownHeightPx) {
444- // Special handling when the height of the popup is maxsize (== windowHeightPx)
445- statusBarPx
494+ dropdownOffsetPx - dropdownHeightPx - insideTopHeightPx / 2
446495 } else {
447- val maxInsideHeight = max(insideTopHeightPx, insideBottomHeightPx)
448- if (windowHeightPx - dropdownOffsetPx < dropdownHeightPx / 2 + captionBarPx + navigationBarPx + maxInsideHeight + componentHeightPx / 2 ) {
449- windowHeightPx - dropdownHeightPx - maxInsideHeight - captionBarPx - navigationBarPx
450- } else {
451- val offset = dropdownOffsetPx - dropdownHeightPx / 2 + componentHeightPx / 2
452- if (offset > maxInsideHeight + statusBarPx) offset else maxInsideHeight + statusBarPx
453- }
454- }
496+ // Middle
497+ dropdownOffsetPx + componentHeightPx / 2 - dropdownHeightPx / 2
498+ }).coerceIn(statusBarPx, windowHeightPx - captionBarPx - navigationBarPx)
455499}
456500
457501/* *
0 commit comments