From 24fe32a973596eb7c1df07ef91217e8dc1beeeea Mon Sep 17 00:00:00 2001 From: inventory69 Date: Wed, 11 Feb 2026 08:48:49 +0100 Subject: [PATCH] fix(editor): IMPL_13 - Fix gradient regression & drag-flicker Bug A1: Gradient never showed because maxLines capped lineCount, making lineCount > maxLines impossible. Fixed by always setting maxLines = Int.MAX_VALUE and using hasVisualOverflow + heightIn(max) for clipped overflow detection. Bug A2: derivedStateOf captured stale val from Detekt refactor (commit 1da1a63). Replaced with direct computation. Bug B1: animateItem() on dragged item conflicted with manual offset (from IMPL_017 commit 900dad7). Fixed with conditional Modifier: if (!isDragging) Modifier.animateItem() else Modifier. Bug B2: Item size could change during drag. Added size snapshot in DragDropListState.onDragStart for stable endOffset calculation. Files changed: - ChecklistItemRow.kt: hasVisualOverflow, direct lineCount check, maxLines=Int.MAX_VALUE always, heightIn(max=collapsedHeightDp) - NoteEditorScreen.kt: conditional animateItem on isDragging - DragDropListState.kt: draggingItemSize snapshot for stable drag --- .../ui/editor/DragDropListState.kt | 11 +++++-- .../simplenotes/ui/editor/NoteEditorScreen.kt | 9 ++++-- .../ui/editor/components/ChecklistItemRow.kt | 31 +++++++++---------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt index 96be346..0f6284e 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt @@ -36,6 +36,9 @@ class DragDropListState( private var draggingItemDraggedDelta by mutableFloatStateOf(0f) private var draggingItemInitialOffset by mutableFloatStateOf(0f) + // 🆕 v1.8.1: Item-Größe beim Drag-Start fixieren + // Verhindert dass Höhenänderungen die Swap-Erkennung destabilisieren + private var draggingItemSize by mutableStateOf(0) private var overscrollJob by mutableStateOf(null) val draggingItemOffset: Float @@ -49,7 +52,9 @@ class DragDropListState( fun onDragStart(offset: Offset, itemIndex: Int) { draggingItemIndex = itemIndex - draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f + val info = draggingItemLayoutInfo + draggingItemInitialOffset = info?.offset?.toFloat() ?: 0f + draggingItemSize = info?.size ?: 0 draggingItemDraggedDelta = 0f } @@ -57,6 +62,7 @@ class DragDropListState( draggingItemDraggedDelta = 0f draggingItemIndex = null draggingItemInitialOffset = 0f + draggingItemSize = 0 overscrollJob?.cancel() } @@ -65,7 +71,8 @@ class DragDropListState( val draggingItem = draggingItemLayoutInfo ?: return val startOffset = draggingItem.offset + draggingItemOffset - val endOffset = startOffset + draggingItem.size + // 🆕 v1.8.1: Fixierte Item-Größe für stabile Swap-Erkennung + val endOffset = startOffset + draggingItemSize // 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter // Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"), diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt index 83c49d6..f5e3e0b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt @@ -370,7 +370,9 @@ private fun ChecklistEditor( key = { _, item -> item.id } ) { index, item -> // 🆕 v1.8.0 (IMPL_017): Separator vor dem ersten Checked-Item - if (showSeparator && index == uncheckedCount) { + // 🆕 v1.8.1: Separator während Drag ausblenden — verhindert Flächenveränderung + // am Separator-Item die Swap-Erkennung destabilisiert + if (showSeparator && index == uncheckedCount && dragDropState.draggingItemIndex == null) { CheckedItemsSeparator(checkedCount = checkedCount) } @@ -409,7 +411,10 @@ private fun ChecklistEditor( // 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle dragModifier = Modifier.dragContainer(dragDropState, index), modifier = Modifier - .animateItem() // 🆕 v1.8.0 (IMPL_017): LazyColumn Item-Animation + // 🆕 v1.8.1: animateItem NUR für nicht-gedraggte Items + // Bei gedraggten Items kämpft animateItem (Layout-Animation) + // gegen den manuellen offset (Finger-Position) → Flackern + .then(if (!isDragging) Modifier.animateItem() else Modifier) .offset { IntOffset( 0, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt index 8076448..1c86102 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -92,17 +91,11 @@ fun ChecklistItemRow( // 🆕 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 by remember { - derivedStateOf { showGradient && scrollState.value > 0 } - } - val showBottomGradient by remember { - derivedStateOf { showGradient && scrollState.value < scrollState.maxValue } - } + // 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf) + // derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert. + val showGradient = hasOverflow && collapsedHeightDp != null && !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) LaunchedEffect(requestFocus) { @@ -173,7 +166,7 @@ fun ChecklistItemRow( Box(modifier = Modifier.weight(1f)) { // Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed Box( - modifier = if (!isFocused && useScrollClipping) { + modifier = if (!isFocused && hasOverflow && collapsedHeightDp != null) { Modifier .heightIn(max = collapsedHeightDp!!) .verticalScroll(scrollState) @@ -216,11 +209,13 @@ fun ChecklistItemRow( onNext = { onAddNewItem() } ), singleLine = false, - // maxLines nur als Fallback bis collapsedHeight berechnet ist - maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES, + // 🆕 v1.8.1: maxLines IMMER Int.MAX_VALUE — keine Oszillation möglich + // Höhenbegrenzung erfolgt ausschließlich über heightIn-Modifier oben. + // Vorher: maxLines=5 → lineCount gedeckelt → Overflow nie erkannt → Deadlock + maxLines = Int.MAX_VALUE, cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), onTextLayout = { textLayoutResult -> - // 🆕 v1.8.0: Overflow erkennen - ABER NUR wenn kein Drag aktiv ist + // 🆕 v1.8.1: lineCount ist jetzt akkurat (maxLines=MAX_VALUE deckelt nicht) if (!isAnyItemDragging) { val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES hasOverflow = overflow @@ -230,6 +225,10 @@ fun ChecklistItemRow( textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp() } } + // Reset wenn Text gekürzt wird + if (!overflow) { + collapsedHeightDp = null + } } }, decorationBox = { innerTextField ->