From b14d27bb598a9d180d4f5f85caaf8559b8985258 Mon Sep 17 00:00:00 2001 From: todayama_r <13657682+Corvus400@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:02:32 +0900 Subject: [PATCH 1/2] :wrench: Fixed so that the language switching switch can be used normally even on small screens. --- .../sessions/TimetableItemDetailPresenter.kt | 8 + .../sessions/TimetableItemDetailScreen.kt | 23 +++ .../component/TimetableItemDetailHeadline.kt | 174 ++++++++++++------ 3 files changed, 151 insertions(+), 54 deletions(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailPresenter.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailPresenter.kt index 1e0326438..700f991ca 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailPresenter.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailPresenter.kt @@ -23,6 +23,7 @@ import io.github.droidkaigi.confsched.model.TimetableItemId import io.github.droidkaigi.confsched.model.TimetableSessionType.NORMAL import io.github.droidkaigi.confsched.model.localSessionsRepository import io.github.droidkaigi.confsched.sessions.TimetableItemDetailEvent.Bookmark +import io.github.droidkaigi.confsched.sessions.TimetableItemDetailEvent.EndTransitionAnimation import io.github.droidkaigi.confsched.sessions.TimetableItemDetailEvent.FavoriteListNavigated import io.github.droidkaigi.confsched.sessions.TimetableItemDetailEvent.SelectDescriptionLanguage import io.github.droidkaigi.confsched.sessions.TimetableItemDetailScreenUiState.Loaded @@ -34,6 +35,7 @@ sealed interface TimetableItemDetailEvent { data class Bookmark(val timetableItem: TimetableItem) : TimetableItemDetailEvent data class SelectDescriptionLanguage(val language: Lang) : TimetableItemDetailEvent data object FavoriteListNavigated : TimetableItemDetailEvent + data object EndTransitionAnimation : TimetableItemDetailEvent } @Composable @@ -52,6 +54,7 @@ fun timetableItemDetailPresenter( ) var selectedDescriptionLanguage by rememberRetained { mutableStateOf(null) } var shouldGoToFavoriteList by remember { mutableStateOf(false) } + var isEndTransitionAnimation by remember { mutableStateOf(false) } val bookmarkedSuccessfullyString = stringResource(SessionsRes.string.bookmarked_successfully) val viewBookmarkListString = stringResource(SessionsRes.string.view_bookmark_list) @@ -82,6 +85,10 @@ fun timetableItemDetailPresenter( is FavoriteListNavigated -> { shouldGoToFavoriteList = false } + + is EndTransitionAnimation -> { + isEndTransitionAnimation = true + } } } SafeLaunchedEffect(timetableItemStateWithBookmark?.first) { @@ -101,6 +108,7 @@ fun timetableItemDetailPresenter( timetableItemDetailSectionUiState = TimetableItemDetailSectionUiState(timetableItem), isBookmarked = bookmarked, isLangSelectable = timetableItem.sessionType == NORMAL, + isEndTransitionAnimation = isEndTransitionAnimation, currentLang = selectedDescriptionLanguage, roomThemeKey = timetableItem.room.getThemeKey(), timetableItemId = timetableItemId, diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt index c2d289c2e..03174b08f 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableItemDetailScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag @@ -53,6 +54,8 @@ import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailHead import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailSummaryCard import io.github.droidkaigi.confsched.sessions.component.TimetableItemDetailTopAppBar import io.github.droidkaigi.confsched.sessions.navigation.TimetableItemDetailDestination +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import org.jetbrains.compose.ui.tooling.preview.Preview const val timetableItemDetailScreenRouteItemIdParameterName = "timetableItemId" @@ -124,6 +127,9 @@ fun TimetableItemDetailScreen( onSelectedLanguage = { eventFlow.tryEmit(TimetableItemDetailEvent.SelectDescriptionLanguage(it)) }, + onEndTransitionAnimation = { + eventFlow.tryEmit(TimetableItemDetailEvent.EndTransitionAnimation) + }, snackbarHostState = snackbarHostState, ) } @@ -139,6 +145,7 @@ sealed interface TimetableItemDetailScreenUiState { val timetableItemDetailSectionUiState: TimetableItemDetailSectionUiState, val isBookmarked: Boolean, val isLangSelectable: Boolean, + val isEndTransitionAnimation: Boolean, val currentLang: Lang?, val roomThemeKey: String, val shouldGoToFavoriteList: Boolean, @@ -164,11 +171,24 @@ private fun TimetableItemDetailScreen( onCalendarRegistrationClick: (TimetableItem) -> Unit, onShareClick: (TimetableItem) -> Unit, onSelectedLanguage: (Lang) -> Unit, + onEndTransitionAnimation: () -> Unit, snackbarHostState: SnackbarHostState, ) { val sharedTransitionScope = LocalSharedTransitionScope.current val animatedScope = LocalAnimatedVisibilityScope.current + if ( + sharedTransitionScope != null && + animatedScope != null + ) { + LaunchedEffect(sharedTransitionScope, animatedScope) { + snapshotFlow { animatedScope.transition.isRunning } + .filter { it.not() } + .distinctUntilChanged() + .collect { onEndTransitionAnimation() } + } + } + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Scaffold( modifier = Modifier @@ -230,6 +250,7 @@ private fun TimetableItemDetailScreen( currentLang = uiState.currentLang, timetableItem = uiState.timetableItem, isLangSelectable = uiState.isLangSelectable, + isAnimationFinished = uiState.isEndTransitionAnimation, onLanguageSelect = onSelectedLanguage, ) } @@ -292,6 +313,7 @@ fun TimetableItemDetailScreenPreview() { ), isBookmarked = isBookMarked, isLangSelectable = true, + isEndTransitionAnimation = true, currentLang = Lang.JAPANESE, roomThemeKey = "iguana", timetableItemId = fakeSession.id, @@ -306,6 +328,7 @@ fun TimetableItemDetailScreenPreview() { onCalendarRegistrationClick = {}, onShareClick = {}, onSelectedLanguage = {}, + onEndTransitionAnimation = {}, snackbarHostState = SnackbarHostState(), ) } diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt index 4d777a48a..0b7d35645 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt @@ -1,6 +1,8 @@ package io.github.droidkaigi.confsched.sessions.component import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -25,9 +27,16 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.CollectionInfo import androidx.compose.ui.semantics.CollectionItemInfo @@ -60,10 +69,21 @@ fun TimetableItemDetailHeadline( currentLang: Lang?, timetableItem: TimetableItem, isLangSelectable: Boolean, + isAnimationFinished: Boolean, onLanguageSelect: (Lang) -> Unit, modifier: Modifier = Modifier, ) { val currentLang = currentLang ?: timetableItem.language.toLang() + var rowWidth by remember { mutableStateOf(0) } + var currentLangTagWidth by remember { mutableStateOf(0) } + val languageWidths = remember { mutableStateListOf() } + val remainingWidth by remember(rowWidth, currentLangTagWidth, languageWidths) { + derivedStateOf { rowWidth - (currentLangTagWidth + languageWidths.sum()) } + } + val hasSpaceForLanguageSwitcher by remember(remainingWidth) { + derivedStateOf { remainingWidth >= 390 } + } + Column( modifier = modifier // FIXME: Implement and use a theme color instead of fixed colors like RoomColors.primary and RoomColors.primaryDim @@ -71,25 +91,51 @@ fun TimetableItemDetailHeadline( .padding(horizontal = 8.dp) .fillMaxWidth(), ) { - Row(verticalAlignment = Alignment.CenterVertically) { - TimetableItemTag( - tagText = timetableItem.room.name.currentLangTitle, - tagColor = LocalRoomTheme.current.primaryColor, - ) - timetableItem.language.labels.forEach { label -> - Spacer(modifier = Modifier.padding(4.dp)) + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .onSizeChanged { size -> + rowWidth = size.width + } + ) { TimetableItemTag( - tagText = label, - tagColor = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .onSizeChanged { size -> + currentLangTagWidth = size.width + }, + tagText = timetableItem.room.name.currentLangTitle, + tagColor = LocalRoomTheme.current.primaryColor, ) - } - Spacer(modifier = Modifier.weight(1f)) - if (isLangSelectable) { + timetableItem.language.labels.forEachIndexed { index, label -> + Spacer(modifier = Modifier.padding(4.dp)) + TimetableItemTag( + modifier = Modifier + .onSizeChanged { size -> + if (index < languageWidths.size) { + languageWidths[index] = size.width + } else { + languageWidths.add(size.width) + } + }, + tagText = label, + tagColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.weight(1f)) LanguageSwitcher( currentLang = currentLang, onLanguageSelect = onLanguageSelect, + isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher ) } + LanguageSwitcher( + currentLang = currentLang, + onLanguageSelect = onLanguageSelect, + isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher.not() + ) } Spacer(modifier = Modifier.height(16.dp)) Text( @@ -133,6 +179,7 @@ fun TimetableItemDetailHeadline( private fun LanguageSwitcher( currentLang: Lang, onLanguageSelect: (Lang) -> Unit, + isVisible: Boolean, modifier: Modifier = Modifier, ) { val normalizedCurrentLang = if (currentLang == Lang.MIXED) { @@ -145,7 +192,11 @@ private fun LanguageSwitcher( stringResource(SessionsRes.string.english) to Lang.ENGLISH, ) val switcherContentDescription = stringResource(SessionsRes.string.select_language) - Row( + + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(), + exit = ExitTransition.None, modifier = modifier .selectableGroup() .semantics { @@ -155,52 +206,64 @@ private fun LanguageSwitcher( columnCount = 1, ) }, - verticalAlignment = Alignment.CenterVertically, ) { - val lastIndex = availableLangs.size - 1 - availableLangs.entries.forEachIndexed { index, (label, lang) -> - val isSelected = normalizedCurrentLang == lang - TextButton( - onClick = { onLanguageSelect(lang) }, - modifier = Modifier - .semantics { - role = Role.Tab - selected = isSelected - collectionItemInfo = CollectionItemInfo( - rowIndex = index, - rowSpan = 1, - columnIndex = 0, - columnSpan = 1, + Row( + modifier = modifier + .selectableGroup() + .semantics { + contentDescription = switcherContentDescription + collectionInfo = CollectionInfo( + rowCount = availableLangs.size, + columnCount = 1, + ) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + val lastIndex = availableLangs.size - 1 + availableLangs.entries.forEachIndexed { index, (label, lang) -> + val isSelected = normalizedCurrentLang == lang + TextButton( + onClick = { onLanguageSelect(lang) }, + modifier = Modifier + .semantics { + role = Role.Tab + selected = isSelected + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 0, + columnSpan = 1, + ) + }, + contentPadding = PaddingValues(12.dp), + ) { + val contentColor = if (isSelected) { + LocalRoomTheme.current.primaryColor + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + AnimatedVisibility(isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier + .padding(end = 4.dp) + .size(12.dp), + tint = contentColor, ) - }, - contentPadding = PaddingValues(12.dp), - ) { - val contentColor = if (isSelected) { - LocalRoomTheme.current.primaryColor - } else { - MaterialTheme.colorScheme.onSurfaceVariant + } + Text( + text = label, + color = contentColor, + style = MaterialTheme.typography.labelMedium, + ) } - AnimatedVisibility(isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier - .padding(end = 4.dp) - .size(12.dp), - tint = contentColor, + if (index < lastIndex) { + VerticalDivider( + modifier = Modifier.height(11.dp), + color = MaterialTheme.colorScheme.outlineVariant, ) } - Text( - text = label, - color = contentColor, - style = MaterialTheme.typography.labelMedium, - ) - } - if (index < lastIndex) { - VerticalDivider( - modifier = Modifier.height(11.dp), - color = MaterialTheme.colorScheme.outlineVariant, - ) } } } @@ -216,6 +279,7 @@ fun TimetableItemDetailHeadlinePreview() { timetableItem = TimetableItem.Session.fake(), currentLang = Lang.JAPANESE, isLangSelectable = true, + isAnimationFinished = true, onLanguageSelect = {}, ) } @@ -233,6 +297,7 @@ fun TimetableItemDetailHeadlineWithEnglishPreview() { timetableItem = TimetableItem.Session.fake(), currentLang = Lang.ENGLISH, isLangSelectable = true, + isAnimationFinished = true, onLanguageSelect = {}, ) } @@ -250,6 +315,7 @@ fun TimetableItemDetailHeadlineWithMixedPreview() { timetableItem = TimetableItem.Session.fake(), currentLang = Lang.MIXED, isLangSelectable = true, + isAnimationFinished = true, onLanguageSelect = {}, ) } From 000da83aa52f0851743c26b4e78517db9454d908 Mon Sep 17 00:00:00 2001 From: todayama_r <13657682+Corvus400@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:03:42 +0900 Subject: [PATCH 2/2] :wrench: ./gradlew detekt --auto-correct --- .../sessions/component/TimetableItemDetailHeadline.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt index 0b7d35645..13b326f58 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/component/TimetableItemDetailHeadline.kt @@ -99,7 +99,7 @@ fun TimetableItemDetailHeadline( .height(56.dp) .onSizeChanged { size -> rowWidth = size.width - } + }, ) { TimetableItemTag( modifier = Modifier @@ -128,13 +128,13 @@ fun TimetableItemDetailHeadline( LanguageSwitcher( currentLang = currentLang, onLanguageSelect = onLanguageSelect, - isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher + isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher, ) } LanguageSwitcher( currentLang = currentLang, onLanguageSelect = onLanguageSelect, - isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher.not() + isVisible = isAnimationFinished && isLangSelectable && hasSpaceForLanguageSwitcher.not(), ) } Spacer(modifier = Modifier.height(16.dp)) @@ -208,7 +208,7 @@ private fun LanguageSwitcher( }, ) { Row( - modifier = modifier + modifier = Modifier .selectableGroup() .semantics { contentDescription = switcherContentDescription