feat(v1.8.0): IMPL_023b Drag & Drop Flicker Fix - Straddle-Target-Center Detection
- Swap detection: Changed from midpoint check to straddle-target-center detection * Old: Checks if midpoint of dragged item lies within target * New: Checks if dragged item spans the midpoint of target * Prevents oscillation when items have different sizes - Adjacency filter: Only adjacent items (index ± 1) as swap candidates * Prevents item jumps during fast drag * Reduces recalculation of visibleItemsInfo - Race-condition fix for scroll + move * draggingItemIndex update moved after onMove() in coroutine block * Prevents inconsistent state between index update and layout change Affected files: - DragDropListState.kt: onDrag() method (~10 lines changed)
This commit is contained in:
@@ -9,6 +9,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -19,9 +20,11 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FOSS Drag & Drop State für LazyList
|
* FOSS Drag & Drop State für LazyList
|
||||||
*
|
*
|
||||||
* Native Compose-Implementierung ohne externe Dependencies
|
* Native Compose-Implementierung ohne externe Dependencies
|
||||||
* v1.5.0: NoteEditor Redesign
|
* v1.5.0: NoteEditor Redesign
|
||||||
|
* v1.8.0: IMPL_023 - Drag & Drop Fix (pointerInput key + Handle-only drag)
|
||||||
|
* v1.8.0: IMPL_023b - Flicker-Fix (Straddle-Target-Center-Erkennung statt Mittelpunkt)
|
||||||
*/
|
*/
|
||||||
class DragDropListState(
|
class DragDropListState(
|
||||||
private val state: LazyListState,
|
private val state: LazyListState,
|
||||||
@@ -64,11 +67,17 @@ class DragDropListState(
|
|||||||
val startOffset = draggingItem.offset + draggingItemOffset
|
val startOffset = draggingItem.offset + draggingItemOffset
|
||||||
val endOffset = startOffset + draggingItem.size
|
val endOffset = startOffset + draggingItem.size
|
||||||
|
|
||||||
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
// 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter
|
||||||
|
// Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"),
|
||||||
val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
|
// wird geprüft ob das gezogene Item den MITTELPUNKT des Targets überspannt.
|
||||||
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
// Dies verhindert Oszillation bei Items unterschiedlicher Größe.
|
||||||
draggingItem.index != item.index
|
// Zusätzlich: Nur adjazente Items (Index ± 1) als Swap-Kandidaten.
|
||||||
|
val targetItem = state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
|
||||||
|
(item.index == draggingItem.index - 1 || item.index == draggingItem.index + 1) &&
|
||||||
|
run {
|
||||||
|
val targetCenter = item.offset + item.size / 2
|
||||||
|
startOffset < targetCenter && endOffset > targetCenter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetItem != null) {
|
if (targetItem != null) {
|
||||||
@@ -84,12 +93,13 @@ class DragDropListState(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
|
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
|
||||||
onMove(draggingItem.index, targetItem.index)
|
onMove(draggingItem.index, targetItem.index)
|
||||||
|
// 🆕 v1.8.0: IMPL_023b — Index-Update NACH dem Move (verhindert Race-Condition)
|
||||||
|
draggingItemIndex = targetItem.index
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onMove(draggingItem.index, targetItem.index)
|
onMove(draggingItem.index, targetItem.index)
|
||||||
|
draggingItemIndex = targetItem.index
|
||||||
}
|
}
|
||||||
|
|
||||||
draggingItemIndex = targetItem.index
|
|
||||||
} else {
|
} else {
|
||||||
val overscroll = when {
|
val overscroll = when {
|
||||||
draggingItemDraggedDelta > 0 ->
|
draggingItemDraggedDelta > 0 ->
|
||||||
@@ -130,14 +140,16 @@ fun rememberDragDropListState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
fun Modifier.dragContainer(
|
fun Modifier.dragContainer(
|
||||||
dragDropState: DragDropListState,
|
dragDropState: DragDropListState,
|
||||||
itemIndex: Int
|
itemIndex: Int
|
||||||
): Modifier {
|
): Modifier {
|
||||||
return this.pointerInput(dragDropState) {
|
val currentIndex = rememberUpdatedState(itemIndex) // 🆕 v1.8.0: rememberUpdatedState statt Key
|
||||||
|
return this.pointerInput(dragDropState) { // Nur dragDropState als Key - verhindert Gesture-Restart
|
||||||
detectDragGesturesAfterLongPress(
|
detectDragGesturesAfterLongPress(
|
||||||
onDragStart = { offset ->
|
onDragStart = { offset ->
|
||||||
dragDropState.onDragStart(offset, itemIndex)
|
dragDropState.onDragStart(offset, currentIndex.value) // Aktuellen Wert lesen
|
||||||
},
|
},
|
||||||
onDragEnd = {
|
onDragEnd = {
|
||||||
dragDropState.onDragInterrupted()
|
dragDropState.onDragInterrupted()
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
import dev.dettmer.simplenotes.models.NoteType
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.ui.editor.components.ChecklistItemRow
|
import dev.dettmer.simplenotes.ui.editor.components.ChecklistItemRow
|
||||||
@@ -351,14 +352,17 @@ private fun ChecklistEditor(
|
|||||||
onDelete = { onDelete(item.id) },
|
onDelete = { onDelete(item.id) },
|
||||||
onAddNewItem = { onAddNewItemAfter(item.id) },
|
onAddNewItem = { onAddNewItemAfter(item.id) },
|
||||||
requestFocus = shouldFocus,
|
requestFocus = shouldFocus,
|
||||||
|
isDragging = isDragging, // 🆕 v1.8.0: IMPL_023 - Drag state übergeben
|
||||||
|
isAnyItemDragging = dragDropState.draggingItemIndex != null, // 🆕 v1.8.0: IMPL_023 - Gradient während Drag ausblenden
|
||||||
|
dragModifier = Modifier.dragContainer(dragDropState, index), // 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.dragContainer(dragDropState, index)
|
|
||||||
.offset {
|
.offset {
|
||||||
IntOffset(
|
IntOffset(
|
||||||
0,
|
0,
|
||||||
if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
|
if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.zIndex(if (isDragging) 10f else 0f) // 🆕 v1.8.0: IMPL_023 - Gedraggtes Item liegt über anderen
|
||||||
.shadow(elevation, shape = RoundedCornerShape(8.dp))
|
.shadow(elevation, shape = RoundedCornerShape(8.dp))
|
||||||
.background(
|
.background(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.DragHandle
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
@@ -32,6 +35,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
@@ -39,6 +43,7 @@ import androidx.compose.ui.text.input.ImeAction
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
import dev.dettmer.simplenotes.ui.editor.ChecklistItemState
|
import dev.dettmer.simplenotes.ui.editor.ChecklistItemState
|
||||||
@@ -48,6 +53,7 @@ import dev.dettmer.simplenotes.ui.editor.ChecklistItemState
|
|||||||
*
|
*
|
||||||
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
* v1.5.0: Jetpack Compose NoteEditor Redesign
|
||||||
* v1.8.0: Long text UX improvements (gradient fade, auto-expand on focus)
|
* v1.8.0: Long text UX improvements (gradient fade, auto-expand on focus)
|
||||||
|
* v1.8.0: IMPL_023 - Enlarged drag handle (48dp touch target) + drag modifier
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ChecklistItemRow(
|
fun ChecklistItemRow(
|
||||||
@@ -57,12 +63,16 @@ fun ChecklistItemRow(
|
|||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onAddNewItem: () -> Unit,
|
onAddNewItem: () -> Unit,
|
||||||
requestFocus: Boolean = false,
|
requestFocus: Boolean = false,
|
||||||
|
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
|
||||||
|
isAnyItemDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Hide gradient during any drag
|
||||||
|
dragModifier: Modifier = Modifier, // 🆕 v1.8.0: IMPL_023 - Drag modifier for handle
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val density = LocalDensity.current
|
||||||
var textFieldValue by remember(item.id) {
|
var textFieldValue by remember(item.id) {
|
||||||
mutableStateOf(TextFieldValue(text = item.text, selection = TextRange(item.text.length)))
|
mutableStateOf(TextFieldValue(text = item.text, selection = TextRange(0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 v1.8.0: Focus-State tracken für Expand/Collapse
|
// 🆕 v1.8.0: Focus-State tracken für Expand/Collapse
|
||||||
@@ -71,9 +81,20 @@ fun ChecklistItemRow(
|
|||||||
// 🆕 v1.8.0: Overflow erkennen (Text länger als maxLines)
|
// 🆕 v1.8.0: Overflow erkennen (Text länger als maxLines)
|
||||||
var hasOverflow by remember { mutableStateOf(false) }
|
var hasOverflow by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 🆕 v1.8.0: Dynamische maxLines basierend auf Focus
|
// 🆕 v1.8.0: Höhe für collapsed-Ansicht (aus TextLayout berechnet)
|
||||||
val currentMaxLines = if (isFocused) Int.MAX_VALUE else COLLAPSED_MAX_LINES
|
var collapsedHeightDp by remember { mutableStateOf<Dp?>(null) }
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: ScrollState für dynamischen Gradient
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: Scroll-basierter Ansatz aktiv wenn Höhe berechnet wurde
|
||||||
|
val useScrollClipping = hasOverflow && collapsedHeightDp != null
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: Dynamische Gradient-Sichtbarkeit basierend auf Scroll-Position
|
||||||
|
val showGradient = useScrollClipping && !isFocused && !isAnyItemDragging
|
||||||
|
val showTopGradient = showGradient && scrollState.value > 0
|
||||||
|
val showBottomGradient = showGradient && scrollState.value < scrollState.maxValue
|
||||||
|
|
||||||
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
|
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
|
||||||
LaunchedEffect(requestFocus) {
|
LaunchedEffect(requestFocus) {
|
||||||
if (requestFocus) {
|
if (requestFocus) {
|
||||||
@@ -81,121 +102,163 @@ fun ChecklistItemRow(
|
|||||||
keyboardController?.show()
|
keyboardController?.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: Cursor ans Ende setzen wenn fokussiert (für Bearbeitung)
|
||||||
|
LaunchedEffect(isFocused) {
|
||||||
|
if (isFocused && textFieldValue.selection.start == 0) {
|
||||||
|
textFieldValue = textFieldValue.copy(
|
||||||
|
selection = TextRange(textFieldValue.text.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update text field when external state changes
|
// Update text field when external state changes
|
||||||
LaunchedEffect(item.text) {
|
LaunchedEffect(item.text) {
|
||||||
if (textFieldValue.text != item.text) {
|
if (textFieldValue.text != item.text) {
|
||||||
textFieldValue = TextFieldValue(
|
textFieldValue = TextFieldValue(
|
||||||
text = item.text,
|
text = item.text,
|
||||||
selection = TextRange(item.text.length)
|
selection = if (isFocused) TextRange(item.text.length) else TextRange(0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val alpha = if (item.isChecked) 0.6f else 1.0f
|
val alpha = if (item.isChecked) 0.6f else 1.0f
|
||||||
val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None
|
val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
|
||||||
@Suppress("MagicNumber") // UI padding values are self-explanatory
|
@Suppress("MagicNumber") // UI padding values are self-explanatory
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(end = 8.dp, top = 4.dp, bottom = 4.dp), // 🆕 v1.8.0: IMPL_023 - links kein Padding (Handle hat eigene Fläche)
|
||||||
verticalAlignment = Alignment.Top // 🆕 v1.8.0: Top statt CenterVertically für lange Texte
|
verticalAlignment = if (hasOverflow) Alignment.Top else Alignment.CenterVertically // 🆕 v1.8.0: Dynamisch
|
||||||
) {
|
) {
|
||||||
// Drag Handle
|
// 🆕 v1.8.0: IMPL_023 - Vergrößerter Drag Handle (48dp Touch-Target)
|
||||||
Icon(
|
Box(
|
||||||
imageVector = Icons.Default.DragHandle,
|
modifier = dragModifier
|
||||||
contentDescription = stringResource(R.string.drag_to_reorder),
|
.size(48.dp) // Material Design minimum touch target
|
||||||
modifier = Modifier
|
.alpha(if (isDragging) 1.0f else 0.6f), // Visual feedback beim Drag
|
||||||
.size(24.dp)
|
contentAlignment = Alignment.Center
|
||||||
.padding(top = 12.dp) // 🆕 v1.8.0: Visuell am oberen Rand ausrichten
|
) {
|
||||||
.alpha(0.5f),
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
imageVector = Icons.Default.DragHandle,
|
||||||
)
|
contentDescription = stringResource(R.string.drag_to_reorder),
|
||||||
|
modifier = Modifier.size(28.dp), // Icon größer als vorher (24dp → 28dp)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
tint = if (isDragging) {
|
||||||
|
MaterialTheme.colorScheme.primary // Primary color während Drag
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Checkbox
|
// Checkbox
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = item.isChecked,
|
checked = item.isChecked,
|
||||||
onCheckedChange = onCheckedChange,
|
onCheckedChange = onCheckedChange,
|
||||||
modifier = Modifier.alpha(alpha)
|
modifier = Modifier.alpha(alpha)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
// 🆕 v1.8.0: Text Input mit Overflow-Gradient
|
// 🆕 v1.8.0: Text Input mit dynamischem Overflow-Gradient
|
||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
BasicTextField(
|
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
|
||||||
value = textFieldValue,
|
Box(
|
||||||
onValueChange = { newValue ->
|
modifier = if (!isFocused && useScrollClipping) {
|
||||||
// Check for newline (Enter key)
|
Modifier
|
||||||
if (newValue.text.contains("\n")) {
|
.heightIn(max = collapsedHeightDp!!)
|
||||||
val cleanText = newValue.text.replace("\n", "")
|
.verticalScroll(scrollState)
|
||||||
textFieldValue = TextFieldValue(
|
} else {
|
||||||
text = cleanText,
|
Modifier
|
||||||
selection = TextRange(cleanText.length)
|
|
||||||
)
|
|
||||||
onTextChange(cleanText)
|
|
||||||
onAddNewItem()
|
|
||||||
} else {
|
|
||||||
textFieldValue = newValue
|
|
||||||
onTextChange(newValue.text)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.onFocusChanged { focusState ->
|
|
||||||
isFocused = focusState.isFocused
|
|
||||||
}
|
|
||||||
.alpha(alpha),
|
|
||||||
textStyle = LocalTextStyle.current.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
textDecoration = textDecoration
|
|
||||||
),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
imeAction = ImeAction.Next
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(
|
|
||||||
onNext = { onAddNewItem() }
|
|
||||||
),
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = currentMaxLines, // 🆕 v1.8.0: Dynamisch
|
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
|
||||||
onTextLayout = { textLayoutResult ->
|
|
||||||
// 🆕 v1.8.0: Overflow erkennen
|
|
||||||
hasOverflow = textLayoutResult.hasVisualOverflow ||
|
|
||||||
textLayoutResult.lineCount > COLLAPSED_MAX_LINES
|
|
||||||
},
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Box {
|
|
||||||
if (textFieldValue.text.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.item_placeholder),
|
|
||||||
style = LocalTextStyle.current.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = textFieldValue,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
// Check for newline (Enter key)
|
||||||
|
if (newValue.text.contains("\n")) {
|
||||||
|
val cleanText = newValue.text.replace("\n", "")
|
||||||
|
textFieldValue = TextFieldValue(
|
||||||
|
text = cleanText,
|
||||||
|
selection = TextRange(cleanText.length)
|
||||||
|
)
|
||||||
|
onTextChange(cleanText)
|
||||||
|
onAddNewItem()
|
||||||
|
} else {
|
||||||
|
textFieldValue = newValue
|
||||||
|
onTextChange(newValue.text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onFocusChanged { focusState ->
|
||||||
|
isFocused = focusState.isFocused
|
||||||
|
}
|
||||||
|
.alpha(alpha),
|
||||||
|
textStyle = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textDecoration = textDecoration
|
||||||
|
),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = { onAddNewItem() }
|
||||||
|
),
|
||||||
|
singleLine = false,
|
||||||
|
// maxLines nur als Fallback bis collapsedHeight berechnet ist
|
||||||
|
maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
|
onTextLayout = { textLayoutResult ->
|
||||||
|
// 🆕 v1.8.0: Overflow erkennen - ABER NUR wenn kein Drag aktiv ist
|
||||||
|
if (!isAnyItemDragging) {
|
||||||
|
val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES
|
||||||
|
hasOverflow = overflow
|
||||||
|
// Höhe der ersten 5 Zeilen berechnen (einmalig)
|
||||||
|
if (overflow && collapsedHeightDp == null) {
|
||||||
|
collapsedHeightDp = with(density) {
|
||||||
|
textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box {
|
||||||
|
if (textFieldValue.text.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.item_placeholder),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 v1.8.0: Gradient-Fade Overlay wenn Text überläuft
|
// 🆕 v1.8.0: Dynamischer Gradient basierend auf Scroll-Position
|
||||||
// Zeige nur Gradient oben, da man am unteren Rand startet und nach oben scrollt
|
// Oben: sichtbar wenn nach unten gescrollt (Text oberhalb versteckt)
|
||||||
if (hasOverflow && !isFocused) {
|
if (showTopGradient) {
|
||||||
// Gradient oben (zeigt: es gibt Text oberhalb der sichtbaren Zeilen)
|
|
||||||
OverflowGradient(
|
OverflowGradient(
|
||||||
modifier = Modifier.align(Alignment.TopCenter),
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
isTopGradient = true
|
isTopGradient = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unten: sichtbar wenn noch Text unterhalb vorhanden
|
||||||
|
if (showBottomGradient) {
|
||||||
|
OverflowGradient(
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
isTopGradient = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
// Delete Button
|
// Delete Button
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onDelete,
|
onClick = onDelete,
|
||||||
@@ -232,7 +295,9 @@ private fun ChecklistItemRowShortTextPreview() {
|
|||||||
onTextChange = {},
|
onTextChange = {},
|
||||||
onCheckedChange = {},
|
onCheckedChange = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
onAddNewItem = {}
|
onAddNewItem = {},
|
||||||
|
isDragging = false,
|
||||||
|
dragModifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +318,9 @@ private fun ChecklistItemRowLongTextPreview() {
|
|||||||
onTextChange = {},
|
onTextChange = {},
|
||||||
onCheckedChange = {},
|
onCheckedChange = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
onAddNewItem = {}
|
onAddNewItem = {},
|
||||||
|
isDragging = false,
|
||||||
|
dragModifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +336,27 @@ private fun ChecklistItemRowCheckedPreview() {
|
|||||||
onTextChange = {},
|
onTextChange = {},
|
||||||
onCheckedChange = {},
|
onCheckedChange = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
onAddNewItem = {}
|
onAddNewItem = {},
|
||||||
|
isDragging = false,
|
||||||
|
dragModifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 v1.8.0: IMPL_023 - Preview for dragging state
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ChecklistItemRowDraggingPreview() {
|
||||||
|
ChecklistItemRow(
|
||||||
|
item = ChecklistItemState(
|
||||||
|
id = "preview-4",
|
||||||
|
text = "Wird gerade verschoben - Handle ist highlighted",
|
||||||
|
isChecked = false
|
||||||
|
),
|
||||||
|
onTextChange = {},
|
||||||
|
onCheckedChange = {},
|
||||||
|
onDelete = {},
|
||||||
|
onAddNewItem = {},
|
||||||
|
isDragging = true,
|
||||||
|
dragModifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user