1
\$\begingroup\$

I'm piecewise reimplementing an existing iOS app. My initial go at it visually looks like this:

Demo of Animation

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()
    }
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.