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.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -36,16 +35,16 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
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.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.kiwix.kiwixmobile.R.string import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R 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.KiwixButton
import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle 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.SwipeRefreshLayout
import org.kiwix.kiwixmobile.core.ui.components.rememberLazyListScrollListener
import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White import org.kiwix.kiwixmobile.core.ui.theme.White
@ -81,19 +82,13 @@ fun LocalLibraryScreen(
onMultiSelect: ((BookOnDisk) -> Unit)? = null, onMultiSelect: ((BookOnDisk) -> Unit)? = null,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val lazyListState = rememberLazyListState() val (bottomNavHeight, lazyListState) = rememberScrollBehavior(state)
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
}
}
KiwixTheme { KiwixTheme {
Scaffold( Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) }, 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() modifier = Modifier.systemBarsPadding()
) { contentPadding -> ) { contentPadding ->
SwipeRefreshLayout( 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 @Composable
private fun BookItemList( private fun BookItemList(
state: FileSelectListState, state: FileSelectListState,

View File

@ -19,6 +19,8 @@
package org.kiwix.kiwixmobile.core.ui.components package org.kiwix.kiwixmobile.core.ui.components
import androidx.annotation.StringRes 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.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource 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.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.Black
@ -53,14 +65,23 @@ fun KiwixAppBar(
@StringRes titleId: Int, @StringRes titleId: Int,
navigationIcon: @Composable () -> Unit, navigationIcon: @Composable () -> Unit,
actionMenuItems: List<ActionMenuItem> = emptyList(), 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 // Optional search bar, used in fragments that require it
searchBar: (@Composable () -> Unit)? = null 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 { KiwixTheme {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(KIWIX_APP_BAR_HEIGHT) .height(appBarHeight)
.background(color = Black), .background(color = Black),
verticalAlignment = Alignment.CenterVertically 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.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.White import org.kiwix.kiwixmobile.core.ui.theme.White
@ -51,11 +53,17 @@ fun SwipeRefreshLayout(
}, },
content: @Composable BoxScope.() -> Unit content: @Composable BoxScope.() -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope()
Box( Box(
modifier.pullToRefresh( modifier.pullToRefresh(
state = state, state = state,
isRefreshing = isRefreshing, isRefreshing = isRefreshing,
onRefresh = onRefresh, onRefresh = {
coroutineScope.launch {
state.animateToHidden()
onRefresh.invoke()
}
},
enabled = isEnabled enabled = isEnabled
), ),
contentAlignment = contentAlignment contentAlignment = contentAlignment