design(home) : allow scrolling to top when clicking on already selected tab

This commit is contained in:
ganfra 2025-10-23 17:00:40 +02:00
parent eb2b527236
commit a5b4f50334
3 changed files with 81 additions and 33 deletions

View file

@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
@ -25,7 +26,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@ -40,9 +40,9 @@ import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState import dev.chrisbanes.haze.rememberHazeState
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.components.HomeTopBar
import io.element.android.features.home.impl.components.RoomListContentView import io.element.android.features.home.impl.components.RoomListContentView
import io.element.android.features.home.impl.components.RoomListMenuAction import io.element.android.features.home.impl.components.RoomListMenuAction
import io.element.android.features.home.impl.components.HomeTopBar
import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.roomlist.RoomListContextMenu import io.element.android.features.home.impl.roomlist.RoomListContextMenu
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
@ -63,6 +63,7 @@ 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.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.launch
@Composable @Composable
fun HomeView( fun HomeView(
@ -155,6 +156,8 @@ private fun HomeScaffold(
} }
val hazeState = rememberHazeState() val hazeState = rememberHazeState()
val roomsLazyListState = rememberLazyListState()
val spacesLazyListState = rememberLazyListState()
Scaffold( Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@ -181,41 +184,39 @@ private fun HomeScaffold(
style = HazeMaterials.thick(), style = HazeMaterials.thick(),
) )
} else { } else {
Modifier Modifier.background(ElementTheme.colors.bgCanvasDefault)
.background(ElementTheme.colors.bgCanvasDefault)
} }
) )
}, },
bottomBar = { bottomBar = {
if (state.showNavigationBar) { if (state.showNavigationBar) {
NavigationBar( val coroutineScope = rememberCoroutineScope()
containerColor = Color.Transparent, HomeBottomBar(
modifier = Modifier currentHomeNavigationBarItem = state.currentHomeNavigationBarItem,
.hazeEffect( onItemClick = { item ->
state = hazeState, // scroll to top if selecting the same item
style = HazeMaterials.thick(), if (item == state.currentHomeNavigationBarItem) {
) val lazyListStateTarget = when (item) {
) { HomeNavigationBarItem.Chats -> roomsLazyListState
HomeNavigationBarItem.entries.forEach { item -> HomeNavigationBarItem.Spaces -> spacesLazyListState
val isSelected = state.currentHomeNavigationBarItem == item
NavigationBarItem(
selected = isSelected,
onClick = {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
},
icon = {
NavigationBarIcon(
imageVector = item.icon(isSelected),
)
},
label = {
NavigationBarText(
text = stringResource(item.labelRes),
)
} }
) coroutineScope.launch {
} if (lazyListStateTarget.firstVisibleItemIndex > 10) {
} lazyListStateTarget.scrollToItem(10)
}
// Also reset the scrollBehavior height offset as it's not triggered by programmatic scrolls
scrollBehavior.state.heightOffset = 0f
lazyListStateTarget.animateScrollToItem(0)
}
} else {
state.eventSink(HomeEvents.SelectHomeNavigationBarItem(item))
}
},
modifier = Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick(),
)
)
} }
}, },
content = { padding -> content = { padding ->
@ -224,6 +225,7 @@ private fun HomeScaffold(
RoomListContentView( RoomListContentView(
contentState = roomListState.contentState, contentState = roomListState.contentState,
filtersState = roomListState.filtersState, filtersState = roomListState.filtersState,
lazyListState = roomsLazyListState,
hideInvitesAvatars = roomListState.hideInvitesAvatars, hideInvitesAvatars = roomListState.hideInvitesAvatars,
eventSink = roomListState.eventSink, eventSink = roomListState.eventSink,
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
@ -261,6 +263,7 @@ private fun HomeScaffold(
.consumeWindowInsets(padding) .consumeWindowInsets(padding)
.hazeSource(state = hazeState), .hazeSource(state = hazeState),
state = state.homeSpacesState, state = state.homeSpacesState,
lazyListState = spacesLazyListState,
onSpaceClick = { spaceId -> onSpaceClick = { spaceId ->
onRoomClick(spaceId) onRoomClick(spaceId)
} }
@ -284,6 +287,38 @@ private fun HomeScaffold(
) )
} }
@Composable
private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem,
onItemClick: (HomeNavigationBarItem) -> Unit,
modifier: Modifier = Modifier,
) {
NavigationBar(
containerColor = Color.Transparent,
modifier = modifier
) {
HomeNavigationBarItem.entries.forEach { item ->
val isSelected = currentHomeNavigationBarItem == item
NavigationBarItem(
selected = isSelected,
onClick = {
onItemClick(item)
},
icon = {
NavigationBarIcon(
imageVector = item.icon(isSelected),
)
},
label = {
NavigationBarText(
text = stringResource(item.labelRes),
)
}
)
}
}
}
internal fun RoomListRoomSummary.contentType() = displayType.ordinal internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight @PreviewsDayNight

View file

@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxSize
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.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
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.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -60,6 +61,7 @@ import kotlinx.collections.immutable.ImmutableList
fun RoomListContentView( fun RoomListContentView(
contentState: RoomListContentState, contentState: RoomListContentState,
filtersState: RoomListFiltersState, filtersState: RoomListFiltersState,
lazyListState: LazyListState,
hideInvitesAvatars: Boolean, hideInvitesAvatars: Boolean,
eventSink: (RoomListEvents) -> Unit, eventSink: (RoomListEvents) -> Unit,
onSetUpRecoveryClick: () -> Unit, onSetUpRecoveryClick: () -> Unit,
@ -97,6 +99,7 @@ fun RoomListContentView(
onSetUpRecoveryClick = onSetUpRecoveryClick, onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
lazyListState = lazyListState,
contentPadding = contentPadding, contentPadding = contentPadding,
) )
} }
@ -176,6 +179,7 @@ private fun RoomsView(
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
contentPadding: PaddingValues, contentPadding: PaddingValues,
lazyListState: LazyListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) { if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
@ -192,6 +196,7 @@ private fun RoomsView(
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick, onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
onRoomClick = onRoomClick, onRoomClick = onRoomClick,
contentPadding = contentPadding, contentPadding = contentPadding,
lazyListState = lazyListState,
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) )
} }
@ -206,9 +211,9 @@ private fun RoomsViewList(
onConfirmRecoveryKeyClick: () -> Unit, onConfirmRecoveryKeyClick: () -> Unit,
onRoomClick: (RoomListRoomSummary) -> Unit, onRoomClick: (RoomListRoomSummary) -> Unit,
contentPadding: PaddingValues, contentPadding: PaddingValues,
lazyListState: LazyListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val lazyListState = rememberLazyListState()
val visibleRange by remember { val visibleRange by remember {
derivedStateOf { derivedStateOf {
val layoutInfo = lazyListState.layoutInfo val layoutInfo = lazyListState.layoutInfo
@ -343,6 +348,7 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
onConfirmRecoveryKeyClick = {}, onConfirmRecoveryKeyClick = {},
onRoomClick = {}, onRoomClick = {},
onCreateRoomClick = {}, onCreateRoomClick = {},
lazyListState = rememberLazyListState(),
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(0.dp),
) )
} }

View file

@ -8,6 +8,8 @@
package io.element.android.features.home.impl.spaces package io.element.android.features.home.impl.spaces
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -26,10 +28,14 @@ import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun HomeSpacesView( fun HomeSpacesView(
state: HomeSpacesState, state: HomeSpacesState,
lazyListState: LazyListState,
onSpaceClick: (RoomId) -> Unit, onSpaceClick: (RoomId) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LazyColumn(modifier) { LazyColumn(
modifier = modifier,
state = lazyListState
) {
val space = state.space val space = state.space
when (space) { when (space) {
CurrentSpace.Root -> { CurrentSpace.Root -> {
@ -77,6 +83,7 @@ internal fun HomeSpacesViewPreview(
) = ElementPreview { ) = ElementPreview {
HomeSpacesView( HomeSpacesView(
state = state, state = state,
lazyListState = rememberLazyListState(),
onSpaceClick = {}, onSpaceClick = {},
modifier = Modifier, modifier = Modifier,
) )