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.
This commit is contained in:
MohitMaliFtechiz 2025-03-20 19:37:23 +05:30
parent 80fbf74d9a
commit dd45b8a612
4 changed files with 159 additions and 15 deletions

View File

@ -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<MutableState<Dp>, 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,

View File

@ -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<ActionMenuItem> = 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<ActionMenuItem>) {
}
}
}
@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
}

View File

@ -0,0 +1,75 @@
/*
* Kiwix Android
* Copyright (c) 2025 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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
}

View File

@ -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