Floating toolbar (#6147)

* Use floating toolbar on homepage

* Fix deprecation issue

* Create HorizontalFloatingToolbar wrapper in our components.

* Fix Konsist test.

* Fix compilation issue after rebase.

* Fix lint issue. `floatingActionButton` must be the last parameter.

* Add Preview for the case empty space.

* Fix navigation bar overlapping buttons in empty space view.

* Increase content padding, and apply it to the space tab too.

* Update screenshots

---------

Co-authored-by: chelsea <git@cdhildit.ch>
Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Benoit Marty 2026-02-26 14:54:06 +01:00 committed by GitHub
commit e56c7e1227
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 395 additions and 165 deletions

View file

@ -33,7 +33,6 @@ data class HomeState(
val eventSink: (HomeEvent) -> Unit,
) {
val isBackHandlerEnabled = currentHomeNavigationBarItem != HomeNavigationBarItem.Chats || roomListState.spaceFiltersState is SpaceFiltersState.Selected
val displayActions = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats
val displayRoomListFilters = currentHomeNavigationBarItem == HomeNavigationBarItem.Chats && roomListState.displayFilters
val showNavigationBar = homeSpacesState.canCreateSpaces || homeSpacesState.spaceRooms.isNotEmpty()
}

View file

@ -45,7 +45,12 @@ open class HomeStateProvider : PreviewParameterProvider<HomeState> {
),
) + RoomListStateProvider().values.map {
aHomeState(roomListState = it)
}
} + aHomeState(
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
homeSpacesState = aHomeSpacesState(
spaceRooms = emptyList(),
),
)
}
internal fun aHomeState(

View file

@ -21,19 +21,22 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@ -58,15 +61,15 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbar
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarItem
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.NavigationBar
import io.element.android.libraries.designsystem.theme.components.NavigationBarIcon
import io.element.android.libraries.designsystem.theme.components.NavigationBarItem
import io.element.android.libraries.designsystem.theme.components.NavigationBarText
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@Composable
@ -185,12 +188,10 @@ private fun HomeScaffold(
onAccountSwitch = {
state.eventSink(HomeEvent.SwitchToAccount(it))
},
onCreateSpace = onCreateSpaceClick,
scrollBehavior = scrollBehavior,
displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect(
state = hazeState,
@ -198,7 +199,7 @@ private fun HomeScaffold(
)
)
},
bottomBar = {
floatingActionButton = {
if (state.showNavigationBar) {
val coroutineScope = rememberCoroutineScope()
HomeBottomBar(
@ -222,14 +223,29 @@ private fun HomeScaffold(
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
}
},
modifier = Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick(),
)
floatingActionButton = when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
{
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
}
HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) {
{
HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
}
} else {
// No FAB for spaces if we cannot create spaces
null
}
},
)
}
},
floatingActionButtonPosition = FabPosition.Center,
content = { padding ->
val contentPadding = PaddingValues(
bottom = 112.dp,
)
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
RoomListContentView(
@ -243,15 +259,7 @@ private fun HomeScaffold(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = ::onRoomClick,
onCreateRoomClick = onStartChatClick,
contentPadding = PaddingValues(
// FAB height is 56dp, bottom padding is 16dp, we add 8dp as extra margin -> 56+16+8 = 80,
// and include provided bottom padding
// Disable contentPadding due to navigation issue using the keyboard
// See https://issuetracker.google.com/issues/436432313
bottom = 80.dp,
// bottom = 80.dp + padding.calculateBottomPadding(),
// top = padding.calculateTopPadding()
),
contentPadding = contentPadding,
modifier = Modifier
.padding(
PaddingValues(
@ -274,6 +282,7 @@ private fun HomeScaffold(
.padding(padding)
.consumeWindowInsets(padding)
.hazeSource(state = hazeState),
contentPadding = contentPadding,
state = state.homeSpacesState,
lazyListState = spacesLazyListState,
onSpaceClick = { spaceId ->
@ -286,49 +295,48 @@ private fun HomeScaffold(
}
}
},
floatingActionButton = {
if (state.displayActions) {
FloatingActionButton(
onClick = onStartChatClick,
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(id = R.string.screen_roomlist_a11y_create_message),
)
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) },
)
}
@Composable
private fun HomeFloatingActionButton(
onClick: () -> Unit,
contentDescription: Int,
modifier: Modifier = Modifier,
) {
FloatingActionButton(onClick = onClick, modifier = modifier) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(id = contentDescription),
)
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem,
onItemClick: (HomeNavigationBarItem) -> Unit,
modifier: Modifier = Modifier,
floatingActionButton: (@Composable () -> Unit)?,
) {
NavigationBar(
containerColor = Color.Transparent,
HorizontalFloatingToolbar(
floatingActionButton = floatingActionButton,
modifier = modifier
.padding(bottom = ScreenOffset)
.zIndex(1f),
) {
HomeNavigationBarItem.entries.forEach { item ->
HomeNavigationBarItem.entries.forEachIndexed { index, item ->
if (index > 0) {
HorizontalFloatingToolbarSeparator()
}
val isSelected = currentHomeNavigationBarItem == item
NavigationBarItem(
selected = isSelected,
onClick = {
onItemClick(item)
},
icon = {
NavigationBarIcon(
imageVector = item.icon(isSelected),
)
},
label = {
NavigationBarText(
text = stringResource(item.labelRes),
)
}
HorizontalFloatingToolbarItem(
icon = item.icon(isSelected),
tooltipLabel = stringResource(item.labelRes),
isSelected = isSelected,
onClick = { onItemClick(item) },
)
}
}

View file

@ -87,9 +87,7 @@ fun HomeTopBar(
onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
onCreateSpace: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
canCreateSpaces: Boolean,
canReportBug: Boolean,
displayFilters: Boolean,
filtersState: RoomListFiltersState,
@ -134,17 +132,13 @@ fun HomeTopBar(
)
},
actions = {
when (selectedNavigationItem) {
HomeNavigationBarItem.Chats -> RoomListMenuItems(
if (selectedNavigationItem == HomeNavigationBarItem.Chats) {
RoomListMenuItems(
onToggleSearch = onToggleSearch,
onMenuActionClick = onMenuActionClick,
canReportBug = canReportBug,
spaceFiltersState = spaceFiltersState,
)
HomeNavigationBarItem.Spaces -> SpacesMenuItems(
canCreateSpaces = canCreateSpaces,
onCreateSpace = onCreateSpace
)
}
},
// We want a 16dp left padding for the navigationIcon :
@ -230,21 +224,6 @@ private fun RoomListMenuItems(
}
}
@Composable
private fun SpacesMenuItems(
canCreateSpaces: Boolean,
onCreateSpace: () -> Unit
) {
if (canCreateSpaces) {
IconButton(onClick = onCreateSpace) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = stringResource(CommonStrings.action_create_space)
)
}
}
}
@Composable
private fun SpaceFilterButton(
spaceFiltersState: SpaceFiltersState,
@ -365,8 +344,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@ -388,8 +365,6 @@ internal fun HomeTopBarSpaceFiltersSelectedPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@ -411,8 +386,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = false,
filtersState = aRoomListFiltersState(),
@ -434,8 +407,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@ -457,8 +428,6 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),

View file

@ -10,6 +10,7 @@ package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -48,6 +49,7 @@ import kotlinx.collections.immutable.toImmutableList
fun HomeSpacesView(
state: HomeSpacesState,
lazyListState: LazyListState,
contentPadding: PaddingValues,
onSpaceClick: (RoomId) -> Unit,
onCreateSpaceClick: () -> Unit,
onExploreClick: () -> Unit,
@ -55,7 +57,7 @@ fun HomeSpacesView(
) {
if (state.canCreateSpaces && state.spaceRooms.isEmpty()) {
EmptySpaceHomeView(
modifier = modifier,
modifier = modifier.padding(contentPadding),
onCreateSpaceClick = onCreateSpaceClick,
onExploreClick = onExploreClick,
canExploreSpaces = state.canExploreSpaces,
@ -63,7 +65,8 @@ fun HomeSpacesView(
} else {
LazyColumn(
modifier = modifier,
state = lazyListState
state = lazyListState,
contentPadding = contentPadding,
) {
val space = state.space
when (space) {
@ -115,6 +118,9 @@ fun HomeSpacesView(
}
}
/**
* Ref: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=1763-74215&t=9IGKMXHDfTGAqzQK-4
*/
@Composable
private fun EmptySpaceHomeView(
onCreateSpaceClick: () -> Unit,
@ -159,8 +165,7 @@ private fun EmptySpaceHomeView(
}
}
}
) {
}
)
}
@PreviewsDayNight
@ -174,5 +179,6 @@ internal fun HomeSpacesViewPreview(
onSpaceClick = {},
onCreateSpaceClick = {},
onExploreClick = {},
contentPadding = PaddingValues(bottom = 112.dp),
)
}