I'm piecewise reimplementing an existing iOS app. My initial go at it visually looks like this:
I'm looking for any feedback on more idiomatic ways to implement what I've done. There's a couple of questions in the code. It's a stock "Emtpy Compose App" with com.google.accompanist:accompanist-navigation-animation:0.29.1-alpha
added to the mix. My 3 basic files are (package and imports omitted for brevity):
MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Pager4Theme {
MainScreen()
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MainScreen() {
val navController = rememberAnimatedNavController()
Scaffold(bottomBar = {
PageTabBar(modifier = Modifier,
onSelect = { index -> navController.navigate("page_$index") })
}) { innerPadding ->
PagesNavHost(
navController = navController, modifier = Modifier.padding(innerPadding)
)
}
}
// This feels ugly, I had the same block of code for both transitions nearly, so I figured out how to
// extract it, but passing a "scope" feels odd
@OptIn(ExperimentalAnimationApi::class)
private fun slideDirection(scope: AnimatedContentScope<NavBackStackEntry>): AnimatedContentScope.SlideDirection {
val initialRoute = scope.initialState.destination.route ?: ""
val targetRoute = scope.targetState.destination.route ?: ""
return if (initialRoute < targetRoute) {
AnimatedContentScope.SlideDirection.Left
} else {
AnimatedContentScope.SlideDirection.Right
}
}
// There's an open question (to me at least) of why I'm using the whole Nav thing here
// I did it because that seemed the way to have separate "screens" and get side to side animation
// But they're not really a stack? Maybe that doesn't matter. But could I just activate one of four
// child composables and accomplish the side-to-side animation a different way?
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun PagesNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String = "page_1"
) {
AnimatedNavHost(modifier = modifier.fillMaxSize(),
navController = navController,
startDestination = startDestination,
enterTransition = { slideIntoContainer(slideDirection(this), animationSpec = tween(250)) },
exitTransition = {
slideOutOfContainer(
slideDirection(this),
animationSpec = tween(250)
)
}) {
composable("page_1") { PlansPage() }
composable("page_2") { MapsPage() }
composable("page_3") { StatusPage() }
composable("page_4") { SettingsPage() }
}
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
Pager4Theme {
MainScreen()
}
}
PageTabs.kt
// TODO: Figure out the Right/New Way(tm) to do the named colors
@Composable
fun PageTabButton(
text: String,
vector: Int,
index: Int,
selectedIndex: MutableState<Int>,
modifier: Modifier = Modifier
) {
val isSelected by remember { derivedStateOf { index == selectedIndex.value } }
val tintColor by animateColorAsState(
targetValue = when (isSelected) {
true -> Color.White; false -> colorResource(id = R.color.label_tabs_inactive)
}
)
Column(
modifier = modifier
.padding(6.dp)
.clickable { selectedIndex.value = index },
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(24.dp),
painter = painterResource(id = vector),
contentDescription = text,
colorFilter = ColorFilter.tint(tintColor)
)
Spacer(modifier = Modifier.height(4.dp))
val weight = when (isSelected) {
true -> FontWeight.Medium; false -> FontWeight.Normal
}
Text(
text = text, color = tintColor, fontSize = 11.sp, fontWeight = weight
)
}
}
// My hunch is that I'm working too hard with this selectedIndex thing
// I see that Modifier has selectable stuff, I should of course add that for accessability,
// but the real question is, can it actually model the state of which button is selected for me as well?
@Composable
fun PageTabBar(
modifier: Modifier = Modifier, onSelect: (Int) -> Unit = { _ -> }
) {
Row(
modifier = modifier
.background(color = colorResource(id = R.color.background_tabs))
.fillMaxWidth()
) {
var selectedIndex = remember { mutableStateOf(1) }
LaunchedEffect(selectedIndex.value) {
onSelect(selectedIndex.value)
}
val fillEach = Modifier.weight(1f, true) // Is this REALLY how I get even distribution?
// CAN I do the following with a loop?
PageTabButton(
text = "PLANS",
vector = R.drawable.tab_plans_mask,
index = 1,
selectedIndex = selectedIndex,
modifier = fillEach
)
PageTabButton(
text = "MAP",
vector = R.drawable.tab_map_mask,
index = 2,
selectedIndex = selectedIndex,
modifier = fillEach
)
PageTabButton(
text = "STATUS",
vector = R.drawable.tab_status_mask,
index = 3,
selectedIndex = selectedIndex,
modifier = fillEach
)
PageTabButton(
text = "SETTINGS",
vector = R.drawable.tab_settings_mask,
index = 4,
selectedIndex = selectedIndex,
modifier = fillEach
)
}
}
@Preview(showBackground = true)
@Composable
fun PageTabBarPreview() {
Pager4Theme {
PageTabBar()
}
}
MockPages.kt
@Composable
fun PlansPage() {
Column {
Text("Plan 1")
Text("Plan 2")
Text("Plan 3")
}
}
@Preview(showBackground = true)
@Composable
fun PlansPagePreview() {
Pager4Theme {
PlansPage()
}
}
@Composable
fun MapsPage() {
Text("Maps")
}
@Preview(showBackground = true)
@Composable
fun MapsPagePreview() {
Pager4Theme {
MapsPage()
}
}
@Composable
fun StatusPage() {
Column {
Text("Valve 1")
Text("Valve 2")
Text("Valve 3")
}
}
@Preview(showBackground = true)
@Composable
fun StatusPagePreview() {
Pager4Theme {
StatusPage()
}
}
@Composable
fun SettingsPage() {
Column {
Text("Setting 1")
Text("Setting 2")
Text("Setting 3")
}
}
@Preview(showBackground = true)
@Composable
fun SettingsPagePreview() {
Pager4Theme {
SettingsPage()
}
}