mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 18:56:44 -04:00
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:
parent
80fbf74d9a
commit
dd45b8a612
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user