Implemented an approach to dynamically show/hide the XML-based bottomNavigationView from compose UI. We will remove this custom logic after we migrate the BottomNavigationView to compose.

* Refactored KiwixAppBar to support the scrolling behavior. By default, it will work as a normal toolbar, if on any screen there is a need to scroll the toolbar then pass the `TopAppBarDefaults.enterAlwaysScrollBehavior()` to `topAppBarScrollBehavior` and it will automatically do the job.
This commit is contained in:
MohitMaliFtechiz 2025-03-21 14:51:43 +05:30
parent dd45b8a612
commit 9a7df5e1d9
8 changed files with 111 additions and 74 deletions

View File

@ -52,6 +52,7 @@ import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DOWNLOAD_NOTIFICATION_TITLE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB
@ -68,6 +69,7 @@ const val NAVIGATE_TO_ZIM_HOST_FRAGMENT = "navigate_to_zim_host_fragment"
const val ACTION_GET_CONTENT = "GET_CONTENT"
const val OPENING_ZIM_FILE_DELAY = 300L
const val GET_CONTENT_SHORTCUT_ID = "get_content_shortcut"
const val KIWIX_BOTTOM_BAR_ANIMATION_DURATION = 250L
class KiwixMainActivity : CoreMainActivity() {
private var actionMode: ActionMode? = null
@ -223,6 +225,25 @@ class KiwixMainActivity : CoreMainActivity() {
setDefaultDeviceLanguage()
}
/**
* This is for manually showing/hiding the BottomNavigationView with animation from compose
* screens until we migrate the BottomNavigationView to compose. Once we migrate we will remove it.
*
* TODO Remove this once we migrate to compose.
*/
fun toggleBottomNavigation(isVisible: Boolean) {
activityKiwixMainBinding.bottomNavView.animate()
?.translationY(
if (isVisible) {
ZERO.toFloat()
} else {
activityKiwixMainBinding.bottomNavView.height.toFloat()
}
)
?.setDuration(KIWIX_BOTTOM_BAR_ANIMATION_DURATION)
?.start()
}
private fun setDefaultDeviceLanguage() {
if (sharedPreferenceUtil.prefDeviceDefaultLanguage.isEmpty()) {
ConfigurationCompat.getLocales(

View File

@ -36,9 +36,11 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.core.app.ActivityCompat
@ -79,6 +81,7 @@ import org.kiwix.kiwixmobile.core.navigateToSettings
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon
import org.kiwix.kiwixmobile.core.ui.components.rememberBottomNavigationVisibility
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.IconItem
import org.kiwix.kiwixmobile.core.utils.EXTERNAL_SELECT_POSITION
@ -173,14 +176,19 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
): View? {
LanguageUtils(requireActivity())
.changeFont(requireActivity(), sharedPreferenceUtil)
val composeView = ComposeView(requireContext()).apply {
return ComposeView(requireContext()).apply {
setContent {
val lazyListState = rememberLazyListState()
val isBottomNavVisible = rememberBottomNavigationVisibility(lazyListState)
LaunchedEffect(isBottomNavVisible) {
(requireActivity() as KiwixMainActivity).toggleBottomNavigation(isBottomNavVisible)
}
updateLibraryScreenState(
bottomNavigationHeight = getBottomNavigationHeight(),
actionMenuItems = actionMenuItems()
)
LocalLibraryScreen(
listState = lazyListState,
state = libraryScreenState.value,
fabButtonClick = { filePickerButtonClick() },
onClick = { onBookItemClick(it) },
@ -192,25 +200,25 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
NavigationIcon(
iconItem = IconItem.Vector(Icons.Filled.Menu),
contentDescription = string.open_drawer,
onClick = {
// Manually handle the navigation open/close.
// Since currently we are using the view based navigation drawer in other screens.
// Once we fully migrate to jetpack compose we will refactor this code to use the
// compose navigation.
// TODO Replace with compose based navigation when migration is done.
val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else {
activity.openNavigationDrawer()
}
}
onClick = { navigationIconClick() }
)
}
}
}
}
return composeView
private fun navigationIconClick() {
// Manually handle the navigation open/close.
// Since currently we are using the view based navigation drawer in other screens.
// Once we fully migrate to jetpack compose we will refactor this code to use the
// compose navigation.
// TODO Replace with compose based navigation when migration is done.
val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer()
} else {
activity.openNavigationDrawer()
}
}
private fun actionMenuItems() = listOf(

View File

@ -34,12 +34,14 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -61,8 +63,7 @@ import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FAB_ICON_BOTTOM_MARGIN
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.ui.BookItem
@ -74,6 +75,7 @@ import org.kiwix.kiwixmobile.zimManager.fileselectView.FileSelectListState
@Composable
fun LocalLibraryScreen(
state: LocalLibraryScreenState,
listState: LazyListState,
onRefresh: () -> Unit,
onDownloadButtonClick: () -> Unit,
fabButtonClick: () -> Unit,
@ -82,14 +84,18 @@ fun LocalLibraryScreen(
onMultiSelect: ((BookOnDisk) -> Unit)? = null,
navigationIcon: @Composable () -> Unit
) {
val (bottomNavHeight, lazyListState) = rememberScrollBehavior(state)
val (bottomNavHeight, lazyListState) = rememberScrollBehavior(state, listState)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
KiwixTheme {
Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
topBar = {
KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems, lazyListState)
KiwixAppBar(R.string.library, navigationIcon, state.actionMenuItems, scrollBehavior)
},
modifier = Modifier.systemBarsPadding()
floatingActionButton = { SelectFileButton(fabButtonClick) },
modifier = Modifier
.systemBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) { contentPadding ->
SwipeRefreshLayout(
isRefreshing = state.swipeRefreshItem.first,
@ -117,13 +123,6 @@ fun LocalLibraryScreen(
lazyListState
)
}
SelectFileButton(
fabButtonClick,
Modifier
.align(Alignment.BottomEnd)
.padding(end = SIXTEEN_DP, bottom = TEN_DP)
)
}
}
}
@ -131,11 +130,13 @@ fun LocalLibraryScreen(
@Composable
private fun rememberScrollBehavior(
state: LocalLibraryScreenState
state: LocalLibraryScreenState,
listState: LazyListState,
): Pair<MutableState<Dp>, LazyListState> {
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val bottomNavHeight = remember { mutableStateOf(bottomNavHeightInDp) }
val lazyListState = rememberLazyListScrollListener(
lazyListState = listState,
onScrollChanged = { direction ->
when (direction) {
ScrollDirection.SCROLL_UP -> {
@ -185,10 +186,10 @@ private fun BookItemList(
}
@Composable
private fun SelectFileButton(fabButtonClick: () -> Unit, modifier: Modifier) {
private fun SelectFileButton(fabButtonClick: () -> Unit) {
FloatingActionButton(
onClick = fabButtonClick,
modifier = modifier,
modifier = Modifier.padding(bottom = FAB_ICON_BOTTOM_MARGIN),
containerColor = Black,
shape = MaterialTheme.shapes.extraLarge
) {

View File

@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -69,6 +70,7 @@ import org.kiwix.kiwixmobile.ui.ZimFilesLanguageHeader
const val START_SERVER_BUTTON_TESTING_TAG = "startServerButtonTestingTag"
const val QR_IMAGE_TESTING_TAG = "qrImageTestingTag"
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming", "LongParameterList")
@Composable
fun ZimHostScreen(

View File

@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@ -57,6 +58,7 @@ const val SAVE_MENU_BUTTON_TESTING_TAG = "saveMenuButtonTestingTag"
const val SHARE_MENU_BUTTON_TESTING_TAG = "shareMenuButtonTestingTag"
const val DELETE_MENU_BUTTON_TESTING_TAG = "deleteMenuButtonTestingTag"
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming")
@Composable
fun AddNoteDialogScreen(

View File

@ -19,20 +19,20 @@
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.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -47,55 +47,58 @@ 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 androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.MineShaftGray350
import org.kiwix.kiwixmobile.core.ui.theme.White
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_APP_BAR_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle"
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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,
topAppBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
// 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(appBarHeight)
.background(color = Black),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(Modifier.padding(start = TWO_DP))
navigationIcon()
searchBar?.let {
// Display the search bar when provided
it()
} ?: run {
// Otherwise, show the title
AppBarTitle(titleId)
}
Spacer(Modifier.weight(1f))
ActionMenu(actionMenuItems)
TopAppBar(
title = { AppBarTitleSection(titleId, searchBar) },
navigationIcon = navigationIcon,
actions = { ActionMenu(actionMenuItems) },
scrollBehavior = topAppBarScrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Black,
scrolledContainerColor = Black
)
)
}
}
@Suppress("ComposableLambdaParameterNaming")
@Composable
private fun AppBarTitleSection(
@StringRes titleId: Int,
searchBar: (@Composable () -> Unit)? = null
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = SIXTEEN_DP),
contentAlignment = Alignment.CenterStart
) {
searchBar?.let {
it()
} ?: run {
AppBarTitle(titleId)
}
}
}
@ -113,9 +116,10 @@ private fun AppBarTitle(
text = stringResource(titleId),
color = appBarTitleColor,
style = MaterialTheme.typography.titleMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier
.padding(horizontal = SIXTEEN_DP)
.testTag(TOOLBAR_TITLE_TESTING_TAG)
.testTag(TOOLBAR_TITLE_TESTING_TAG),
)
}
@ -139,9 +143,9 @@ private fun ActionMenu(actionMenuItems: List<ActionMenuItem>) {
}
@Composable
private fun rememberToolbarVisibility(lazyListState: LazyListState?): Boolean {
fun rememberBottomNavigationVisibility(lazyListState: LazyListState?): Boolean {
var isToolbarVisible by remember { mutableStateOf(true) }
var lastScrollIndex by remember { mutableIntStateOf(0) }
var lastScrollIndex by remember { mutableIntStateOf(ZERO) }
val updatedLazyListState = rememberUpdatedState(lazyListState)
LaunchedEffect(updatedLazyListState) {

View File

@ -19,7 +19,6 @@
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
@ -34,10 +33,10 @@ const val ONE_THOUSAND = 1000
@Composable
fun rememberLazyListScrollListener(
lazyListState: LazyListState,
onScrollChanged: (ScrollDirection) -> Unit,
scrollThreshold: Int = 20
): LazyListState {
val lazyListState = rememberLazyListState()
val updatedOnScrollChanged = rememberUpdatedState(onScrollChanged)
var previousScrollPosition by remember { mutableIntStateOf(0) }

View File

@ -97,5 +97,5 @@ object ComposeDimens {
val BOOK_ICON_SIZE = 40.dp
// LocalLibraryFragment dimens
val FAB_ICON_BOTTOM_MARGIN = 66.dp
val FAB_ICON_BOTTOM_MARGIN = 50.dp
}