From 8a0eda12ed4852be3c86e8bbcb920dc66baf2097 Mon Sep 17 00:00:00 2001 From: Harry Gao Date: Tue, 23 Jan 2024 12:43:45 +0800 Subject: [PATCH] feat(ui): large screen support --- app/build.gradle | 1 + .../infrastructure/android/MainActivity.kt | 6 ++ .../reader/ui/component/base/RYScaffold.kt | 3 +- .../me/ash/reader/ui/page/common/HomeEntry.kt | 17 ++-- .../me/ash/reader/ui/page/common/RouteName.kt | 1 - .../ash/reader/ui/page/home/HomeViewModel.kt | 15 ++++ .../ash/reader/ui/page/home/flow/FlowPage.kt | 34 +++++++- .../ash/reader/ui/page/home/flow/FlowRoute.kt | 77 +++++++++++++++++++ .../reader/ui/page/home/flow/FlowViewModel.kt | 1 + .../ui/page/home/reading/ReadingPage.kt | 6 +- .../ash/reader/ui/page/home/reading/TopBar.kt | 17 ++-- .../theme/palette/core/CompositionLocals.kt | 6 ++ 12 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/me/ash/reader/ui/page/home/flow/FlowRoute.kt diff --git a/app/build.gradle b/app/build.gradle index d16c98c07..3f0a35ba9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -177,6 +177,7 @@ dependencies { // https://developer.android.com/jetpack/androidx/releases/compose-material3 implementation "androidx.compose.material3:material3:$material3" + implementation "androidx.compose.material3:material3-window-size-class:$material3" // https://github.com/google/accompanist/releases implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist" diff --git a/app/src/main/java/me/ash/reader/infrastructure/android/MainActivity.kt b/app/src/main/java/me/ash/reader/infrastructure/android/MainActivity.kt index fb5703438..538635a5f 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/android/MainActivity.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/android/MainActivity.kt @@ -11,6 +11,8 @@ import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.core.app.NotificationManagerCompat @@ -28,6 +30,7 @@ import me.ash.reader.infrastructure.preference.SettingsProvider import me.ash.reader.ui.ext.languages import me.ash.reader.ui.page.common.HomeEntry import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel +import me.ash.reader.ui.theme.palette.core.LocalWidthWindowSizeClass import java.lang.reflect.Field import javax.inject.Inject @@ -43,6 +46,7 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var accountDao: AccountDao + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) @@ -83,8 +87,10 @@ class MainActivity : AppCompatActivity() { setContent { + val widthSizeClass = calculateWindowSizeClass(this).widthSizeClass CompositionLocalProvider( LocalImageLoader provides imageLoader, + LocalWidthWindowSizeClass provides widthSizeClass, ) { AccountSettingsProvider(accountDao) { SettingsProvider { diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt index 3997087ea..b188cd84e 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYScaffold.kt @@ -17,6 +17,7 @@ import me.ash.reader.ui.theme.palette.onDark @OptIn(ExperimentalMaterial3Api::class) @Composable fun RYScaffold( + modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.surface, topBarTonalElevation: Dp = 0.dp, containerTonalElevation: Dp = 0.dp, @@ -27,7 +28,7 @@ fun RYScaffold( content: @Composable () -> Unit = {}, ) { Scaffold( - modifier = Modifier + modifier = modifier .background( MaterialTheme.colorScheme.surfaceColorAtElevation( topBarTonalElevation, diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index a422f59bf..b8c9a5820 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -32,7 +32,7 @@ import me.ash.reader.ui.ext.isFirstLaunch import me.ash.reader.ui.page.home.HomeViewModel import me.ash.reader.ui.page.home.feeds.FeedsPage import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel -import me.ash.reader.ui.page.home.flow.FlowPage +import me.ash.reader.ui.page.home.flow.FlowRoute import me.ash.reader.ui.page.home.reading.ReadingPage import me.ash.reader.ui.page.settings.SettingsPage import me.ash.reader.ui.page.settings.accounts.AccountDetailsPage @@ -97,12 +97,12 @@ fun HomeEntry( ) // This is finally - navController.currentBackStackEntryFlow.collectLatest { + homeViewModel.homeUiState.collectLatest { Log.i("RLog", "currentBackStackEntry: ${navController.currentDestination?.route}") // Animation duration takes 310 ms delay(310L) isReadingPage = - navController.currentDestination?.route == "${RouteName.READING}/{articleId}" + navController.currentDestination?.route == RouteName.FLOW && it.isArticleOpen } } @@ -126,9 +126,7 @@ fun HomeEntry( navController.navigate(RouteName.FLOW) { launchSingleTop = true } - navController.navigate("${RouteName.READING}/${openArticleId}") { - launchSingleTop = true - } + homeViewModel.selectArticle(openArticleId) openArticleId = "" } } @@ -169,14 +167,11 @@ fun HomeEntry( ) } forwardAndBackwardComposable(route = RouteName.FLOW) { - FlowPage( + FlowRoute( navController = navController, - homeViewModel = homeViewModel, + homeViewModel = homeViewModel ) } - forwardAndBackwardComposable(route = "${RouteName.READING}/{articleId}") { - ReadingPage(navController = navController, homeViewModel = homeViewModel) - } // Settings forwardAndBackwardComposable(route = RouteName.SETTINGS) { diff --git a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt index 5897ac3e5..ffd63df9c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/RouteName.kt @@ -8,7 +8,6 @@ object RouteName { // Home const val FEEDS = "feeds" const val FLOW = "flow" - const val READING = "reading" // Settings const val SETTINGS = "settings" diff --git a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt index 55e536aff..ba512b88d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/HomeViewModel.kt @@ -101,6 +101,18 @@ class HomeViewModel @Inject constructor( _homeUiState.update { it.copy(searchContent = content) } fetchArticles() } + + fun selectArticle(articleId: String) { + _homeUiState.update { + it.copy(isArticleOpen = true, articleId = articleId) + } + } + + fun backToFeed() { + _homeUiState.update { + it.copy(isArticleOpen = false, articleId = null) + } + } } data class FilterState( @@ -112,4 +124,7 @@ data class FilterState( data class HomeUiState( val pagingData: Flow> = emptyFlow(), val searchContent: String = "", + + val isArticleOpen: Boolean = false, + val articleId: String? = null, ) diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt index d7e5e0c8d..e1e893c82 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowPage.kt @@ -1,12 +1,14 @@ package me.ash.reader.ui.page.home.flow import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -24,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext @@ -59,6 +62,8 @@ import me.ash.reader.ui.component.base.SwipeRefresh import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.home.HomeViewModel +import me.ash.reader.ui.page.home.reading.ReadingPage +import me.ash.reader.ui.theme.palette.core.LocalWidthWindowSizeClass @OptIn( com.google.accompanist.pager.ExperimentalPagerApi::class, @@ -69,6 +74,7 @@ fun FlowPage( navController: NavHostController, flowViewModel: FlowViewModel = hiltViewModel(), homeViewModel: HomeViewModel, + onArticleClick: (String) -> Unit, ) { val keyboardController = LocalSoftwareKeyboardController.current val topBarTonalElevation = LocalFlowTopBarTonalElevation.current @@ -182,7 +188,10 @@ fun FlowPage( onSearch = false } + val isExpandedScreen = LocalWidthWindowSizeClass.current == WindowWidthSizeClass.Expanded + val width = if (isExpandedScreen) Modifier.width(334.dp) else Modifier RYScaffold( + modifier = width, topBarTonalElevation = topBarTonalElevation.value.dp, containerTonalElevation = articleListTonalElevation.value.dp, navigationIcon = { @@ -328,9 +337,7 @@ fun FlowPage( isSwipeEnabled = { listState.isScrollInProgress }, onClick = { onSearch = false - navController.navigate("${RouteName.READING}/${it.article.id}") { - launchSingleTop = true - } + onArticleClick(it.article.id) }, onToggleStarred = onToggleStarred, onToggleRead = onToggleRead, @@ -368,3 +375,24 @@ fun FlowPage( } ) } + +@Composable +fun FlowWithArticleDetailsScreen( + navController: NavHostController, + homeViewModel: HomeViewModel, + flowViewModel: FlowViewModel, + onArticleClick: (String) -> Unit, +) { + val homeUiState = homeViewModel.homeUiState.collectAsStateValue() + Row { + FlowPage( + navController = navController, + onArticleClick = onArticleClick, + homeViewModel = homeViewModel, + flowViewModel = flowViewModel, + ) + if (homeUiState.isArticleOpen) { + ReadingPage(navController = navController, homeViewModel = homeViewModel) + } + } +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowRoute.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowRoute.kt new file mode 100644 index 000000000..7e3474e2f --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowRoute.kt @@ -0,0 +1,77 @@ +package me.ash.reader.ui.page.home.flow + +import androidx.activity.compose.BackHandler +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavHostController +import me.ash.reader.ui.ext.collectAsStateValue +import me.ash.reader.ui.page.home.HomeUiState +import me.ash.reader.ui.page.home.HomeViewModel +import me.ash.reader.ui.page.home.reading.ReadingPage +import me.ash.reader.ui.theme.palette.core.LocalWidthWindowSizeClass + +enum class FlowScreenType { + FlowWithArticleDetails, + Flow, + ArticleDetails, +} + +fun getFlowScreenType(isExpanded: Boolean, homeUiState: HomeUiState): FlowScreenType = when (isExpanded) { + true -> { + FlowScreenType.FlowWithArticleDetails + } + false -> { + if (homeUiState.isArticleOpen) { + FlowScreenType.ArticleDetails + } else { + FlowScreenType.Flow + } + } +} + +@Composable +fun FlowRoute( + navController: NavHostController, + homeViewModel: HomeViewModel, + flowViewModel: FlowViewModel = hiltViewModel(), +) { + val homeUiState = homeViewModel.homeUiState.collectAsStateValue() + val isExpandedScreen = LocalWidthWindowSizeClass.current == WindowWidthSizeClass.Expanded + val flowScreenType = getFlowScreenType(isExpandedScreen, homeUiState) + + val selectArticle: (String) -> Unit = { articleId -> + homeViewModel.selectArticle(articleId) + } + + when (flowScreenType) { + FlowScreenType.FlowWithArticleDetails -> { + FlowWithArticleDetailsScreen( + navController = navController, + homeViewModel = homeViewModel, + flowViewModel = flowViewModel, + onArticleClick = selectArticle, + ) + + BackHandler { + homeViewModel.backToFeed() + navController.popBackStack() + } + } + FlowScreenType.Flow -> { + FlowPage( + navController = navController, + homeViewModel = homeViewModel, + flowViewModel = flowViewModel, + onArticleClick = selectArticle, + ) + } + FlowScreenType.ArticleDetails -> { + ReadingPage(navController = navController, homeViewModel = homeViewModel) + + BackHandler { + homeViewModel.backToFeed() + } + } + } +} diff --git a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt index 408cded51..734254aed 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/flow/FlowViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.ash.reader.domain.model.article.ArticleFlowItem import me.ash.reader.domain.model.article.ArticleWithFeed diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt index 8c7cac3cf..af3d3609e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/ReadingPage.kt @@ -75,10 +75,12 @@ fun ReadingPage( } val pagingItems = homeUiState.pagingData.collectAsLazyPagingItems().itemSnapshotList + val articleId = homeViewModel.homeUiState.collectAsStateValue().articleId - LaunchedEffect(Unit) { + LaunchedEffect(articleId) { navController.currentBackStackEntryFlow.collect { - it.arguments?.getString("articleId")?.let { articleId -> + val getArticleId : String? = articleId ?: it.arguments?.getString("articleId") + getArticleId?.let { articleId -> if (readerState.articleId != articleId) { readingViewModel.initData(articleId) } diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt index 205ab715e..b0122ca3e 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/TopBar.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -27,6 +28,7 @@ import me.ash.reader.ui.component.base.FeedbackIconButton import me.ash.reader.ui.component.base.RYExtensibleVisibility import me.ash.reader.ui.ext.surfaceColorAtElevation import me.ash.reader.ui.page.common.RouteName +import me.ash.reader.ui.theme.palette.core.LocalWidthWindowSizeClass @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -41,6 +43,7 @@ fun TopBar( val context = LocalContext.current val tonalElevation = LocalReadingPageTonalElevation.current val sharedContent = LocalSharedContent.current + val isExpandedScreen = LocalWidthWindowSizeClass.current == WindowWidthSizeClass.Expanded Box( modifier = Modifier @@ -54,12 +57,14 @@ fun TopBar( modifier = Modifier, windowInsets = windowInsets, navigationIcon = { - FeedbackIconButton( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.close), - tint = MaterialTheme.colorScheme.onSurface - ) { - onClose() + if (!isExpandedScreen) { + FeedbackIconButton( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.close), + tint = MaterialTheme.colorScheme.onSurface + ) { + onClose() + } } }, actions = { diff --git a/app/src/main/java/me/ash/reader/ui/theme/palette/core/CompositionLocals.kt b/app/src/main/java/me/ash/reader/ui/theme/palette/core/CompositionLocals.kt index 6d4dd66d1..61e2ab64d 100644 --- a/app/src/main/java/me/ash/reader/ui/theme/palette/core/CompositionLocals.kt +++ b/app/src/main/java/me/ash/reader/ui/theme/palette/core/CompositionLocals.kt @@ -7,8 +7,10 @@ package me.ash.reader.ui.theme.palette.core +import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf import me.ash.reader.ui.theme.palette.colorspace.cielab.CieLab import me.ash.reader.ui.theme.palette.colorspace.ciexyz.CieXyz @@ -32,6 +34,10 @@ val LocalZcamViewingConditions = staticCompositionLocalOf { createZcamViewingConditions() } +val LocalWidthWindowSizeClass = compositionLocalOf { + WindowWidthSizeClass.Compact +} + @Composable fun ProvideZcamViewingConditions( whitePoint: CieXyz = Illuminant.D65,