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
This commit is contained in:
inventory69
2026-02-11 08:48:49 +01:00
parent b5a3a3c096
commit 24fe32a973
3 changed files with 31 additions and 20 deletions

View File

@@ -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<Job?>(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?"),

View File

@@ -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,

View File

@@ -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 ->