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 draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset 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) private var overscrollJob by mutableStateOf<Job?>(null)
val draggingItemOffset: Float val draggingItemOffset: Float
@@ -49,7 +52,9 @@ class DragDropListState(
fun onDragStart(offset: Offset, itemIndex: Int) { fun onDragStart(offset: Offset, itemIndex: Int) {
draggingItemIndex = itemIndex draggingItemIndex = itemIndex
draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f val info = draggingItemLayoutInfo
draggingItemInitialOffset = info?.offset?.toFloat() ?: 0f
draggingItemSize = info?.size ?: 0
draggingItemDraggedDelta = 0f draggingItemDraggedDelta = 0f
} }
@@ -57,6 +62,7 @@ class DragDropListState(
draggingItemDraggedDelta = 0f draggingItemDraggedDelta = 0f
draggingItemIndex = null draggingItemIndex = null
draggingItemInitialOffset = 0f draggingItemInitialOffset = 0f
draggingItemSize = 0
overscrollJob?.cancel() overscrollJob?.cancel()
} }
@@ -65,7 +71,8 @@ class DragDropListState(
val draggingItem = draggingItemLayoutInfo ?: return val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset 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 // 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter
// Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"), // 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 } key = { _, item -> item.id }
) { index, item -> ) { index, item ->
// 🆕 v1.8.0 (IMPL_017): Separator vor dem ersten Checked-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) CheckedItemsSeparator(checkedCount = checkedCount)
} }
@@ -409,7 +411,10 @@ private fun ChecklistEditor(
// 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle // 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle
dragModifier = Modifier.dragContainer(dragDropState, index), dragModifier = Modifier.dragContainer(dragDropState, index),
modifier = Modifier 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 { .offset {
IntOffset( IntOffset(
0, 0,

View File

@@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -92,17 +91,11 @@ fun ChecklistItemRow(
// 🆕 v1.8.0: ScrollState für dynamischen Gradient // 🆕 v1.8.0: ScrollState für dynamischen Gradient
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
// 🆕 v1.8.0: Scroll-basierter Ansatz aktiv wenn Höhe berechnet wurde // 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf)
val useScrollClipping = hasOverflow && collapsedHeightDp != null // derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert.
val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging
// 🆕 v1.8.0: Dynamische Gradient-Sichtbarkeit basierend auf Scroll-Position val showTopGradient = showGradient && scrollState.value > 0
val showGradient = useScrollClipping && !isFocused && !isAnyItemDragging val showBottomGradient = showGradient && scrollState.value < scrollState.maxValue
val showTopGradient by remember {
derivedStateOf { showGradient && scrollState.value > 0 }
}
val showBottomGradient by remember {
derivedStateOf { 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) {
@@ -173,7 +166,7 @@ fun ChecklistItemRow(
Box(modifier = Modifier.weight(1f)) { Box(modifier = Modifier.weight(1f)) {
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed // Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
Box( Box(
modifier = if (!isFocused && useScrollClipping) { modifier = if (!isFocused && hasOverflow && collapsedHeightDp != null) {
Modifier Modifier
.heightIn(max = collapsedHeightDp!!) .heightIn(max = collapsedHeightDp!!)
.verticalScroll(scrollState) .verticalScroll(scrollState)
@@ -216,11 +209,13 @@ fun ChecklistItemRow(
onNext = { onAddNewItem() } onNext = { onAddNewItem() }
), ),
singleLine = false, singleLine = false,
// maxLines nur als Fallback bis collapsedHeight berechnet ist // 🆕 v1.8.1: maxLines IMMER Int.MAX_VALUE — keine Oszillation möglich
maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES, // 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), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
onTextLayout = { textLayoutResult -> 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) { if (!isAnyItemDragging) {
val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES
hasOverflow = overflow hasOverflow = overflow
@@ -230,6 +225,10 @@ fun ChecklistItemRow(
textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp() textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp()
} }
} }
// Reset wenn Text gekürzt wird
if (!overflow) {
collapsedHeightDp = null
}
} }
}, },
decorationBox = { innerTextField -> decorationBox = { innerTextField ->