feat(editor): IMPL_05 - Auto-scroll on line wrap in checklist editor

Changes:
- ChecklistItemRow.kt: Import mutableIntStateOf
- ChecklistItemRow.kt: Add onHeightChanged callback parameter
- ChecklistItemRow.kt: Track lastLineCount state for line wrap detection
- ChecklistItemRow.kt: Detect line count increase in onTextLayout and trigger callback
- NoteEditorScreen.kt: Add scrollToItemIndex state in ChecklistEditor
- NoteEditorScreen.kt: Add LaunchedEffect for auto-scroll on height change
- NoteEditorScreen.kt: Pass onHeightChanged callback to ChecklistItemRow

When typing in a checklist item at the bottom of the list, the editor now
automatically scrolls to keep the cursor visible when text wraps to a new line.
This commit is contained in:
inventory69
2026-02-11 10:42:08 +01:00
parent 66d98c0cad
commit 3e4b1bd07e
2 changed files with 36 additions and 3 deletions

View File

@@ -338,6 +338,7 @@ private fun LazyItemScope.DraggableChecklistItem(
onDelete: (String) -> Unit, onDelete: (String) -> Unit,
onAddNewItemAfter: (String) -> Unit, onAddNewItemAfter: (String) -> Unit,
onFocusHandled: () -> Unit, onFocusHandled: () -> Unit,
onHeightChanged: () -> Unit, // 🆕 v1.8.1 (IMPL_05)
) { ) {
val isDragging = dragDropState.draggingItemIndex == visualIndex val isDragging = dragDropState.draggingItemIndex == visualIndex
val elevation by animateDpAsState( val elevation by animateDpAsState(
@@ -363,6 +364,7 @@ private fun LazyItemScope.DraggableChecklistItem(
isDragging = isDragging, isDragging = isDragging,
isAnyItemDragging = dragDropState.draggingItemIndex != null, isAnyItemDragging = dragDropState.draggingItemIndex != null,
dragModifier = Modifier.dragContainer(dragDropState, visualIndex), dragModifier = Modifier.dragContainer(dragDropState, visualIndex),
onHeightChanged = onHeightChanged, // 🆕 v1.8.1 (IMPL_05)
modifier = Modifier modifier = Modifier
.then(if (!isDragging) Modifier.animateItem() else Modifier) .then(if (!isDragging) Modifier.animateItem() else Modifier)
.offset { .offset {
@@ -404,6 +406,9 @@ private fun ChecklistEditor(
onMove = onMove onMove = onMove
) )
// 🆕 v1.8.1 (IMPL_05): Auto-Scroll bei Zeilenumbruch
var scrollToItemIndex by remember { mutableStateOf<Int?>(null) }
// 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen // 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen
val uncheckedCount = items.count { !it.isChecked } val uncheckedCount = items.count { !it.isChecked }
val checkedCount = items.count { it.isChecked } val checkedCount = items.count { it.isChecked }
@@ -418,6 +423,21 @@ private fun ChecklistEditor(
dragDropState.separatorVisualIndex = separatorVisualIndex dragDropState.separatorVisualIndex = separatorVisualIndex
} }
// 🆕 v1.8.1 (IMPL_05): Auto-Scroll wenn ein Item durch Zeilenumbruch wächst
LaunchedEffect(scrollToItemIndex) {
scrollToItemIndex?.let { index ->
delay(50) // Warten bis Layout-Pass abgeschlossen
val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
if (index >= lastVisibleIndex - 1) {
listState.animateScrollToItem(
index = minOf(index + 1, items.size + if (showSeparator) 1 else 0),
scrollOffset = 0
)
}
scrollToItemIndex = null
}
}
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@@ -438,7 +458,8 @@ private fun ChecklistEditor(
onCheckedChange = onCheckedChange, onCheckedChange = onCheckedChange,
onDelete = onDelete, onDelete = onDelete,
onAddNewItemAfter = onAddNewItemAfter, onAddNewItemAfter = onAddNewItemAfter,
onFocusHandled = onFocusHandled onFocusHandled = onFocusHandled,
onHeightChanged = { scrollToItemIndex = index } // 🆕 v1.8.1 (IMPL_05)
) )
} }
@@ -466,7 +487,8 @@ private fun ChecklistEditor(
onCheckedChange = onCheckedChange, onCheckedChange = onCheckedChange,
onDelete = onDelete, onDelete = onDelete,
onAddNewItemAfter = onAddNewItemAfter, onAddNewItemAfter = onAddNewItemAfter,
onFocusHandled = onFocusHandled onFocusHandled = onFocusHandled,
onHeightChanged = { scrollToItemIndex = visualIndex } // 🆕 v1.8.1 (IMPL_05)
) )
} }
} }

View File

@@ -25,6 +25,7 @@ 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.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -70,6 +71,7 @@ fun ChecklistItemRow(
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
isAnyItemDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Hide gradient during any drag 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 dragModifier: Modifier = Modifier, // 🆕 v1.8.0: IMPL_023 - Drag modifier for handle
onHeightChanged: (() -> Unit)? = null, // 🆕 v1.8.1: IMPL_05 - Auto-scroll callback
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -91,6 +93,9 @@ 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.1: IMPL_05 - Letzte Zeilenanzahl tracken für Auto-Scroll
var lastLineCount by remember { mutableIntStateOf(0) }
// 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf) // 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf)
// derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert. // derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert.
val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging
@@ -216,8 +221,9 @@ fun ChecklistItemRow(
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
onTextLayout = { textLayoutResult -> onTextLayout = { textLayoutResult ->
// 🆕 v1.8.1: lineCount ist jetzt akkurat (maxLines=MAX_VALUE deckelt nicht) // 🆕 v1.8.1: lineCount ist jetzt akkurat (maxLines=MAX_VALUE deckelt nicht)
val lineCount = textLayoutResult.lineCount
if (!isAnyItemDragging) { if (!isAnyItemDragging) {
val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES val overflow = lineCount > COLLAPSED_MAX_LINES
hasOverflow = overflow hasOverflow = overflow
// Höhe der ersten 5 Zeilen berechnen (einmalig) // Höhe der ersten 5 Zeilen berechnen (einmalig)
if (overflow && collapsedHeightDp == null) { if (overflow && collapsedHeightDp == null) {
@@ -230,6 +236,11 @@ fun ChecklistItemRow(
collapsedHeightDp = null collapsedHeightDp = null
} }
} }
// 🆕 v1.8.1 (IMPL_05): Höhenänderung bei Zeilenumbruch melden
if (isFocused && lineCount > lastLineCount && lastLineCount > 0) {
onHeightChanged?.invoke()
}
lastLineCount = lineCount
}, },
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
Box { Box {