From dd45b8a6120a9e0155bb847f8f6923845ff1fd1f Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Thu, 20 Mar 2025 19:37:23 +0530 Subject: [PATCH] Fixed: The `swipeRefreshIndicator` is showing when disabling the `SwipeRefreshLayout`. * Implemented rememberLazyListScrollListener to efficiently handle scrolling events. It triggers onScrollChanged only when the scroll direction changes, reducing unnecessary event triggers and recompositions. This improves performance and allows reuse across multiple screens. * Refactored KiwixAppBar to optionally auto-hide and show based on scroll behavior when a LazyListState is provided. Simply pass the LazyListState where scrolling behavior is needed, and the toolbar will automatically hide/show as the user scrolls. If no LazyListState is passed, it functions as a standard toolbar. --- .../library/local/LocalLibraryScreen.kt | 46 ++++++++---- .../core/ui/components/KiwixAppBar.kt | 43 ++++++++++- .../ui/components/LazyListScrollListener.kt | 75 +++++++++++++++++++ .../core/ui/components/SwipeRefreshLayout.kt | 10 ++- 4 files changed, 159 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/LazyListScrollListener.kt diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryScreen.kt index d48e88867..e428f8812 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/local/LocalLibraryScreen.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -36,16 +35,16 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.R.string import org.kiwix.kiwixmobile.core.R @@ -55,7 +54,9 @@ import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixButton import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle +import org.kiwix.kiwixmobile.core.ui.components.ScrollDirection import org.kiwix.kiwixmobile.core.ui.components.SwipeRefreshLayout +import org.kiwix.kiwixmobile.core.ui.components.rememberLazyListScrollListener import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.White @@ -81,19 +82,13 @@ fun LocalLibraryScreen( onMultiSelect: ((BookOnDisk) -> Unit)? = null, navigationIcon: @Composable () -> Unit ) { - val lazyListState = rememberLazyListState() - val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() } - val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) } - LaunchedEffect(lazyListState) { - snapshotFlow { lazyListState.firstVisibleItemScrollOffset } - .collect { scrollOffset -> - bottomNavHeight.value = if (scrollOffset > 0) ZERO.dp else bottomNavHeightInDp - } - } + val (bottomNavHeight, lazyListState) = rememberScrollBehavior(state) KiwixTheme { Scaffold( snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) }, - topBar = { KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems) }, + topBar = { + KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems, lazyListState) + }, modifier = Modifier.systemBarsPadding() ) { contentPadding -> SwipeRefreshLayout( @@ -134,6 +129,31 @@ fun LocalLibraryScreen( } } +@Composable +private fun rememberScrollBehavior( + state: LocalLibraryScreenState +): Pair, LazyListState> { + val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() } + val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) } + val lazyListState = rememberLazyListScrollListener( + onScrollChanged = { direction -> + when (direction) { + ScrollDirection.SCROLL_UP -> { + bottomNavHeight.value = bottomNavHeightInDp + } + + ScrollDirection.SCROLL_DOWN -> { + bottomNavHeight.value = ZERO.dp + } + + ScrollDirection.IDLE -> {} + } + } + ) + + return bottomNavHeight to lazyListState +} + @Composable private fun BookItemList( state: FileSelectListState, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt index de3d696ea..d282c9feb 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt @@ -19,6 +19,8 @@ package org.kiwix.kiwixmobile.core.ui.components import androidx.annotation.StringRes +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Row @@ -26,16 +28,26 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.toPainter import org.kiwix.kiwixmobile.core.ui.theme.Black @@ -53,14 +65,23 @@ fun KiwixAppBar( @StringRes titleId: Int, navigationIcon: @Composable () -> Unit, actionMenuItems: List = emptyList(), + // If this state is provided, the app bar will automatically hide on scroll down and show + // on scroll up, same like scrollingToolbar. + lazyListState: LazyListState? = null, // Optional search bar, used in fragments that require it searchBar: (@Composable () -> Unit)? = null ) { + val isToolbarVisible = rememberToolbarVisibility(lazyListState) + + val appBarHeight by animateDpAsState( + targetValue = if (isToolbarVisible) KIWIX_APP_BAR_HEIGHT else 0.dp, + animationSpec = tween(durationMillis = 250) + ) KiwixTheme { Row( modifier = Modifier .fillMaxWidth() - .height(KIWIX_APP_BAR_HEIGHT) + .height(appBarHeight) .background(color = Black), verticalAlignment = Alignment.CenterVertically ) { @@ -116,3 +137,23 @@ private fun ActionMenu(actionMenuItems: List) { } } } + +@Composable +private fun rememberToolbarVisibility(lazyListState: LazyListState?): Boolean { + var isToolbarVisible by remember { mutableStateOf(true) } + var lastScrollIndex by remember { mutableIntStateOf(0) } + val updatedLazyListState = rememberUpdatedState(lazyListState) + + LaunchedEffect(updatedLazyListState) { + updatedLazyListState.value?.let { state -> + snapshotFlow { state.firstVisibleItemIndex } + .collect { newScrollIndex -> + if (newScrollIndex != lastScrollIndex) { + isToolbarVisible = newScrollIndex < lastScrollIndex + lastScrollIndex = newScrollIndex + } + } + } + } + return isToolbarVisible +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/LazyListScrollListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/LazyListScrollListener.kt new file mode 100644 index 000000000..89ad535b3 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/LazyListScrollListener.kt @@ -0,0 +1,75 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.ui.components + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow + +const val ONE_THOUSAND = 1000 + +@Composable +fun rememberLazyListScrollListener( + onScrollChanged: (ScrollDirection) -> Unit, + scrollThreshold: Int = 20 +): LazyListState { + val lazyListState = rememberLazyListState() + val updatedOnScrollChanged = rememberUpdatedState(onScrollChanged) + + var previousScrollPosition by remember { mutableIntStateOf(0) } + var lastScrollDirection by remember { mutableStateOf(ScrollDirection.IDLE) } + + LaunchedEffect(lazyListState) { + snapshotFlow { + lazyListState.firstVisibleItemIndex to lazyListState.firstVisibleItemScrollOffset + }.collect { (index, offset) -> + val currentScrollPosition = index * ONE_THOUSAND + offset + val scrollDelta = currentScrollPosition - previousScrollPosition + + val scrollDirection = when { + scrollDelta > scrollThreshold -> ScrollDirection.SCROLL_DOWN + scrollDelta < -scrollThreshold -> ScrollDirection.SCROLL_UP + else -> lastScrollDirection + } + + if (scrollDirection != lastScrollDirection) { + lastScrollDirection = scrollDirection + updatedOnScrollChanged.value(scrollDirection) + } + + previousScrollPosition = currentScrollPosition + } + } + + return lazyListState +} + +enum class ScrollDirection { + SCROLL_UP, + SCROLL_DOWN, + IDLE +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/SwipeRefreshLayout.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/SwipeRefreshLayout.kt index c9da3d0c1..4593dda79 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/SwipeRefreshLayout.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/SwipeRefreshLayout.kt @@ -26,8 +26,10 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshState import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.White @@ -51,11 +53,17 @@ fun SwipeRefreshLayout( }, content: @Composable BoxScope.() -> Unit ) { + val coroutineScope = rememberCoroutineScope() Box( modifier.pullToRefresh( state = state, isRefreshing = isRefreshing, - onRefresh = onRefresh, + onRefresh = { + coroutineScope.launch { + state.animateToHidden() + onRefresh.invoke() + } + }, enabled = isEnabled ), contentAlignment = contentAlignment