Refactored the bottomNavigation to properly show the current selected page.

* Fixed: `JNI DETECTED ERROR IN APPLICATION` when opening the ZIM file in lower devices.
This commit is contained in:
MohitMaliFtechiz 2025-08-06 00:56:47 +05:30
parent 03cd131b9b
commit 19006f30b7
2 changed files with 103 additions and 85 deletions

View File

@ -32,6 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
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
@ -181,7 +182,7 @@ fun BottomNavigationBar(
val currentDestinationRoute = navBackStackEntry?.destination?.route val currentDestinationRoute = navBackStackEntry?.destination?.route
BottomAppBar( BottomAppBar(
containerColor = Black, containerColor = Black,
contentColor = White, contentColor = White.copy(alpha = 0.5f),
scrollBehavior = bottomAppBarScrollBehaviour scrollBehavior = bottomAppBarScrollBehaviour
) { ) {
bottomNavItems.forEach { item -> bottomNavItems.forEach { item ->
@ -198,11 +199,14 @@ fun BottomNavigationBar(
icon = { icon = {
Icon( Icon(
painter = painterResource(id = item.iconRes), painter = painterResource(id = item.iconRes),
contentDescription = item.title contentDescription = item.title,
tint = White
) )
}, },
label = { Text(item.title) }, label = { Text(item.title, color = White) },
modifier = Modifier.semantics { testTag = item.testingTag } modifier = Modifier.semantics { testTag = item.testingTag },
colors = NavigationBarItemDefaults.colors()
.copy(selectedIndicatorColor = White.copy(alpha = 0.3f))
) )
} }
} }

View File

