Implemented experimental SharedElement transition API

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:47 +00:00
1 parent aa9365b2fd
commit a6cbd89ff1
7 files changed
+127 -41

No files matched your search

@@ -1,21 +1,28 @@
@file:OptIn(ExperimentalSharedTransitionApi::class)
package com.rtbishop.look4sat.presentation
import androidx.compose.foundation.layout.padding
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navigation
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.info.infoDestination
import com.rtbishop.look4sat.presentation.map.mapDestination
@@ -25,69 +32,84 @@ import com.rtbishop.look4sat.presentation.satellites.satellitesDestination
import com.rtbishop.look4sat.presentation.settings.settingsDestination
sealed class Screen(var title: String, var icon: Int, var route: String) {
data object Main : Screen("Main", R.drawable.ic_sputnik, "main")
data object Radar : Screen("Radar", R.drawable.ic_sputnik, "radar")
data object Satellites : Screen("Satellites", R.drawable.ic_sputnik, "satellites")
data object PassesGraph : Screen("Passes", R.drawable.ic_passes, "passesGraph")
data object Passes : Screen("Passes", R.drawable.ic_passes, "passes")
data object Radar : Screen("Radar", R.drawable.ic_sputnik, "radar")
data object Map : Screen("Map", R.drawable.ic_map, "map")
data object Settings : Screen("Settings", R.drawable.ic_settings, "settings")
data object Info : Screen("Info", R.drawable.ic_info, "info")
}
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
@Composable
fun MainScreen() {
val outerNavController: NavHostController = rememberNavController()
val navController: NavHostController = rememberNavController()
val navigateBack: () -> Unit = { navController.navigateUp() }
val navigateToRadar = { catNum: Int, aosTime: Long ->
val routeWithParams = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}"
outerNavController.navigate(routeWithParams)
navController.navigate(routeWithParams)
}
SharedTransitionLayout {
CompositionLocalProvider(LocalSharedTransitionScope provides this) {
NavHost(navController = navController, startDestination = Screen.PassesGraph.route) {
satellitesDestination(navController, navigateBack)
passesGraph(navController, navigateBack, navigateToRadar)
radarDestination(navigateBack)
mapDestination(navController)
settingsDestination(navController)
infoDestination(navController)
}
}
NavHost(navController = outerNavController, startDestination = Screen.Main.route) {
mainDestination(navigateToRadar)
radarDestination { outerNavController.navigateUp() }
}
}
private fun NavGraphBuilder.mainDestination(navigateToRadar: (Int, Long) -> Unit) {
composable(Screen.Main.route) { NavBarScreen(navigateToRadar) }
}
@Composable
private fun NavBarScreen(navigateToRadar: (Int, Long) -> Unit) {
val innerNavController: NavHostController = rememberNavController()
val navigateToPasses = { innerNavController.navigate(Screen.Passes.route) }
Scaffold(bottomBar = { MainNavBar(innerNavController) }) { innerPadding ->
NavHost(
navController = innerNavController,
startDestination = Screen.Passes.route,
modifier = Modifier.padding(innerPadding)
private fun NavGraphBuilder.passesGraph(
navController: NavHostController,
navigateBack: () -> Unit,
navigateToRadar: (Int, Long) -> Unit
) {
satellitesDestination(navigateToPasses)
passesDestination(navigateToRadar)
mapDestination()
settingsDestination()
infoDestination()
}
navigation(route = Screen.PassesGraph.route, startDestination = Screen.Passes.route) {
passesDestination(navController, navigateToRadar)
radarDestination(navigateBack)
}
}
@SuppressLint("RestrictedApi", "StateFlowValueCalledInComposition")
@Composable
private fun MainNavBar(navController: NavController) {
fun MainNavBar(navController: NavController) {
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No SharedElementScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No SharedElementScope found")
val sharedTransitionState = sharedTransitionScope.rememberSharedContentState("bottomNav")
val items = listOf(Screen.Satellites, Screen.Passes, Screen.Map, Screen.Settings, Screen.Info)
val destinationRoute = navController.currentBackStackEntryAsState().value?.destination?.route
NavigationBar {
val stack = navController.currentBackStack.value.map { it.destination.route }
Log.d("NavBar", "NavStack: $stack")
with(sharedTransitionScope) {
NavigationBar(
modifier = Modifier.sharedElement(sharedTransitionState, animatedVisibilityScope)
) {
items.forEach { item ->
NavigationBarItem(
icon = { Icon(painterResource(item.icon), item.title) },
label = { Text(item.title) },
selected = destinationRoute?.contains(item.route) ?: false,
selected = item.route == navController.currentDestination?.route,
onClick = {
if (item.route == navController.currentDestination?.route) {
// selecting the same tab
} else {
navController.navigate(item.route) {
popUpTo(navController.graph.startDestinationId) { saveState = false }
popUpTo(Screen.Passes.route) { saveState = false }
launchSingleTop = true
restoreState = false
}
}
}
)
}
}
}
}
@@ -1,6 +1,7 @@
package com.rtbishop.look4sat.presentation.info
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,8 +11,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -22,8 +25,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainNavBar
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardButton
@@ -32,8 +38,16 @@ import com.rtbishop.look4sat.presentation.components.gotoUrl
private const val POLICY_URL = "https://sites.google.com/view/look4sat-privacy-policy/home"
private const val LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html"
fun NavGraphBuilder.infoDestination() {
composable(Screen.Info.route) { InfoScreen() }
fun NavGraphBuilder.infoDestination(navController: NavHostController) {
composable(Screen.Info.route) {
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
Scaffold(bottomBar = { MainNavBar(navController) }) { innerPadding ->
Box(Modifier.padding(innerPadding)) {
InfoScreen()
}
}
}
}
}
@Composable
@@ -15,8 +15,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
@@ -37,11 +39,14 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalObject
import com.rtbishop.look4sat.domain.predict.OrbitalPos
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainNavBar
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
import com.rtbishop.look4sat.presentation.components.NextPassRow
@@ -80,14 +85,20 @@ private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
}
private val labelRect = Rect()
fun NavGraphBuilder.mapDestination() {
fun NavGraphBuilder.mapDestination(navController: NavHostController) {
composable(Screen.Map.route) {
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
val mapView = rememberMapViewWithLifecycle()
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
Scaffold(bottomBar = { MainNavBar(navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
MapScreen(uiState, mapView)
}
}
}
}
}
@Composable
private fun MapScreen(uiState: State<MapState>, mapView: MapView) {
@@ -3,6 +3,7 @@ package com.rtbishop.look4sat.presentation.passes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,12 +19,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -36,12 +39,15 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.DeepSpaceObject
import com.rtbishop.look4sat.domain.predict.NearEarthObject
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainNavBar
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
@@ -56,16 +62,22 @@ import java.util.Locale
private val sdfDate = SimpleDateFormat("EEE dd MMM", Locale.ENGLISH)
private val sdfTime = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
fun NavGraphBuilder.passesDestination(navigateToRadar: (Int, Long) -> Unit) {
fun NavGraphBuilder.passesDestination(navController: NavHostController, navigateToRadar: (Int, Long) -> Unit) {
composable(Screen.Passes.route) {
val viewModel = viewModel(
modelClass = PassesViewModel::class.java,
factory = PassesViewModel.Factory
)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
Scaffold(bottomBar = { MainNavBar(navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
PassesScreen(uiState, navigateToRadar)
}
}
}
}
}
@Composable
private fun PassesScreen(uiState: PassesState, navigateToRadar: (Int, Long) -> Unit) {
@@ -31,6 +31,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -51,6 +52,7 @@ import androidx.navigation.navArgument
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
@@ -68,9 +70,11 @@ fun NavGraphBuilder.radarDestination(navigateBack: () -> Unit) {
composable(radarRoute, radarArgs) {
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
RadarScreen(uiState, navigateBack)
}
}
}
@Composable
private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
@@ -22,9 +22,11 @@ import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
@@ -41,25 +43,34 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainNavBar
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardIcon
import com.rtbishop.look4sat.presentation.components.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.components.InfoDialog
fun NavGraphBuilder.satellitesDestination(navigateToPasses: () -> Unit) {
fun NavGraphBuilder.satellitesDestination(navController: NavHostController, navigateToPasses: () -> Unit) {
composable(Screen.Satellites.route) {
val viewModel = viewModel(
modelClass = SatellitesViewModel::class.java,
factory = SatellitesViewModel.Factory
)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
Scaffold(bottomBar = { MainNavBar(navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
SatellitesScreen(uiState, navigateToPasses)
}
}
}
}
}
@Composable
private fun SatellitesScreen(uiState: SatellitesState, navigateToPasses: () -> Unit) {
@@ -5,6 +5,7 @@ import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -18,9 +19,11 @@ import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@@ -35,10 +38,13 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.LocalNavAnimatedVisibilityScope
import com.rtbishop.look4sat.presentation.MainNavBar
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardButton
@@ -47,16 +53,22 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun NavGraphBuilder.settingsDestination() {
fun NavGraphBuilder.settingsDestination(navController: NavHostController) {
composable(Screen.Settings.route) {
val viewModel = viewModel(
modelClass = SettingsViewModel::class.java,
factory = SettingsViewModel.Factory
)
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this@composable) {
Scaffold(bottomBar = { MainNavBar(navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
SettingsScreen(uiState)
}
}
}
}
}
@Composable
private fun SettingsScreen(uiState: SettingsState) {