From b24b14c73d5ae992534d1350ebd8373d5826f3a6 Mon Sep 17 00:00:00 2001 From: dhanuarf <10323845+dhanuarf@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:21:41 +0700 Subject: [PATCH] fix(sources-search): search box visibility glitch when scrolling (#1278) * fix(sources-screen): search box visibility glitch when scrolling * Refactor to a reusable function * Apply to MigrateSourceScreen * Fix warning * suggestions * Revert "Fix warning" This reverts commit b1c97bcf04b9e34b3eb314fd54c45be953d2f451. * Add comment --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> --- .../browse/MigrateSourceScreen.kt | 178 +++++++++--------- .../presentation/browse/SourcesScreen.kt | 35 ++-- .../components/SourceSearchBox.kt | 5 +- .../presentation/core/util/LazyListState.kt | 38 ++++ 4 files changed, 153 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt index ab06ae783..6418c3878 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSourceScreen.kt @@ -3,6 +3,7 @@ package eu.kanade.presentation.browse import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -30,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.model.installedExtension import eu.kanade.presentation.browse.components.BaseSourceItem @@ -48,7 +50,6 @@ import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.components.Badge import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.components.FastScrollLazyColumn -import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.icons.FlagEmoji @@ -126,100 +127,105 @@ private fun MigrateSourceList( } Column( - // Wrap around so we can use stickyHeader modifier = Modifier.padding(contentPadding), ) { - AnimatedFloatingSearchBox( - listState = lazyListState, - searchQuery = state.searchQuery, - onChangeSearchQuery = onChangeSearchQuery, - placeholderText = stringResource(KMR.strings.action_search_for_source), + // KMK <-- + Row( modifier = Modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), - ) - - FastScrollLazyColumn( - state = lazyListState, - // contentPadding = contentPadding + topSmallPaddingValues, - // KMK <-- + .background(MaterialTheme.colorScheme.background) + .padding(start = MaterialTheme.padding.medium), + verticalAlignment = Alignment.CenterVertically, ) { - stickyHeader(key = STICKY_HEADER_KEY_PREFIX) { - Row( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(start = MaterialTheme.padding.medium), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(MR.strings.migration_selection_prompt), - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.header, - ) + Text( + text = stringResource(MR.strings.migration_selection_prompt), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.header, + ) - // KMK --> - IconButton(onClick = { filterObsoleteSource = !filterObsoleteSource }) { - Icon( - Icons.Outlined.NewReleases, - contentDescription = stringResource(MR.strings.ext_obsolete), - tint = MaterialTheme.colorScheme.error - .takeIf { filterObsoleteSource } ?: LocalContentColor.current, - ) - } + // KMK --> + IconButton(onClick = { filterObsoleteSource = !filterObsoleteSource }) { + Icon( + Icons.Outlined.NewReleases, + contentDescription = stringResource(MR.strings.ext_obsolete), + tint = MaterialTheme.colorScheme.error + .takeIf { filterObsoleteSource } ?: LocalContentColor.current, + ) + } + // KMK <-- + IconButton(onClick = onToggleSortingMode) { + when (sortingMode) { + SetMigrateSorting.Mode.ALPHABETICAL -> Icon( + Icons.Outlined.SortByAlpha, + contentDescription = stringResource(MR.strings.action_sort_alpha), + ) + SetMigrateSorting.Mode.TOTAL -> Icon( + Icons.Outlined.Numbers, + contentDescription = stringResource(MR.strings.action_sort_count), + ) + } + } + IconButton(onClick = onToggleSortingDirection) { + when (sortingDirection) { + SetMigrateSorting.Direction.ASCENDING -> Icon( + Icons.Outlined.ArrowUpward, + contentDescription = stringResource(MR.strings.action_asc), + ) + SetMigrateSorting.Direction.DESCENDING -> Icon( + Icons.Outlined.ArrowDownward, + contentDescription = stringResource(MR.strings.action_desc), + ) + } + } + } + + // KMK --> + Box { + FastScrollLazyColumn( + state = lazyListState, + contentPadding = PaddingValues(top = 65.dp), + // KMK <-- + ) { + items( + items = list + // KMK --> + .filter { + !filterObsoleteSource || + ( + it.first.installedExtension?.isObsolete != false && + (!isHentaiEnabled || it.first.id !in (EHENTAI_EXT_SOURCES.keys + EXHENTAI_EXT_SOURCES.keys)) + ) + }, // KMK <-- - IconButton(onClick = onToggleSortingMode) { - when (sortingMode) { - SetMigrateSorting.Mode.ALPHABETICAL -> Icon( - Icons.Outlined.SortByAlpha, - contentDescription = stringResource(MR.strings.action_sort_alpha), - ) - SetMigrateSorting.Mode.TOTAL -> Icon( - Icons.Outlined.Numbers, - contentDescription = stringResource(MR.strings.action_sort_count), - ) - } - } - IconButton(onClick = onToggleSortingDirection) { - when (sortingDirection) { - SetMigrateSorting.Direction.ASCENDING -> Icon( - Icons.Outlined.ArrowUpward, - contentDescription = stringResource(MR.strings.action_asc), - ) - SetMigrateSorting.Direction.DESCENDING -> Icon( - Icons.Outlined.ArrowDownward, - contentDescription = stringResource(MR.strings.action_desc), - ) - } - } + key = { (source, _) -> "migrate-${source.id}" }, + ) { (source, count) -> + MigrateSourceItem( + // KMK --> + // modifier = Modifier.animateItem(), + modifier = Modifier.animateItemFastScroll(), + // KMK <-- + source = source, + count = count, + onClickItem = { onClickItem(source) }, + onLongClickItem = { onLongClickItem(source) }, + ) } } - items( - items = list - // KMK --> - .filter { - !filterObsoleteSource || - ( - it.first.installedExtension?.isObsolete != false && - (!isHentaiEnabled || it.first.id !in (EHENTAI_EXT_SOURCES.keys + EXHENTAI_EXT_SOURCES.keys)) - ) - }, - // KMK <-- - key = { (source, _) -> "migrate-${source.id}" }, - ) { (source, count) -> - MigrateSourceItem( - // KMK --> - // modifier = Modifier.animateItem(), - modifier = Modifier.animateItemFastScroll(), - // KMK <-- - source = source, - count = count, - onClickItem = { onClickItem(source) }, - onLongClickItem = { onLongClickItem(source) }, - ) - } + // KMK --> + AnimatedFloatingSearchBox( + listState = lazyListState, + searchQuery = state.searchQuery, + onChangeSearchQuery = onChangeSearchQuery, + placeholderText = stringResource(KMR.strings.action_search_for_source), + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ) + .align(Alignment.TopCenter), + ) + // KMK <-- } } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt index 6a07dd63d..515be57b4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/SourcesScreen.kt @@ -1,7 +1,9 @@ package eu.kanade.presentation.browse import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -23,6 +25,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -82,24 +85,12 @@ fun SourcesScreen( modifier = Modifier.padding(contentPadding), ) // KMK --> - else -> Column( - // Wrap around so we can use stickyHeader + else -> Box( modifier = Modifier.padding(contentPadding), ) { - AnimatedFloatingSearchBox( - listState = lazyListState, - searchQuery = state.searchQuery, - onChangeSearchQuery = onChangeSearchQuery, - placeholderText = stringResource(KMR.strings.action_search_for_source), - modifier = Modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), - ) - FastScrollLazyColumn( state = lazyListState, + contentPadding = PaddingValues(top = 65.dp), // KMK <-- ) { items( @@ -141,6 +132,22 @@ fun SourcesScreen( } } } + + // KMK --> + AnimatedFloatingSearchBox( + listState = lazyListState, + searchQuery = state.searchQuery, + onChangeSearchQuery = onChangeSearchQuery, + placeholderText = stringResource(KMR.strings.action_search_for_source), + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ) + .align(Alignment.TopCenter), + ) + // KMK <-- } } } diff --git a/app/src/main/java/eu/kanade/presentation/components/SourceSearchBox.kt b/app/src/main/java/eu/kanade/presentation/components/SourceSearchBox.kt index bc03c9a5a..bc4852b6b 100644 --- a/app/src/main/java/eu/kanade/presentation/components/SourceSearchBox.kt +++ b/app/src/main/java/eu/kanade/presentation/components/SourceSearchBox.kt @@ -44,8 +44,7 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide -import tachiyomi.presentation.core.util.isScrolledToStart -import tachiyomi.presentation.core.util.isScrollingDown +import tachiyomi.presentation.core.util.isItemScrollingUp import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.secondaryItemAlpha @@ -61,7 +60,7 @@ fun AnimatedFloatingSearchBox( keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current, ) { AnimatedVisibility( - visible = listState.isScrolledToStart() || !listState.isScrollingDown(), + visible = listState.isItemScrollingUp(), enter = expandVertically(), exit = shrinkVertically(), modifier = modifier, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt index 5106934ef..5d77db82c 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/LazyListState.kt @@ -2,11 +2,15 @@ package tachiyomi.presentation.core.util import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun LazyListState.shouldExpandFAB(): Boolean { @@ -75,3 +79,37 @@ fun LazyListState.isScrollingDown(): Boolean { } }.value } + +// KMK --> +@Composable +private fun LazyListState.isItemScrolling( + initialValue: Boolean, + comparison: (Int, Int) -> Boolean, +): Boolean { + var isScrolling by remember { mutableStateOf(initialValue) } + var previousIndex by remember { mutableIntStateOf(firstVisibleItemIndex) } + + LaunchedEffect(this) { + snapshotFlow { firstVisibleItemIndex } + .distinctUntilChanged() + .collect { currentIndex -> + if (previousIndex != currentIndex) { + isScrolling = comparison(previousIndex, currentIndex) + previousIndex = currentIndex + } + } + } + + return isScrolling +} + +@Composable +fun LazyListState.isItemScrollingUp(initiallyVisible: Boolean = true): Boolean { + return isItemScrolling(initialValue = initiallyVisible) { previous, current -> previous > current } +} + +@Composable +fun LazyListState.isItemScrollingDown(initiallyVisible: Boolean = false): Boolean { + return isItemScrolling(initialValue = initiallyVisible) { previous, current -> previous < current } +} +// KMK <--