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 (commit1da1a63). Replaced with direct computation. Bug B1: animateItem() on dragged item conflicted with manual offset (from IMPL_017 commit900dad7). 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:
@@ -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?"),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
Reference in New Issue
Block a user