fix: bottom navigation insets (#458)

* update main screen

* update post list

* update post detail

* update community detail

* update user detail

* make explore section similar to the home section

* fix formatting in saved items screen

closes #457
This commit is contained in:
Diego Beraldin 2024-01-17 19:16:46 +01:00
parent 2b03b93f8d
commit 1dae8bfad4
8 changed files with 265 additions and 196 deletions

View File

@ -8,8 +8,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -122,6 +124,7 @@ class ExploreScreen : Screen {
val defaultDownVoteColor = MaterialTheme.colorScheme.tertiary
val lazyListState = rememberLazyListState()
val detailOpener = remember { getDetailOpener() }
val connection = navigationCoordinator.getBottomBarScrollConnection()
val scope = rememberCoroutineScope()
LaunchedEffect(navigationCoordinator) {
@ -147,6 +150,11 @@ class ExploreScreen : Screen {
Scaffold(
modifier = Modifier.padding(Spacing.xxs),
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
topBar = {
ExploreTopBar(
scrollBehavior = scrollBehavior,
@ -275,13 +283,22 @@ class ExploreScreen : Screen {
{ model.reduce(ExploreMviModel.Intent.Refresh) },
)
Box(
modifier = Modifier.padding(Spacing.xxs).then(
if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
Modifier
}
).nestedScroll(keyboardScrollConnection).pullRefresh(pullRefreshState),
modifier = Modifier
.padding(Spacing.xxs)
.then(
if (connection != null && settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(connection)
} else {
Modifier
}
)
.then(
if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
Modifier
}
).nestedScroll(keyboardScrollConnection).pullRefresh(pullRefreshState),
) {
LazyColumn(
state = lazyListState,

View File

@ -1,6 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.material.BottomAppBar
import androidx.compose.material3.MaterialTheme
@ -32,7 +34,6 @@ import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.tab.CurrentTab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.DrawerEvent
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection
@ -70,6 +71,9 @@ internal object MainScreen : Screen {
val exitMessage = stringResource(MR.strings.message_confirm_exit)
val drawerCoordinator = remember { getDrawerCoordinator() }
val notificationCenter = remember { getNotificationCenter() }
val bottomNavigationInset = with(LocalDensity.current) {
WindowInsets.navigationBars.getBottom(this).toDp()
}
LaunchedEffect(model) {
model.effects.onEach {
@ -199,7 +203,7 @@ internal object MainScreen : Screen {
start = 0.dp,
top = 0.dp,
end = 0.dp,
bottom = Spacing.m,
bottom = bottomNavigationInset,
),
backgroundColor = MaterialTheme.colorScheme.background,
) {

View File

@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -211,6 +212,11 @@ class CommunityDetailScreen(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
topBar = {
val maxTopInset = Dimensions.topBarHeight.value.toInt()
var topInset by remember { mutableStateOf(maxTopInset) }

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -125,6 +126,11 @@ class MultiCommunityScreen(
val detailOpener = remember { getDetailOpener() }
Scaffold(
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
topBar = {
val sortType = uiState.sortType
val maxTopInset = Dimensions.topBarHeight.value.toInt()

View File

@ -17,8 +17,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -118,7 +120,6 @@ class PostDetailScreen(
private val highlightCommentId: Int? = null,
private val isMod: Boolean = false,
) : Screen {
override val key: ScreenKey
get() = super.key + postId.toString()
@ -188,8 +189,14 @@ class PostDetailScreen(
}
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.background)
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
topBar = {
TopAppBar(
title = {
@ -1102,11 +1109,12 @@ class PostDetailScreen(
Text(
text = buildString {
append(stringResource(MR.strings.post_detail_load_more_comments))
comment.comments?.takeIf { it > 0 }?.also { count ->
append(" (")
append(count)
append(")")
}
comment.comments?.takeIf { it > 0 }
?.also { count ->
append(" (")
append(count)
append(")")
}
},
style = MaterialTheme.typography.labelSmall,
)

View File

@ -9,8 +9,10 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -52,13 +54,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Dimensions
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenu
@ -124,6 +126,9 @@ class PostListScreen : Screen {
val keepScreenOn = rememberKeepScreenOn()
val detailOpener = remember { getDetailOpener() }
val connection = navigationCoordinator.getBottomBarScrollConnection()
val bottomNavigationInset = with(LocalDensity.current) {
WindowInsets.navigationBars.getBottom(this).toDp()
}
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.onDoubleTabSelection.onEach { section ->
@ -163,6 +168,11 @@ class PostListScreen : Screen {
}
Scaffold(
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
modifier = Modifier.padding(Spacing.xxs),
topBar = {
PostsTopBar(
@ -211,7 +221,9 @@ class PostListScreen : Screen {
),
) {
FloatingActionButtonMenu(
modifier = Modifier.padding(bottom = Dimensions.topBarHeight),
modifier = Modifier.padding(
bottom = Spacing.xxl + Spacing.s + bottomNavigationInset,
),
items = buildList {
if (uiState.zombieModeActive) {
this += FloatingActionButtonMenuItem(

View File

@ -149,19 +149,21 @@ class SavedItemsScreen : Screen {
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
},
)
})
FloatingActionButtonMenu(
items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
},
)
},
)
}
},
) { paddingValues ->

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@ -179,185 +180,198 @@ class UserDetailScreen(
}.launchIn(this)
}
Scaffold(modifier = Modifier.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs), topBar = {
val userName = uiState.user.name
val userHost = uiState.user.host
val maxTopInset = Dimensions.topBarHeight.value.toInt()
var topInset by remember { mutableStateOf(maxTopInset) }
snapshotFlow { topAppBarState.collapsedFraction }.onEach {
topInset = (maxTopInset * (1 - it)).toInt()
}.launchIn(scope)
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
contentWindowInsets = if (settings.edgeToEdge) {
WindowInsets(0, 0, 0, 0)
} else {
WindowInsets.navigationBars
},
topBar = {
val userName = uiState.user.name
val userHost = uiState.user.host
val maxTopInset = Dimensions.topBarHeight.value.toInt()
var topInset by remember { mutableStateOf(maxTopInset) }
snapshotFlow { topAppBarState.collapsedFraction }.onEach {
topInset = (maxTopInset * (1 - it)).toInt()
}.launchIn(scope)
TopAppBar(
windowInsets = if (settings.edgeToEdge) {
WindowInsets(0, topInset, 0, 0)
} else {
TopAppBarDefaults.windowInsets
},
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = buildString {
append(userName)
if (userHost.isNotEmpty()) {
append("@$userHost")
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
actions = {
// sort button
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
val sheet = SortBottomSheet(
sheetKey = key,
values = uiState.availableSortTypes.map { it.toInt() },
comments = false,
expandTop = true,
)
navigationCoordinator.showBottomSheet(sheet)
TopAppBar(
windowInsets = if (settings.edgeToEdge) {
WindowInsets(0, topInset, 0, 0)
} else {
TopAppBarDefaults.windowInsets
},
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = buildString {
append(userName)
if (userHost.isNotEmpty()) {
append("@$userHost")
}
},
),
imageVector = uiState.sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
// options menu
Box {
val options = listOf(
Option(
OptionId.Info, stringResource(MR.strings.user_detail_info)
),
Option(
OptionId.Block,
stringResource(MR.strings.community_detail_block)
),
Option(
OptionId.BlockInstance,
stringResource(MR.strings.community_detail_block_instance)
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
Image(
modifier = Modifier.onGloballyPositioned {
optionsOffset = it.positionInParent()
}.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
optionsExpanded = true
},
),
imageVector = Icons.Default.MoreVert,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
CustomDropDown(
expanded = optionsExpanded,
onDismiss = {
optionsExpanded = false
},
offset = DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
options.forEach { option ->
Text(
modifier = Modifier.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
).onClick(
onClick = rememberCallback {
optionsExpanded = false
when (option.id) {
OptionId.BlockInstance -> model.reduce(
UserDetailMviModel.Intent.BlockInstance
)
OptionId.Block -> model.reduce(
UserDetailMviModel.Intent.Block
)
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
UserInfoScreen(uiState.user.id),
)
}
else -> Unit
}
},
),
text = option.text,
)
}
}
}
},
navigationIcon = {
if (navigationCoordinator.canPop.value) {
},
actions = {
// sort button
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
navigationCoordinator.popScreen()
val sheet = SortBottomSheet(
sheetKey = key,
values = uiState.availableSortTypes.map { it.toInt() },
comments = false,
expandTop = true,
)
navigationCoordinator.showBottomSheet(sheet)
},
),
imageVector = Icons.Default.ArrowBack,
imageVector = uiState.sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
},
)
}, floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter = slideInVertically(
initialOffsetY = { it * 2 },
),
exit = slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
// options menu
Box {
val options = listOf(
Option(
OptionId.Info, stringResource(MR.strings.user_detail_info)
),
Option(
OptionId.Block,
stringResource(MR.strings.community_detail_block)
),
Option(
OptionId.BlockInstance,
stringResource(MR.strings.community_detail_block_instance)
),
)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
Image(
modifier = Modifier.onGloballyPositioned {
optionsOffset = it.positionInParent()
}.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
optionsExpanded = true
},
),
imageVector = Icons.Default.MoreVert,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
CustomDropDown(
expanded = optionsExpanded,
onDismiss = {
optionsExpanded = false
},
offset = DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
options.forEach { option ->
Text(
modifier = Modifier.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
).onClick(
onClick = rememberCallback {
optionsExpanded = false
when (option.id) {
OptionId.BlockInstance -> model.reduce(
UserDetailMviModel.Intent.BlockInstance
)
OptionId.Block -> model.reduce(
UserDetailMviModel.Intent.Block
)
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
UserInfoScreen(uiState.user.id),
)
}
else -> Unit
}
},
),
text = option.text,
)
}
}
}
},
navigationIcon = {
if (navigationCoordinator.canPop.value) {
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
},
)
},
floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter = slideInVertically(
initialOffsetY = { it * 2 },
),
exit = slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(
items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
},
)
if (uiState.isLogged && !isOnOtherInstance) {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.Chat,
text = stringResource(MR.strings.action_chat),
onSelected = rememberCallback {
val screen = InboxChatScreen(otherUserId = userId)
navigationCoordinator.pushScreen(screen)
},
)
}
},
)
if (uiState.isLogged && !isOnOtherInstance) {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.Chat,
text = stringResource(MR.strings.action_chat),
onSelected = rememberCallback {
val screen = InboxChatScreen(otherUserId = userId)
navigationCoordinator.pushScreen(screen)
},
)
}
})
}
}, snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
snackbarData = data,
)
}
}) { padding ->
}
},
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
snackbarData = data,
)
}
},
) { padding ->
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) {