@ -34,7 +34,6 @@ import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -83,13 +82,13 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -117,7 +116,6 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
@ -192,10 +190,14 @@ fun ReaderScreen(
mainActivityBottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?, mainActivityBottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val localWebViewScrollState: MutableState<ScrollState?> = // For managing the scroll event handling of webView.
remember { mutableStateOf(ScrollState(0)) } val shouldUpdateTopAppBarAndBottomAppBarOnScrolling = remember { mutableStateOf(true) }
val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
LaunchedEffect(bottomAppBarScrollBehavior.state.heightOffset) {
mainActivityBottomAppBarScrollBehaviour?.state?.heightOffset =
bottomAppBarScrollBehavior.state.heightOffset
}
KiwixDialogTheme { KiwixDialogTheme {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Scaffold( Scaffold(
@ -213,11 +215,6 @@ fun ReaderScreen(
.systemBarsPadding() .systemBarsPadding()
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection) .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
.nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection) .nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection)
.let { baseModifier ->
mainActivityBottomAppBarScrollBehaviour?.let {
baseModifier.nestedScroll(it.nestedScrollConnection)
} ?: baseModifier
}
.semantics { testTag = READER_SCREEN_TESTING_TAG } .semantics { testTag = READER_SCREEN_TESTING_TAG }
) { paddingValues -> ) { paddingValues ->
OnBackPressed(onUserBackPressed, navHostController) OnBackPressed(onUserBackPressed, navHostController)
@ -225,9 +222,13 @@ fun ReaderScreen(
state, state,
Modifier.padding(paddingValues), Modifier.padding(paddingValues),
bottomAppBarScrollBehavior, bottomAppBarScrollBehavior,
localWebViewScrollState.value topAppBarScrollBehavior,
shouldUpdateTopAppBarAndBottomAppBarOnScrolling
) )
} }
LaunchedEffect(showTableOfContentDrawer.value) {
shouldUpdateTopAppBarAndBottomAppBarOnScrolling.value = !showTableOfContentDrawer.value
}
if (showTableOfContentDrawer.value) { if (showTableOfContentDrawer.value) {
// Showing the background color on screen so that it look same as navigation drawer. // Showing the background color on screen so that it look same as navigation drawer.
Box( Box(
@ -248,7 +249,6 @@ fun ReaderScreen(
TableDrawerSheet( TableDrawerSheet(
title = state.tableOfContentTitle, title = state.tableOfContentTitle,
sections = documentSections.orEmpty(), sections = documentSections.orEmpty(),
localWebViewScrollState,
state.selectedWebView, state.selectedWebView,
showTableOfContentDrawer showTableOfContentDrawer
) )
@ -297,7 +297,8 @@ private fun ReaderContentLayout(
state: ReaderScreenState, state: ReaderScreenState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior, bottomAppBarScrollBehavior: BottomAppBarScrollBehavior,
webViewScrollState: ScrollState? topAppBarScrollBehavior: TopAppBarScrollBehavior,
shouldUpdateTopAppBarAndBottomAppBarOnScrolling: MutableState<Boolean>,
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
TabSwitcherAnimated(state) TabSwitcherAnimated(state)
@ -307,7 +308,12 @@ private fun ReaderContentLayout(
state.fullScreenItem.first -> ShowFullScreenView(state) state.fullScreenItem.first -> ShowFullScreenView(state)
else -> { else -> {
ShowZIMFileContent(state, webViewScrollState) ShowZIMFileContent(
state,
bottomAppBarScrollBehavior,
topAppBarScrollBehavior,
shouldUpdateTopAppBarAndBottomAppBarOnScrolling
)
ShowProgressBarIfZIMFilePageIsLoading(state) ShowProgressBarIfZIMFilePageIsLoading(state)
Column(Modifier.align(Alignment.BottomCenter)) { Column(Modifier.align(Alignment.BottomCenter)) {
TtsControls(state) TtsControls(state)
@ -332,12 +338,11 @@ private fun ReaderContentLayout(
} }
} }
@Suppress("LongMethod", "UnsafeCallOnNullableType") @Suppress("LongMethod")
@Composable @Composable
fun TableDrawerSheet( fun TableDrawerSheet(
title: String, title: String,
sections: List<DocumentSection>, sections: List<DocumentSection>,
webViewScrollState: MutableState<ScrollState?>,
selectedWebView: KiwixWebView?, selectedWebView: KiwixWebView?,
showTableOfContentDrawer: MutableState<Boolean> showTableOfContentDrawer: MutableState<Boolean>
) { ) {
@ -346,13 +351,6 @@ fun TableDrawerSheet(
} else { } else {
White White
} }
var scrollToSectionIndex by remember { mutableStateOf<Int?>(null) }
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(scrollToSectionIndex) {
scrollToSectionIndex?.let {
webViewScrollState.value = null
}
}
ModalDrawerSheet( ModalDrawerSheet(
modifier = Modifier.width(NAVIGATION_DRAWER_WIDTH), modifier = Modifier.width(NAVIGATION_DRAWER_WIDTH),
drawerShape = RectangleShape, drawerShape = RectangleShape,
@ -366,10 +364,10 @@ fun TableDrawerSheet(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable {
coroutineScope.launch { onTableOfContentHeaderClick(
webViewScrollState.value?.animateScrollTo(ZERO) selectedWebView,
} showTableOfContentDrawer
showTableOfContentDrawer.update { false } )
} }
.padding(horizontal = SIXTEEN_DP, vertical = TWELVE_DP) .padding(horizontal = SIXTEEN_DP, vertical = TWELVE_DP)
) )
@ -384,28 +382,43 @@ fun TableDrawerSheet(
), ),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { scrollToSectionIndex = index } .clickable {
onTableOfContentSectionClick(
selectedWebView,
index,
sections,
showTableOfContentDrawer
)
}
.padding(start = paddingStart.dp, top = EIGHT_DP, bottom = EIGHT_DP, end = SIXTEEN_DP) .padding(start = paddingStart.dp, top = EIGHT_DP, bottom = EIGHT_DP, end = SIXTEEN_DP)
) )
} }
} }
} }
LaunchedEffect(webViewScrollState.value) { }
if (webViewScrollState.value == null &&
scrollToSectionIndex != null && private fun onTableOfContentHeaderClick(
hasItemForPositionInDocumentSectionsList(scrollToSectionIndex!!, sections) selectedWebView: KiwixWebView?,
) { showTableOfContentDrawer: MutableState<Boolean>
val targetId = sections[scrollToSectionIndex!!].id.replace("'", "\\'") ) {
selectedWebView?.evaluateJavascript( selectedWebView?.scrollY = ZERO
"document.getElementById('$targetId')?.scrollIntoView();", showTableOfContentDrawer.update { false }
null }
)
delay(HUNDERED.toLong()) private fun onTableOfContentSectionClick(
webViewScrollState.value = ScrollState(selectedWebView?.scrollY ?: ZERO) selectedWebView: KiwixWebView?,
scrollToSectionIndex = null position: Int,
showTableOfContentDrawer.update { false } sections: List<DocumentSection>,
} showTableOfContentDrawer: MutableState<Boolean>
) {
if (hasItemForPositionInDocumentSectionsList(position, sections)) {
val targetId = sections[position].id.replace("'", "\\'")
selectedWebView?.evaluateJavascript(
"document.getElementById('$targetId')?.scrollIntoView();",
null
)
} }
showTableOfContentDrawer.update { false }
} }
private fun hasItemForPositionInDocumentSectionsList( private fun hasItemForPositionInDocumentSectionsList(
@ -523,49 +536,50 @@ private fun BoxScope.CloseFullScreenImageButton(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ShowZIMFileContent(state: ReaderScreenState, webViewScrollState: ScrollState?) { private fun ShowZIMFileContent(
state: ReaderScreenState,
bottomAppBarScrollBehavior: BottomAppBarScrollBehavior,
topAppBarScrollBehavior: TopAppBarScrollBehavior,
shouldUpdateTopAppBarAndBottomAppBarOnScrolling: MutableState<Boolean>
) {
state.selectedWebView?.let { selectedWebView -> state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) { key(selectedWebView) {
ScrollableWebViewWithNestedScroll( DisposableEffect(Unit) {
selectedWebView = selectedWebView, val listener = View.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
modifier = Modifier.fillMaxSize(), val deltaY = (scrollY - oldScrollY).toFloat()
webViewScrollState = webViewScrollState if (deltaY == 0f || !shouldUpdateTopAppBarAndBottomAppBarOnScrolling.value) return@OnScrollChangeListener
) val topLimit = topAppBarScrollBehavior.state.heightOffsetLimit
} val bottomLimit = bottomAppBarScrollBehavior.state.heightOffsetLimit
}
}
@OptIn(ExperimentalMaterial3Api::class) topAppBarScrollBehavior.state.heightOffset =
@Composable (topAppBarScrollBehavior.state.heightOffset - deltaY)
fun ScrollableWebViewWithNestedScroll( .coerceIn(topLimit, 0f)
selectedWebView: KiwixWebView,
modifier: Modifier = Modifier, bottomAppBarScrollBehavior.state.heightOffset =
webViewScrollState: ScrollState? (bottomAppBarScrollBehavior.state.heightOffset - deltaY)
) { .coerceIn(bottomLimit, 0f)
Box( }
modifier = modifier
.fillMaxSize() selectedWebView.setOnScrollChangeListener(listener)
.let { baseModifier ->
webViewScrollState?.let { onDispose {
baseModifier.verticalScroll(it) selectedWebView.setOnScrollChangeListener(null)
} ?: run {
baseModifier
} }
} }
) { AndroidView(
AndroidView( factory = { context ->
factory = { context -> FrameLayout(context).apply {
FrameLayout(context).apply { (selectedWebView.parent as? ViewGroup)?.removeView(selectedWebView)
(selectedWebView.parent as? ViewGroup)?.removeView(selectedWebView) selectedWebView.layoutParams = FrameLayout.LayoutParams(
selectedWebView.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT
FrameLayout.LayoutParams.MATCH_PARENT )
) addView(selectedWebView)
addView(selectedWebView) }
} },
}, modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(), )
) }
} }
} }