From 5a9d900faf3af65c7df8c783c38ad5fb3c1dd524 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 18:42:48 -0400 Subject: [PATCH 01/40] Onboarding profile picture selection --- .../com/cornellappdev/score/MainActivity.kt | 27 ++- .../score/screen/EditProfileScreen.kt | 40 +---- .../score/screen/OnboardingProfileScreen.kt | 170 ++++++++++++++++++ .../score/screen/ProfilePictureOptions.kt | 87 +++++++++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 512793e..f1153cd 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -5,11 +5,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.OnboardingProfileScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics @@ -18,6 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" +private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -31,8 +33,8 @@ class MainActivity : AppCompatActivity() { val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) setContent { - var showOnboarding by rememberSaveable { - mutableStateOf(!onboardingCompleted) + var onboardingStep by rememberSaveable { + mutableIntStateOf(if (onboardingCompleted) 2 else 0) } val completeOnboarding = remember { @@ -40,18 +42,25 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putBoolean(KEY_ONBOARDING_COMPLETED, true) .apply() - showOnboarding = false + onboardingStep = 2 } } - if (showOnboarding) { - OnboardingScreen( - onSignUp = completeOnboarding, + when (onboardingStep) { + 0 -> OnboardingScreen( + onSignUp = { onboardingStep = 1 }, onLogIn = completeOnboarding, onLater = completeOnboarding ) - } else { - RootNavigation() + 1 -> OnboardingProfileScreen( + onNext = { selectedPicture -> + preferences.edit() + .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) + .apply() + completeOnboarding() + } + ) + else -> RootNavigation() } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt index f81b4b0..4c93ff0 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt @@ -4,23 +4,14 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -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 -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,11 +21,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.R @@ -89,31 +78,10 @@ fun EditProfileScreen( modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(Modifier.height(24.dp)) - Row( - modifier = Modifier.widthIn(max = 393.dp).fillMaxWidth() - .padding(horizontal = 24.dp).selectableGroup(), - horizontalArrangement = Arrangement.spacedBy(20.dp) - ) { - ProfilePicture.entries.forEach { picture -> - val selected = picture == selectedPicture - Box( - modifier = Modifier.weight(1f).aspectRatio(1f).clip(CircleShape) - .selectable(selected = selected, role = Role.RadioButton, - onClick = { onPictureSelected(picture) }), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(picture.drawable), - contentDescription = stringResource(picture.label), - modifier = Modifier.fillMaxSize() - ) - if (selected) { - Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF4DBB35), CircleShape)) - } - } - } - } + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = onPictureSelected + ) } } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt new file mode 100644 index 0000000..a9062a7 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -0,0 +1,170 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.Style + +@Composable +fun OnboardingProfileScreen( + onNext: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier, + displayName: String? = null +) { + var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.TENNIS) } + val name = displayName ?: stringResource(R.string.onboarding_default_name) + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.edit_profile), + style = Style.heading1, + color = Color(0xFF333333) + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.onboarding_profile_subtitle), + style = Style.bodyMedium, + color = Color(0xFF333333) + ) + + Spacer(Modifier.height(designDp(47f))) + Image( + painter = painterResource(selectedPicture.drawable), + contentDescription = stringResource( + R.string.selected_profile_picture, + stringResource(selectedPicture.label) + ), + modifier = Modifier.size(designDp(100f)) + ) + Spacer(Modifier.height(designDp(16f))) + Text( + text = name, + style = Style.bodyMedium, + color = Color.Black + ) + + Spacer(Modifier.height(designDp(60f))) + Text( + text = stringResource(R.string.onboarding_select_profile_picture), + style = Style.metricMedium, + color = Color.Black, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + Spacer(Modifier.height(designDp(24f))) + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = { selectedPicture = it }, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(Modifier.weight(1f)) + OnboardingProfileFooter( + onNext = { onNext(selectedPicture) }, + designDp = designDp + ) + } + } +} + +@Composable +private fun OnboardingProfileFooter( + onNext: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onNext, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = Color.White + ) + ) { + Text( + text = stringResource(R.string.next), + style = Style.heading3, + color = Color.White + ) + } + + Spacer(Modifier.height(designDp(16f))) + RowProgressDots() + } +} + +@Composable +private fun RowProgressDots() { + androidx.compose.foundation.layout.Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun OnboardingProfileScreenPreview() { + OnboardingProfileScreen(onNext = {}) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt new file mode 100644 index 0000000..ed5fcc2 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -0,0 +1,87 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.components.ScorePreview + +/** + * Shared profile picture choices used by profile editing and first-time onboarding. + */ +@Composable +fun ProfilePictureOptions( + selectedPicture: ProfilePicture, + onPictureSelected: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .widthIn(max = 393.dp) + .fillMaxWidth() + .padding(horizontal = 24.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + ProfilePicture.entries.forEach { picture -> + val selected = picture == selectedPicture + Box( + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onPictureSelected(picture) } + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(picture.drawable), + contentDescription = stringResource(picture.label), + modifier = Modifier.fillMaxSize() + ) + if (selected) { + Box( + Modifier + .fillMaxSize() + .background(Color.White.copy(alpha = 0.6f), CircleShape) + .border(3.dp, Color(0xFF55B63C), CircleShape) + ) + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 393) +@Composable +private fun ProfilePictureOptionsPreview() { + ScorePreview(backgroundColor = Color.White) { + ProfilePictureOptions( + selectedPicture = ProfilePicture.TENNIS, + onPictureSelected = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c264cc3..474c141 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,8 @@ Tennis Baseball Soccer + Claire Yu + You can change this later in the profile page. + Select profile picture: + Next \ No newline at end of file From 617b178025506ba3eb2c0824197f67c89b1ca121 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 18:42:48 -0400 Subject: [PATCH 02/40] Onboarding profile picture selection --- .../com/cornellappdev/score/MainActivity.kt | 27 ++- .../score/screen/EditProfileScreen.kt | 40 +---- .../score/screen/OnboardingProfileScreen.kt | 170 ++++++++++++++++++ .../score/screen/ProfilePictureOptions.kt | 87 +++++++++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 512793e..f1153cd 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -5,11 +5,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.OnboardingProfileScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics @@ -18,6 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" +private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -31,8 +33,8 @@ class MainActivity : AppCompatActivity() { val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) setContent { - var showOnboarding by rememberSaveable { - mutableStateOf(!onboardingCompleted) + var onboardingStep by rememberSaveable { + mutableIntStateOf(if (onboardingCompleted) 2 else 0) } val completeOnboarding = remember { @@ -40,18 +42,25 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putBoolean(KEY_ONBOARDING_COMPLETED, true) .apply() - showOnboarding = false + onboardingStep = 2 } } - if (showOnboarding) { - OnboardingScreen( - onSignUp = completeOnboarding, + when (onboardingStep) { + 0 -> OnboardingScreen( + onSignUp = { onboardingStep = 1 }, onLogIn = completeOnboarding, onLater = completeOnboarding ) - } else { - RootNavigation() + 1 -> OnboardingProfileScreen( + onNext = { selectedPicture -> + preferences.edit() + .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) + .apply() + completeOnboarding() + } + ) + else -> RootNavigation() } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt index f81b4b0..4c93ff0 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt @@ -4,23 +4,14 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -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 -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,11 +21,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.R @@ -89,31 +78,10 @@ fun EditProfileScreen( modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(Modifier.height(24.dp)) - Row( - modifier = Modifier.widthIn(max = 393.dp).fillMaxWidth() - .padding(horizontal = 24.dp).selectableGroup(), - horizontalArrangement = Arrangement.spacedBy(20.dp) - ) { - ProfilePicture.entries.forEach { picture -> - val selected = picture == selectedPicture - Box( - modifier = Modifier.weight(1f).aspectRatio(1f).clip(CircleShape) - .selectable(selected = selected, role = Role.RadioButton, - onClick = { onPictureSelected(picture) }), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(picture.drawable), - contentDescription = stringResource(picture.label), - modifier = Modifier.fillMaxSize() - ) - if (selected) { - Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF4DBB35), CircleShape)) - } - } - } - } + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = onPictureSelected + ) } } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt new file mode 100644 index 0000000..a9062a7 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -0,0 +1,170 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.Style + +@Composable +fun OnboardingProfileScreen( + onNext: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier, + displayName: String? = null +) { + var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.TENNIS) } + val name = displayName ?: stringResource(R.string.onboarding_default_name) + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.edit_profile), + style = Style.heading1, + color = Color(0xFF333333) + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.onboarding_profile_subtitle), + style = Style.bodyMedium, + color = Color(0xFF333333) + ) + + Spacer(Modifier.height(designDp(47f))) + Image( + painter = painterResource(selectedPicture.drawable), + contentDescription = stringResource( + R.string.selected_profile_picture, + stringResource(selectedPicture.label) + ), + modifier = Modifier.size(designDp(100f)) + ) + Spacer(Modifier.height(designDp(16f))) + Text( + text = name, + style = Style.bodyMedium, + color = Color.Black + ) + + Spacer(Modifier.height(designDp(60f))) + Text( + text = stringResource(R.string.onboarding_select_profile_picture), + style = Style.metricMedium, + color = Color.Black, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + Spacer(Modifier.height(designDp(24f))) + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = { selectedPicture = it }, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(Modifier.weight(1f)) + OnboardingProfileFooter( + onNext = { onNext(selectedPicture) }, + designDp = designDp + ) + } + } +} + +@Composable +private fun OnboardingProfileFooter( + onNext: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onNext, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = Color.White + ) + ) { + Text( + text = stringResource(R.string.next), + style = Style.heading3, + color = Color.White + ) + } + + Spacer(Modifier.height(designDp(16f))) + RowProgressDots() + } +} + +@Composable +private fun RowProgressDots() { + androidx.compose.foundation.layout.Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun OnboardingProfileScreenPreview() { + OnboardingProfileScreen(onNext = {}) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt new file mode 100644 index 0000000..ed5fcc2 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -0,0 +1,87 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.components.ScorePreview + +/** + * Shared profile picture choices used by profile editing and first-time onboarding. + */ +@Composable +fun ProfilePictureOptions( + selectedPicture: ProfilePicture, + onPictureSelected: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .widthIn(max = 393.dp) + .fillMaxWidth() + .padding(horizontal = 24.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + ProfilePicture.entries.forEach { picture -> + val selected = picture == selectedPicture + Box( + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onPictureSelected(picture) } + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(picture.drawable), + contentDescription = stringResource(picture.label), + modifier = Modifier.fillMaxSize() + ) + if (selected) { + Box( + Modifier + .fillMaxSize() + .background(Color.White.copy(alpha = 0.6f), CircleShape) + .border(3.dp, Color(0xFF55B63C), CircleShape) + ) + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 393) +@Composable +private fun ProfilePictureOptionsPreview() { + ScorePreview(backgroundColor = Color.White) { + ProfilePictureOptions( + selectedPicture = ProfilePicture.TENNIS, + onPictureSelected = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c264cc3..474c141 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,8 @@ Tennis Baseball Soccer + Claire Yu + You can change this later in the profile page. + Select profile picture: + Next \ No newline at end of file From a645f2d89dc724a60b6e2bd4bcba251c32516166 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 22:53:12 -0400 Subject: [PATCH 03/40] Make screen name more informative --- app/src/main/java/com/cornellappdev/score/MainActivity.kt | 4 ++-- .../cornellappdev/score/screen/OnboardingProfileScreen.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index f1153cd..aaf0da5 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation -import com.cornellappdev.score.screen.OnboardingProfileScreen +import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics @@ -52,7 +52,7 @@ class MainActivity : AppCompatActivity() { onLogIn = completeOnboarding, onLater = completeOnboarding ) - 1 -> OnboardingProfileScreen( + 1 -> OnboardingProfileSelectScreen( onNext = { selectedPicture -> preferences.edit() .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt index a9062a7..1dfa4a7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -35,7 +35,7 @@ import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.Style @Composable -fun OnboardingProfileScreen( +fun OnboardingProfileSelectScreen( onNext: (ProfilePicture) -> Unit, modifier: Modifier = Modifier, displayName: String? = null @@ -165,6 +165,6 @@ private fun RowProgressDots() { @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun OnboardingProfileScreenPreview() { - OnboardingProfileScreen(onNext = {}) +private fun OnboardingProfileSelectScreenPreview() { + OnboardingProfileSelectScreen(onNext = {}) } From 1f59b71ece6b55be1678fe6d94e5615e2eed7679 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 19:12:18 -0400 Subject: [PATCH 04/40] first draft onboarding profile screen # Conflicts: # app/src/main/java/com/cornellappdev/score/MainActivity.kt --- .../com/cornellappdev/score/MainActivity.kt | 26 +- .../score/screen/FavoriteSportsScreen.kt | 420 ++++++++++++++++++ .../score/screen/OnboardingProfileScreen.kt | 8 +- .../score/screen/ProfilePictureOptions.kt | 3 +- .../com/cornellappdev/score/theme/Color.kt | 1 + app/src/main/res/values/strings.xml | 9 + 6 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index aaf0da5..a53f694 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.FavoriteSport +import com.cornellappdev.score.screen.FavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase @@ -20,6 +22,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" +private const val KEY_ONBOARDING_FAVORITE_SPORTS = "onboarding_favorite_sports" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -31,10 +34,14 @@ class MainActivity : AppCompatActivity() { val preferences = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE) val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) + val savedFavoriteSportKeys = preferences + .getStringSet(KEY_ONBOARDING_FAVORITE_SPORTS, emptySet()) + .orEmpty() + .joinToString("|") setContent { var onboardingStep by rememberSaveable { - mutableIntStateOf(if (onboardingCompleted) 2 else 0) + mutableIntStateOf(if (onboardingCompleted) 3 else 0) } val completeOnboarding = remember { @@ -42,7 +49,7 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putBoolean(KEY_ONBOARDING_COMPLETED, true) .apply() - onboardingStep = 2 + onboardingStep = 3 } } @@ -57,7 +64,20 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) .apply() - completeOnboarding() + onboardingStep = 2 + } + ) + 2 -> FavoriteSportsScreen( + initiallySelectedKeys = savedFavoriteSportKeys, + onFinish = { selectedSports -> + preferences.edit() + .putStringSet( + KEY_ONBOARDING_FAVORITE_SPORTS, + selectedSports.map(FavoriteSport::storageKey).toSet() + ) + .putBoolean(KEY_ONBOARDING_COMPLETED, true) + .apply() + onboardingStep = 3 } ) else -> RootNavigation() diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt new file mode 100644 index 0000000..6696eee --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt @@ -0,0 +1,420 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.GreenWash +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White +import com.cornellappdev.score.theme.saturatedGreen +import com.cornellappdev.score.components.ScorePreview + +data class FavoriteSport( + val sport: Sport, + val gender: GenderDivision +) { + val storageKey: String get() = "${sport.name}:${gender.name}" + + companion object { + fun fromStorageKey(key: String): FavoriteSport? { + val parts = key.split(':', limit = 2) + if (parts.size != 2) return null + val sport = runCatching { Sport.valueOf(parts[0]) }.getOrNull() ?: return null + val gender = runCatching { GenderDivision.valueOf(parts[1]) }.getOrNull() + ?: return null + if (gender == GenderDivision.ALL) return null + return FavoriteSport(sport, gender) + } + } +} + +@Composable +fun FavoriteSportsScreen( + onFinish: (Set) -> Unit, + modifier: Modifier = Modifier, + initiallySelectedKeys: String = "" +) { + var selectedGender by rememberSaveable { mutableStateOf(GenderDivision.ALL) } + var selectedKeys by rememberSaveable { + mutableStateOf(initiallySelectedKeys) + } + val selectedSports = selectedKeys.split('|').filter(String::isNotBlank).toSet() + val allOptions = rememberFavoriteSportOptions() + val visibleOptions = when (selectedGender) { + GenderDivision.ALL -> allOptions + else -> allOptions.filter { it.gender == selectedGender } + } + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.select_favorite_sports), + style = Style.heading1, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.favorite_sports_subtitle), + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(32f))) + FavoriteSportsGenderFilter( + selectedGender = selectedGender, + onGenderSelected = { selectedGender = it }, + designDp = designDp + ) + } + + Spacer(Modifier.height(designDp(45f))) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues( + start = 24.dp, + end = 24.dp, + bottom = designDp(8f) + ), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items(visibleOptions, key = { it.storageKey }) { option -> + FavoriteSportCard( + option = option, + selected = option.storageKey in selectedSports, + onClick = { + selectedKeys = if (option.storageKey in selectedSports) { + selectedSports - option.storageKey + } else { + selectedSports + option.storageKey + }.joinToString("|") + }, + designDp = designDp + ) + } + } + + FavoriteSportsFooter( + selectedCount = selectedSports.size, + onFinish = { + onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) + }, + designDp = designDp + ) + } + } +} + +@Composable +private fun rememberFavoriteSportOptions(): List { + return remember { + Sport.entries + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } + } + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) + } +} + +@Composable +private fun FavoriteSportsGenderFilter( + selectedGender: GenderDivision, + onGenderSelected: (GenderDivision) -> Unit, + designDp: (Float) -> Dp +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .height(designDp(48f)) + .border( + BorderStroke(1.dp, GrayLight), + CircleShape + ) + .padding(designDp(5f)) + .selectableGroup(), + verticalAlignment = Alignment.CenterVertically + ) { + listOf( + GenderDivision.ALL to R.string.all, + GenderDivision.MALE to R.string.onboarding_mens, + GenderDivision.FEMALE to R.string.onboarding_womens + ).forEach { (gender, label) -> + val selected = gender == selectedGender + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .clip(CircleShape) + .background(if (selected) CrimsonPrimary else Color.Transparent) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onGenderSelected(gender) } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(label), + style = if (selected) Style.bodySemibold else Style.bodyMedium, + color = if (selected) White else GrayMedium + ) + } + } + } +} + +@Composable +private fun FavoriteSportCard( + option: FavoriteSport, + selected: Boolean, + onClick: () -> Unit, + designDp: (Float) -> Dp +) { + val cardShape = RoundedCornerShape(designDp(20f)) + Column( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selected, + role = Role.Checkbox, + onClick = onClick + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(cardShape) + .background(if (selected) GreenWash else GrayStroke) + .border( + width = if (selected) designDp(3f) else designDp(1f), + color = if (selected) saturatedGreen else GrayStroke, + shape = cardShape + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(option.sport.emptyIcon), + contentDescription = null, + modifier = Modifier.size(designDp(45f)), + colorFilter = ColorFilter.tint( + if (selected) saturatedGreen else GrayPrimary + ) + ) + } + + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = designDp(-8f), y = designDp(-8f)) + .size(designDp(32f)) + .background(CrimsonPrimary, CircleShape), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(R.drawable.ic_round_minus), + contentDescription = stringResource(R.string.remove_favorite_sport), + modifier = Modifier.size(designDp(24f)), + colorFilter = ColorFilter.tint(White) + ) + } + } + } + + Spacer(Modifier.height(designDp(8f))) + Text( + text = option.sport.displayName, + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(designDp(2f))) + FavoriteSportGenderTag(option.gender, designDp) + } +} + +@Composable +private fun FavoriteSportGenderTag( + gender: GenderDivision, + designDp: (Float) -> Dp +) { + val label = if (gender == GenderDivision.FEMALE) "W" else "M" + val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + + Row( + modifier = Modifier + .background(White, RoundedCornerShape(100)) + .padding(horizontal = designDp(2f)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + ) { + Box( + modifier = Modifier + .size(designDp(18f)) + .background(GrayStroke, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol, + color = GrayMedium, + fontSize = 10.sp, + lineHeight = 10.sp + ) + } + Text( + text = label, + style = Style.labelsNormal, + color = GrayPrimary + ) + } +} + +@Composable +private fun FavoriteSportsFooter( + selectedCount: Int, + onFinish: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onFinish, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + val buttonText = when (selectedCount) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedCount) + } + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(designDp(16f))) + Row( + horizontalArrangement = Arrangement.spacedBy(designDp(4f)), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + } + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsScreenPreview() { + ScorePreview { + FavoriteSportsScreen(onFinish = {}) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsSelectedScreenPreview() { + ScorePreview { + FavoriteSportsScreen( + onFinish = {}, + initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt index 1dfa4a7..3e33714 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable @@ -61,13 +63,13 @@ fun OnboardingProfileSelectScreen( Text( text = stringResource(R.string.edit_profile), style = Style.heading1, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(designDp(4f))) Text( text = stringResource(R.string.onboarding_profile_subtitle), style = Style.bodyMedium, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(designDp(47f))) @@ -159,7 +161,7 @@ private fun RowProgressDots() { verticalAlignment = Alignment.CenterVertically ) { Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + Box(Modifier.size(8.dp).background(GrayStroke, CircleShape)) } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt index ed5fcc2..c43ca31 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.theme.saturatedGreen /** * Shared profile picture choices used by profile editing and first-time onboarding. @@ -66,7 +67,7 @@ fun ProfilePictureOptions( Modifier .fillMaxSize() .background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF55B63C), CircleShape) + .border(3.dp, saturatedGreen, CircleShape) ) } } diff --git a/app/src/main/java/com/cornellappdev/score/theme/Color.kt b/app/src/main/java/com/cornellappdev/score/theme/Color.kt index a4f8fa5..799aaee 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/Color.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/Color.kt @@ -11,6 +11,7 @@ val GrayStroke = Color(0xFFEAEAEA) val GreenAccent = Color(0xFF247211) val RedAccent = Color(0xFFC41616) val GreenLight = Color(0xFF88D598) +val GreenWash = Color(0xFFF6FFF3) val BlueLight = Color(0XFF5AA8D5) val CrimsonPrimary = Color(0xFFA5210D) val PeachPrimary = Color(0xFFE1A69F) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 474c141..18fbbc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,4 +11,13 @@ You can change this later in the profile page. Select profile picture: Next + Select Favorite Sports + You can change your picks at any time! + All + Men’s + Women’s + Finish + Finish with %1$d sports + Finish with 1 sport + Remove favorite sport \ No newline at end of file From 6ceef4e97761cfd6fc194dc50d1bfb53534a21f5 Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:05:56 -0400 Subject: [PATCH 05/40] Make name more informative --- .../main/java/com/cornellappdev/score/MainActivity.kt | 4 ++-- ...ortsScreen.kt => OnboardingFavoriteSportsScreen.kt} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/src/main/java/com/cornellappdev/score/screen/{FavoriteSportsScreen.kt => OnboardingFavoriteSportsScreen.kt} (98%) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index a53f694..715c105 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation import com.cornellappdev.score.screen.FavoriteSport -import com.cornellappdev.score.screen.FavoriteSportsScreen +import com.cornellappdev.score.screen.OnboardingFavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase @@ -67,7 +67,7 @@ class MainActivity : AppCompatActivity() { onboardingStep = 2 } ) - 2 -> FavoriteSportsScreen( + 2 -> OnboardingFavoriteSportsScreen( initiallySelectedKeys = savedFavoriteSportKeys, onFinish = { selectedSports -> preferences.edit() diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt similarity index 98% rename from app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt rename to app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index 6696eee..d11b4d7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -81,7 +81,7 @@ data class FavoriteSport( } @Composable -fun FavoriteSportsScreen( +fun OnboardingFavoriteSportsScreen( onFinish: (Set) -> Unit, modifier: Modifier = Modifier, initiallySelectedKeys: String = "" @@ -402,17 +402,17 @@ private fun FavoriteSportsFooter( @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsScreenPreview() { +private fun OnboardingFavoriteSportsScreenPreview() { ScorePreview { - FavoriteSportsScreen(onFinish = {}) + OnboardingFavoriteSportsScreen(onFinish = {}) } } @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsSelectedScreenPreview() { +private fun OnboardingFavoriteSportsSelectedScreenPreview() { ScorePreview { - FavoriteSportsScreen( + OnboardingFavoriteSportsScreen( onFinish = {}, initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" ) From f69c41e6701cde0ece5d1eb24ca029120c04a569 Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:06:53 -0400 Subject: [PATCH 06/40] Rename file --- ...nboardingProfileScreen.kt => OnboardingProfileSelectScreen.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/cornellappdev/score/screen/{OnboardingProfileScreen.kt => OnboardingProfileSelectScreen.kt} (100%) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt similarity index 100% rename from app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt rename to app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt From 0689e24fc4301d22a5f28e3c2ce5c935c7b6bc5a Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 17:22:48 -0400 Subject: [PATCH 07/40] fix page dot indicator --- .../screen/OnboardingFavoriteSportsScreen.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index d11b4d7..0e16a6e 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -25,10 +25,13 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -358,6 +361,11 @@ private fun FavoriteSportsFooter( onFinish: () -> Unit, designDp: (Float) -> Dp ) { + var showActiveProgressDot by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showActiveProgressDot = true + } + Column( modifier = Modifier .fillMaxWidth() @@ -394,8 +402,13 @@ private fun FavoriteSportsFooter( horizontalArrangement = Arrangement.spacedBy(designDp(4f)), verticalAlignment = Alignment.CenterVertically ) { - Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + val activeColor by animateColorAsState( + targetValue = if (showActiveProgressDot) CrimsonPrimary else GrayStroke, + animationSpec = tween(durationMillis = 250), + label = "favoriteSportsActiveDot" + ) Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) } } } From fe3103a4327300e734c746a6dd3e4a1f45d375e7 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 19:12:18 -0400 Subject: [PATCH 08/40] first draft onboarding profile screen # Conflicts: # app/src/main/java/com/cornellappdev/score/MainActivity.kt --- .../com/cornellappdev/score/MainActivity.kt | 26 +- .../score/screen/FavoriteSportsScreen.kt | 420 ++++++++++++++++++ .../screen/OnboardingProfileSelectScreen.kt | 8 +- .../score/screen/ProfilePictureOptions.kt | 3 +- .../com/cornellappdev/score/theme/Color.kt | 1 + app/src/main/res/values/strings.xml | 9 + 6 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index aaf0da5..a53f694 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.FavoriteSport +import com.cornellappdev.score.screen.FavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase @@ -20,6 +22,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" +private const val KEY_ONBOARDING_FAVORITE_SPORTS = "onboarding_favorite_sports" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -31,10 +34,14 @@ class MainActivity : AppCompatActivity() { val preferences = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE) val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) + val savedFavoriteSportKeys = preferences + .getStringSet(KEY_ONBOARDING_FAVORITE_SPORTS, emptySet()) + .orEmpty() + .joinToString("|") setContent { var onboardingStep by rememberSaveable { - mutableIntStateOf(if (onboardingCompleted) 2 else 0) + mutableIntStateOf(if (onboardingCompleted) 3 else 0) } val completeOnboarding = remember { @@ -42,7 +49,7 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putBoolean(KEY_ONBOARDING_COMPLETED, true) .apply() - onboardingStep = 2 + onboardingStep = 3 } } @@ -57,7 +64,20 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) .apply() - completeOnboarding() + onboardingStep = 2 + } + ) + 2 -> FavoriteSportsScreen( + initiallySelectedKeys = savedFavoriteSportKeys, + onFinish = { selectedSports -> + preferences.edit() + .putStringSet( + KEY_ONBOARDING_FAVORITE_SPORTS, + selectedSports.map(FavoriteSport::storageKey).toSet() + ) + .putBoolean(KEY_ONBOARDING_COMPLETED, true) + .apply() + onboardingStep = 3 } ) else -> RootNavigation() diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt new file mode 100644 index 0000000..6696eee --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt @@ -0,0 +1,420 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.GreenWash +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White +import com.cornellappdev.score.theme.saturatedGreen +import com.cornellappdev.score.components.ScorePreview + +data class FavoriteSport( + val sport: Sport, + val gender: GenderDivision +) { + val storageKey: String get() = "${sport.name}:${gender.name}" + + companion object { + fun fromStorageKey(key: String): FavoriteSport? { + val parts = key.split(':', limit = 2) + if (parts.size != 2) return null + val sport = runCatching { Sport.valueOf(parts[0]) }.getOrNull() ?: return null + val gender = runCatching { GenderDivision.valueOf(parts[1]) }.getOrNull() + ?: return null + if (gender == GenderDivision.ALL) return null + return FavoriteSport(sport, gender) + } + } +} + +@Composable +fun FavoriteSportsScreen( + onFinish: (Set) -> Unit, + modifier: Modifier = Modifier, + initiallySelectedKeys: String = "" +) { + var selectedGender by rememberSaveable { mutableStateOf(GenderDivision.ALL) } + var selectedKeys by rememberSaveable { + mutableStateOf(initiallySelectedKeys) + } + val selectedSports = selectedKeys.split('|').filter(String::isNotBlank).toSet() + val allOptions = rememberFavoriteSportOptions() + val visibleOptions = when (selectedGender) { + GenderDivision.ALL -> allOptions + else -> allOptions.filter { it.gender == selectedGender } + } + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.select_favorite_sports), + style = Style.heading1, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.favorite_sports_subtitle), + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(32f))) + FavoriteSportsGenderFilter( + selectedGender = selectedGender, + onGenderSelected = { selectedGender = it }, + designDp = designDp + ) + } + + Spacer(Modifier.height(designDp(45f))) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues( + start = 24.dp, + end = 24.dp, + bottom = designDp(8f) + ), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items(visibleOptions, key = { it.storageKey }) { option -> + FavoriteSportCard( + option = option, + selected = option.storageKey in selectedSports, + onClick = { + selectedKeys = if (option.storageKey in selectedSports) { + selectedSports - option.storageKey + } else { + selectedSports + option.storageKey + }.joinToString("|") + }, + designDp = designDp + ) + } + } + + FavoriteSportsFooter( + selectedCount = selectedSports.size, + onFinish = { + onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) + }, + designDp = designDp + ) + } + } +} + +@Composable +private fun rememberFavoriteSportOptions(): List { + return remember { + Sport.entries + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } + } + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) + } +} + +@Composable +private fun FavoriteSportsGenderFilter( + selectedGender: GenderDivision, + onGenderSelected: (GenderDivision) -> Unit, + designDp: (Float) -> Dp +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .height(designDp(48f)) + .border( + BorderStroke(1.dp, GrayLight), + CircleShape + ) + .padding(designDp(5f)) + .selectableGroup(), + verticalAlignment = Alignment.CenterVertically + ) { + listOf( + GenderDivision.ALL to R.string.all, + GenderDivision.MALE to R.string.onboarding_mens, + GenderDivision.FEMALE to R.string.onboarding_womens + ).forEach { (gender, label) -> + val selected = gender == selectedGender + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .clip(CircleShape) + .background(if (selected) CrimsonPrimary else Color.Transparent) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onGenderSelected(gender) } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(label), + style = if (selected) Style.bodySemibold else Style.bodyMedium, + color = if (selected) White else GrayMedium + ) + } + } + } +} + +@Composable +private fun FavoriteSportCard( + option: FavoriteSport, + selected: Boolean, + onClick: () -> Unit, + designDp: (Float) -> Dp +) { + val cardShape = RoundedCornerShape(designDp(20f)) + Column( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selected, + role = Role.Checkbox, + onClick = onClick + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(cardShape) + .background(if (selected) GreenWash else GrayStroke) + .border( + width = if (selected) designDp(3f) else designDp(1f), + color = if (selected) saturatedGreen else GrayStroke, + shape = cardShape + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(option.sport.emptyIcon), + contentDescription = null, + modifier = Modifier.size(designDp(45f)), + colorFilter = ColorFilter.tint( + if (selected) saturatedGreen else GrayPrimary + ) + ) + } + + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = designDp(-8f), y = designDp(-8f)) + .size(designDp(32f)) + .background(CrimsonPrimary, CircleShape), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(R.drawable.ic_round_minus), + contentDescription = stringResource(R.string.remove_favorite_sport), + modifier = Modifier.size(designDp(24f)), + colorFilter = ColorFilter.tint(White) + ) + } + } + } + + Spacer(Modifier.height(designDp(8f))) + Text( + text = option.sport.displayName, + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(designDp(2f))) + FavoriteSportGenderTag(option.gender, designDp) + } +} + +@Composable +private fun FavoriteSportGenderTag( + gender: GenderDivision, + designDp: (Float) -> Dp +) { + val label = if (gender == GenderDivision.FEMALE) "W" else "M" + val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + + Row( + modifier = Modifier + .background(White, RoundedCornerShape(100)) + .padding(horizontal = designDp(2f)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + ) { + Box( + modifier = Modifier + .size(designDp(18f)) + .background(GrayStroke, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol, + color = GrayMedium, + fontSize = 10.sp, + lineHeight = 10.sp + ) + } + Text( + text = label, + style = Style.labelsNormal, + color = GrayPrimary + ) + } +} + +@Composable +private fun FavoriteSportsFooter( + selectedCount: Int, + onFinish: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onFinish, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + val buttonText = when (selectedCount) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedCount) + } + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(designDp(16f))) + Row( + horizontalArrangement = Arrangement.spacedBy(designDp(4f)), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + } + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsScreenPreview() { + ScorePreview { + FavoriteSportsScreen(onFinish = {}) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsSelectedScreenPreview() { + ScorePreview { + FavoriteSportsScreen( + onFinish = {}, + initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 1dfa4a7..3e33714 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -32,6 +32,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable @@ -61,13 +63,13 @@ fun OnboardingProfileSelectScreen( Text( text = stringResource(R.string.edit_profile), style = Style.heading1, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(designDp(4f))) Text( text = stringResource(R.string.onboarding_profile_subtitle), style = Style.bodyMedium, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(designDp(47f))) @@ -159,7 +161,7 @@ private fun RowProgressDots() { verticalAlignment = Alignment.CenterVertically ) { Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + Box(Modifier.size(8.dp).background(GrayStroke, CircleShape)) } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt index ed5fcc2..c43ca31 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.theme.saturatedGreen /** * Shared profile picture choices used by profile editing and first-time onboarding. @@ -66,7 +67,7 @@ fun ProfilePictureOptions( Modifier .fillMaxSize() .background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF55B63C), CircleShape) + .border(3.dp, saturatedGreen, CircleShape) ) } } diff --git a/app/src/main/java/com/cornellappdev/score/theme/Color.kt b/app/src/main/java/com/cornellappdev/score/theme/Color.kt index a4f8fa5..799aaee 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/Color.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/Color.kt @@ -11,6 +11,7 @@ val GrayStroke = Color(0xFFEAEAEA) val GreenAccent = Color(0xFF247211) val RedAccent = Color(0xFFC41616) val GreenLight = Color(0xFF88D598) +val GreenWash = Color(0xFFF6FFF3) val BlueLight = Color(0XFF5AA8D5) val CrimsonPrimary = Color(0xFFA5210D) val PeachPrimary = Color(0xFFE1A69F) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 474c141..18fbbc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,4 +11,13 @@ You can change this later in the profile page. Select profile picture: Next + Select Favorite Sports + You can change your picks at any time! + All + Men’s + Women’s + Finish + Finish with %1$d sports + Finish with 1 sport + Remove favorite sport \ No newline at end of file From bf012228e1ce4129d0d7f6805ec318add7bb2b77 Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:05:56 -0400 Subject: [PATCH 09/40] Make name more informative --- .../main/java/com/cornellappdev/score/MainActivity.kt | 4 ++-- ...ortsScreen.kt => OnboardingFavoriteSportsScreen.kt} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/src/main/java/com/cornellappdev/score/screen/{FavoriteSportsScreen.kt => OnboardingFavoriteSportsScreen.kt} (98%) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index a53f694..715c105 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation import com.cornellappdev.score.screen.FavoriteSport -import com.cornellappdev.score.screen.FavoriteSportsScreen +import com.cornellappdev.score.screen.OnboardingFavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase @@ -67,7 +67,7 @@ class MainActivity : AppCompatActivity() { onboardingStep = 2 } ) - 2 -> FavoriteSportsScreen( + 2 -> OnboardingFavoriteSportsScreen( initiallySelectedKeys = savedFavoriteSportKeys, onFinish = { selectedSports -> preferences.edit() diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt similarity index 98% rename from app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt rename to app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index 6696eee..d11b4d7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -81,7 +81,7 @@ data class FavoriteSport( } @Composable -fun FavoriteSportsScreen( +fun OnboardingFavoriteSportsScreen( onFinish: (Set) -> Unit, modifier: Modifier = Modifier, initiallySelectedKeys: String = "" @@ -402,17 +402,17 @@ private fun FavoriteSportsFooter( @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsScreenPreview() { +private fun OnboardingFavoriteSportsScreenPreview() { ScorePreview { - FavoriteSportsScreen(onFinish = {}) + OnboardingFavoriteSportsScreen(onFinish = {}) } } @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsSelectedScreenPreview() { +private fun OnboardingFavoriteSportsSelectedScreenPreview() { ScorePreview { - FavoriteSportsScreen( + OnboardingFavoriteSportsScreen( onFinish = {}, initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" ) From bb1e5b5faea05e057e79390781ea86ecb6fc1caa Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 17:22:48 -0400 Subject: [PATCH 10/40] fix page dot indicator --- .../screen/OnboardingFavoriteSportsScreen.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index d11b4d7..0e16a6e 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -25,10 +25,13 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -358,6 +361,11 @@ private fun FavoriteSportsFooter( onFinish: () -> Unit, designDp: (Float) -> Dp ) { + var showActiveProgressDot by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showActiveProgressDot = true + } + Column( modifier = Modifier .fillMaxWidth() @@ -394,8 +402,13 @@ private fun FavoriteSportsFooter( horizontalArrangement = Arrangement.spacedBy(designDp(4f)), verticalAlignment = Alignment.CenterVertically ) { - Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + val activeColor by animateColorAsState( + targetValue = if (showActiveProgressDot) CrimsonPrimary else GrayStroke, + animationSpec = tween(durationMillis = 250), + label = "favoriteSportsActiveDot" + ) Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) } } } From 49f5e026e26726f919f6b90f4c22b8623c7a8afd Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:56:40 -0400 Subject: [PATCH 11/40] Onboarding footer component --- .../score/screen/OnboardingFooter.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt new file mode 100644 index 0000000..4af4f06 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt @@ -0,0 +1,94 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.background +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White + +enum class OnboardingState{ + PROFILE_SELECT, SPORT_SELECT +} + +@Composable +fun OnboardingFooter( + buttonText: String, + onClick: () -> Unit, + activeStep: OnboardingState +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = 24.dp, + bottom = 60.dp + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(16.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(2) { index -> + val isActive = when (activeStep) { + OnboardingState.PROFILE_SELECT -> index == 0 + OnboardingState.SPORT_SELECT -> index == 1 + } + Box( + Modifier + .size(8.dp) + .background(if (isActive) CrimsonPrimary else GrayStroke, CircleShape) + ) + } + } + } +} + +@Preview +@Composable +private fun OnboardingFooterPreview() { + ScorePreview { + OnboardingFooter( + buttonText = "Next", + onClick = {}, + activeStep = OnboardingState.PROFILE_SELECT + ) + } +} From 15979167e2d276d00e821462c941d427bad34eac Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:56:57 -0400 Subject: [PATCH 12/40] Add sport select icons --- .../res/drawable/ic_light_gray_female_gender.xml | 16 ++++++++++++++++ .../res/drawable/ic_light_gray_male_gender.xml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/src/main/res/drawable/ic_light_gray_female_gender.xml create mode 100644 app/src/main/res/drawable/ic_light_gray_male_gender.xml diff --git a/app/src/main/res/drawable/ic_light_gray_female_gender.xml b/app/src/main/res/drawable/ic_light_gray_female_gender.xml new file mode 100644 index 0000000..81e066a --- /dev/null +++ b/app/src/main/res/drawable/ic_light_gray_female_gender.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_light_gray_male_gender.xml b/app/src/main/res/drawable/ic_light_gray_male_gender.xml new file mode 100644 index 0000000..3039fdb --- /dev/null +++ b/app/src/main/res/drawable/ic_light_gray_male_gender.xml @@ -0,0 +1,16 @@ + + + + + + + From 5dd429c45458d918c5c29fbeb53921e09566c154 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:58:03 -0400 Subject: [PATCH 13/40] Change SportSelectScreen to not use manual scaling --- .../screen/OnboardingFavoriteSportsScreen.kt | 186 ++++++------------ 1 file changed, 62 insertions(+), 124 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index 0e16a6e..0c8967a 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -25,13 +24,8 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -47,10 +41,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.cornellappdev.score.R +import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.model.GenderDivision import com.cornellappdev.score.model.Sport import com.cornellappdev.score.theme.CrimsonPrimary @@ -62,7 +55,6 @@ import com.cornellappdev.score.theme.GreenWash import com.cornellappdev.score.theme.Style import com.cornellappdev.score.theme.White import com.cornellappdev.score.theme.saturatedGreen -import com.cornellappdev.score.components.ScorePreview data class FavoriteSport( val sport: Sport, @@ -100,19 +92,16 @@ fun OnboardingFavoriteSportsScreen( else -> allOptions.filter { it.gender == selectedGender } } - BoxWithConstraints( + Box( modifier = modifier .fillMaxSize() .background(White) ) { - val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) - val designDp: (Float) -> Dp = { (it * designScale).dp } - Column(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxWidth() - .padding(top = designDp(79f)), + .padding(top = 79.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -121,22 +110,21 @@ fun OnboardingFavoriteSportsScreen( color = GrayPrimary, textAlign = TextAlign.Center ) - Spacer(Modifier.height(designDp(4f))) + Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.favorite_sports_subtitle), style = Style.bodyMedium, color = GrayPrimary, textAlign = TextAlign.Center ) - Spacer(Modifier.height(designDp(32f))) + Spacer(Modifier.height(32.dp)) FavoriteSportsGenderFilter( selectedGender = selectedGender, - onGenderSelected = { selectedGender = it }, - designDp = designDp + onGenderSelected = { selectedGender = it } ) } - Spacer(Modifier.height(designDp(45f))) + Spacer(Modifier.height(32.dp)) LazyVerticalGrid( columns = GridCells.Fixed(3), modifier = Modifier @@ -145,7 +133,8 @@ fun OnboardingFavoriteSportsScreen( contentPadding = PaddingValues( start = 24.dp, end = 24.dp, - bottom = designDp(8f) + top = 8.dp, + bottom = 8.dp ), horizontalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp) @@ -160,18 +149,21 @@ fun OnboardingFavoriteSportsScreen( } else { selectedSports + option.storageKey }.joinToString("|") - }, - designDp = designDp + } ) } } - FavoriteSportsFooter( - selectedCount = selectedSports.size, - onFinish = { + OnboardingFooter( + buttonText = when (selectedSports.size) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedSports.size) + }, + onClick = { onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) }, - designDp = designDp + activeStep = OnboardingState.SPORT_SELECT ) } } @@ -181,39 +173,39 @@ fun OnboardingFavoriteSportsScreen( private fun rememberFavoriteSportOptions(): List { return remember { Sport.entries - .flatMap { sport -> - when (sport.gender) { - GenderDivision.ALL -> listOf( - FavoriteSport(sport, GenderDivision.MALE), - FavoriteSport(sport, GenderDivision.FEMALE) - ) - GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) - GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } } - } - .sortedWith( - compareBy { it.sport.displayName } - .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } - ) + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) } } @Composable private fun FavoriteSportsGenderFilter( selectedGender: GenderDivision, - onGenderSelected: (GenderDivision) -> Unit, - designDp: (Float) -> Dp + onGenderSelected: (GenderDivision) -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) - .height(designDp(48f)) + .height(48.dp) .border( BorderStroke(1.dp, GrayLight), CircleShape ) - .padding(designDp(5f)) + .padding(5.dp) .selectableGroup(), verticalAlignment = Alignment.CenterVertically ) { @@ -250,10 +242,9 @@ private fun FavoriteSportsGenderFilter( private fun FavoriteSportCard( option: FavoriteSport, selected: Boolean, - onClick: () -> Unit, - designDp: (Float) -> Dp + onClick: () -> Unit ) { - val cardShape = RoundedCornerShape(designDp(20f)) + val cardShape = RoundedCornerShape(20.dp) Column( modifier = Modifier .fillMaxWidth() @@ -264,14 +255,16 @@ private fun FavoriteSportCard( ), horizontalAlignment = Alignment.CenterHorizontally ) { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box(modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f)) { Box( modifier = Modifier .fillMaxSize() .clip(cardShape) .background(if (selected) GreenWash else GrayStroke) .border( - width = if (selected) designDp(3f) else designDp(1f), + width = if (selected) 3.dp else 1.dp, color = if (selected) saturatedGreen else GrayStroke, shape = cardShape ), @@ -280,7 +273,7 @@ private fun FavoriteSportCard( Image( painter = painterResource(option.sport.emptyIcon), contentDescription = null, - modifier = Modifier.size(designDp(45f)), + modifier = Modifier.size(45.dp), colorFilter = ColorFilter.tint( if (selected) saturatedGreen else GrayPrimary ) @@ -291,22 +284,22 @@ private fun FavoriteSportCard( Box( modifier = Modifier .align(Alignment.TopStart) - .offset(x = designDp(-8f), y = designDp(-8f)) - .size(designDp(32f)) + .offset(x = (-8).dp, y = (-8).dp) + .size(32.dp) .background(CrimsonPrimary, CircleShape), contentAlignment = Alignment.Center ) { Image( painter = painterResource(R.drawable.ic_round_minus), contentDescription = stringResource(R.string.remove_favorite_sport), - modifier = Modifier.size(designDp(24f)), + modifier = Modifier.size(24.dp), colorFilter = ColorFilter.tint(White) ) } } } - Spacer(Modifier.height(designDp(8f))) + Spacer(Modifier.height(8.dp)) Text( text = option.sport.displayName, style = Style.bodyMedium, @@ -314,37 +307,39 @@ private fun FavoriteSportCard( textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) - Spacer(Modifier.height(designDp(2f))) - FavoriteSportGenderTag(option.gender, designDp) + Spacer(Modifier.height(2.dp)) + FavoriteSportGenderTag(option.gender) } } @Composable private fun FavoriteSportGenderTag( gender: GenderDivision, - designDp: (Float) -> Dp ) { val label = if (gender == GenderDivision.FEMALE) "W" else "M" - val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + val genderIcon = if (gender == GenderDivision.FEMALE) { + R.drawable.ic_light_gray_female_gender + } else { + R.drawable.ic_light_gray_male_gender + } Row( modifier = Modifier .background(White, RoundedCornerShape(100)) - .padding(horizontal = designDp(2f)), + .padding(horizontal = 2.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + horizontalArrangement = Arrangement.spacedBy(2.dp) ) { Box( modifier = Modifier - .size(designDp(18f)) + .size(18.dp) .background(GrayStroke, CircleShape), contentAlignment = Alignment.Center ) { - Text( - text = symbol, - color = GrayMedium, - fontSize = 10.sp, - lineHeight = 10.sp + Image( + painter = painterResource(genderIcon), + contentDescription = null, + modifier = Modifier.size(12.dp) ) } Text( @@ -355,65 +350,8 @@ private fun FavoriteSportGenderTag( } } -@Composable -private fun FavoriteSportsFooter( - selectedCount: Int, - onFinish: () -> Unit, - designDp: (Float) -> Dp -) { - var showActiveProgressDot by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - showActiveProgressDot = true - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(White) - .padding( - start = 24.dp, - end = 24.dp, - top = designDp(24f), - bottom = designDp(60f) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - onClick = onFinish, - modifier = Modifier - .fillMaxWidth() - .height(designDp(48f)), - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = CrimsonPrimary, - contentColor = White - ) - ) { - val buttonText = when (selectedCount) { - 0 -> stringResource(R.string.finish) - 1 -> stringResource(R.string.finish_with_one_sport) - else -> stringResource(R.string.finish_with_sports, selectedCount) - } - Text(buttonText, style = Style.heading3, color = White) - } - - Spacer(Modifier.height(designDp(16f))) - Row( - horizontalArrangement = Arrangement.spacedBy(designDp(4f)), - verticalAlignment = Alignment.CenterVertically - ) { - val activeColor by animateColorAsState( - targetValue = if (showActiveProgressDot) CrimsonPrimary else GrayStroke, - animationSpec = tween(durationMillis = 250), - label = "favoriteSportsActiveDot" - ) - Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) - Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) - } - } -} -@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Preview @Composable private fun OnboardingFavoriteSportsScreenPreview() { ScorePreview { @@ -421,7 +359,7 @@ private fun OnboardingFavoriteSportsScreenPreview() { } } -@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Preview @Composable private fun OnboardingFavoriteSportsSelectedScreenPreview() { ScorePreview { From 0679464c263f5c61b1f1ad95430a628b0b8b16b4 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:58:29 -0400 Subject: [PATCH 14/40] Use new onboarding footer component --- .../screen/OnboardingProfileSelectScreen.kt | 64 ++----------------- 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 3e33714..555d176 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -2,8 +2,6 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -12,9 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -31,9 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R -import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayPrimary -import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable @@ -107,64 +100,15 @@ fun OnboardingProfileSelectScreen( } Spacer(Modifier.weight(1f)) - OnboardingProfileFooter( - onNext = { onNext(selectedPicture) }, - designDp = designDp + OnboardingFooter( + buttonText = stringResource(R.string.next), + onClick = { onNext(selectedPicture) }, + activeStep = OnboardingState.PROFILE_SELECT ) } } } -@Composable -private fun OnboardingProfileFooter( - onNext: () -> Unit, - designDp: (Float) -> Dp -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 24.dp, - end = 24.dp, - top = designDp(24f), - bottom = designDp(60f) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - onClick = onNext, - modifier = Modifier - .fillMaxWidth() - .height(designDp(48f)), - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = CrimsonPrimary, - contentColor = Color.White - ) - ) { - Text( - text = stringResource(R.string.next), - style = Style.heading3, - color = Color.White - ) - } - - Spacer(Modifier.height(designDp(16f))) - RowProgressDots() - } -} - -@Composable -private fun RowProgressDots() { - androidx.compose.foundation.layout.Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(8.dp).background(GrayStroke, CircleShape)) - } -} - @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable private fun OnboardingProfileSelectScreenPreview() { From 9dcfd30dfd4844b53d38b4423f0c8e4b045fc897 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 14 Oct 2026 17:40:25 -0400 Subject: [PATCH 15/40] first draft of onboarding animation --- .../com/cornellappdev/score/MainActivity.kt | 8 +- .../score/components/OnboardingButtons.kt | 196 ++++++ .../OnboardingAnimationPreviewScreen.kt | 141 ++++ .../score/screen/OnboardingScreen.kt.kt | 645 ++++++++++++++++++ 4 files changed, 989 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 830d3aa..a9b44ff 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.OnboardingAnimationPreviewScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.analytics @@ -18,7 +19,12 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - RootNavigation() + //RootNavigation() + OnboardingAnimationPreviewScreen( + onSignUp = {}, + onLogIn = {}, + onLater = {} + ) } firebaseAnalytics = Firebase.analytics } diff --git a/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt new file mode 100644 index 0000000..9619b37 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt @@ -0,0 +1,196 @@ +package com.cornellappdev.score.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.Style.heading3 +import com.cornellappdev.score.theme.Style.labelsNormal +import com.cornellappdev.score.theme.White +import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds + +@Composable +fun OnboardingButtons( + onSignUp: () -> Unit, + onLogIn: () -> Unit, + onLater: () -> Unit, + modifier: Modifier = Modifier +) { + var showSignUp by remember { mutableStateOf(false) } + var showLogIn by remember { mutableStateOf(false) } + var showLater by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + showSignUp = true + delay(120.milliseconds) + showLogIn = true + delay(120.milliseconds) + showLater = true + } + + OnboardingButtonsContent( + onSignUp = onSignUp, + onLogIn = onLogIn, + onLater = onLater, + showSignUp = showSignUp, + showLogIn = showLogIn, + showLater = showLater, + animateEntrance = true, + modifier = modifier + ) +} + +@Composable +private fun OnboardingButtonsContent( + onSignUp: () -> Unit, + onLogIn: () -> Unit, + onLater: () -> Unit, + showSignUp: Boolean, + showLogIn: Boolean, + showLater: Boolean, + animateEntrance: Boolean, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OnboardingButtonSlot( + modifier = Modifier.fillMaxWidth().height(50.dp), + visible = showSignUp, + animateEntrance = animateEntrance + ) { + Button( + onClick = onSignUp, + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(100), + colors = ButtonDefaults.buttonColors( + containerColor = Color.White, + contentColor = CrimsonPrimary + ) + ) { + Text( + text = "Sign Up", + style = heading3, + color = CrimsonPrimary + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + OnboardingButtonSlot( + modifier = Modifier.fillMaxWidth().height(48.dp), + visible = showLogIn, + animateEntrance = animateEntrance + ) { + OutlinedButton( + onClick = onLogIn, + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(100), + border = BorderStroke( + width = 1.dp, + color = Color.White.copy(alpha = 0.9f) + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Color.White + ) + ) { + Text( + text = "Log In", + style = heading3, + color = White + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + OnboardingButtonSlot( + modifier = Modifier.fillMaxWidth().height(40.dp), + visible = showLater, + animateEntrance = animateEntrance + ) { + TextButton( + onClick = onLater, + colors = ButtonDefaults.textButtonColors(contentColor = Color.White) + ) { + Text( + text = "Sign Up Later", + style = labelsNormal + ) + } + } + } +} + +@Composable +private fun OnboardingButtonSlot( + visible: Boolean, + animateEntrance: Boolean, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + if (animateEntrance) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(tween(350)) + slideInVertically( + initialOffsetY = { it / 3 }, + animationSpec = tween(400) + ) + ) { + content() + } + } else if (visible) { + content() + } + } +} + +@Preview(showBackground = true, widthDp = 393) +@Composable +private fun OnboardingButtonsPreview() = ScorePreview( + padding = 24.dp, + backgroundColor = CrimsonPrimary +) { + OnboardingButtonsContent( + onSignUp = {}, + onLogIn = {}, + onLater = {}, + showSignUp = true, + showLogIn = true, + showLater = true, + animateEntrance = false + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt new file mode 100644 index 0000000..25998ca --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt @@ -0,0 +1,141 @@ +package com.cornellappdev.score.screen + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.unit.dp +import com.cornellappdev.score.components.OnboardingButtons +import com.cornellappdev.score.theme.CrimsonPrimary +import com.yourpackage.ui.onboarding.Speaker +import com.yourpackage.ui.onboarding.SportsIconAnimation +import kotlinx.coroutines.delay + +@Composable +fun OnboardingAnimationPreviewScreen( + onSignUp: () -> Unit, + onLogIn: () -> Unit, + onLater: () -> Unit, + modifier: Modifier = Modifier +) { + var animationKey by remember { mutableIntStateOf(0) } + + Box(modifier = modifier.fillMaxSize()) { + OnboardingAnimation( + animationKey = animationKey + ) + + Button( + onClick = { + animationKey++ + }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 56.dp, end = 16.dp) + ) { + Text("Replay") + } + } +} + +@Composable +private fun OnboardingAnimation( + animationKey: Int +) { + var showIcons by remember(animationKey) { + mutableStateOf(false) + } + + var showRings by remember(animationKey) { + mutableStateOf(true) + } + + var showButtons by remember(animationKey) { + mutableStateOf(false) + } + + var moveSpeakerUp by remember(animationKey) { + mutableStateOf(false) + } + + LaunchedEffect(animationKey) { + showIcons = false + showRings = true + showButtons = false + moveSpeakerUp = false + + delay(350) + showIcons = true + + delay(2550) + showIcons = false + showRings = false + + delay(1300) + moveSpeakerUp = true + + delay(700) + showButtons = true + } + + // Your existing Box containing: + // + // SportsIconAnimation(...) + // Megaphone(...) + // OnboardingButtons(...) + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = CrimsonPrimary + ) + ) { + + // Sports icons spread outward and fade as the megaphone grows. + SportsIconAnimation( + modifier = Modifier.fillMaxSize(), + visible = showIcons + ) + + // Speaker + expanding circles + Speaker( + moveUp = moveSpeakerUp, + showRings = showRings, + modifier = Modifier.align(Alignment.Center) + ) + + if (showButtons) { + OnboardingButtons( + onSignUp = { }, + onLogIn = { }, + onLater = { }, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 24.dp, + end = 24.dp, + bottom = 48.dp + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt new file mode 100644 index 0000000..a00f500 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt @@ -0,0 +1,645 @@ +package com.yourpackage.ui.onboarding + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +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.platform.LocalDensity +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.components.OnboardingButtons +import com.cornellappdev.score.theme.CrimsonPrimary +import kotlinx.coroutines.delay +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin + +private enum class OnboardingAnimationPhase { + Initial, + Icons, + IconsFading, + SpeakerTransition, + Complete +} + +@Composable +fun OnboardingScreen( + onSignUp: () -> Unit, + onLogIn: () -> Unit, + onLater: () -> Unit, + modifier: Modifier = Modifier +) { + var showIcons by remember { mutableStateOf(false) } + var showRings by remember { mutableStateOf(true) } + var showButtons by remember { mutableStateOf(false) } + var moveSpeakerUp by remember { mutableStateOf(false) } + + /* + * Timeline based on the Figma animation: + * + * 0ms -> red background + * ~300ms -> megaphone/rings start + * ~400ms -> sports icons start appearing + * ~2500ms -> icons finish + * ~3000ms -> icons fade out + * ~3200ms -> megaphone moves upward + * ~3400ms -> buttons appear + */ + LaunchedEffect(Unit) { + delay(350) + showIcons = true + + delay(2550) + showIcons = false + showRings = false + + delay(1300) + moveSpeakerUp = true + + delay(700) + showButtons = true + } + + Box( + modifier = modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color(0xFFB52A12), + CrimsonPrimary, + Color(0xFF941B0A) + ) + ) + ) + ) { + + // Sports icons spread outward and fade as the megaphone grows. + SportsIconAnimation( + modifier = Modifier.fillMaxSize(), + visible = showIcons + ) + + // Speaker + expanding circles + Speaker( + moveUp = moveSpeakerUp, + showRings = showRings, + modifier = Modifier.align(Alignment.Center) + ) + + if (showButtons) { + OnboardingButtons( + onSignUp = onSignUp, + onLogIn = onLogIn, + onLater = onLater, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 24.dp, + end = 24.dp, + bottom = 48.dp + ) + ) + } + } +} + +@Composable +fun Speaker( + moveUp: Boolean, + modifier: Modifier = Modifier, + showRings: Boolean = true +) { + val transition = updateTransition( + targetState = moveUp, + label = "speakerTransition" + ) + + val verticalOffset by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 700, + easing = FastOutSlowInEasing + ) + }, + label = "speakerY" + ) { isUp -> + if (isUp) -120f else 0f + } + + val scale by transition.animateFloat( + transitionSpec = { + tween( + durationMillis = 700, + easing = FastOutSlowInEasing + ) + }, + label = "speakerScale" + ) { isUp -> + if (isUp) 1.45f else 1f // this is centered + } + + val iconSize by transition.animateDp( + transitionSpec = { + tween( + durationMillis = 700, + easing = FastOutSlowInEasing + ) + }, + label = "speakerIconSize" + ) { isUp -> + if (isUp) 136.dp else 132.dp + } + + Box( + modifier = modifier + .offset { + IntOffset( + x = 0, + y = verticalOffset.roundToInt() + ) + } + .scale(scale) + .size(190.dp), + contentAlignment = Alignment.Center + ) { + + // Expanding circles + val infiniteTransition = rememberInfiniteTransition( + label = "rings" + ) + + val scale1 by infiniteTransition.animateFloat( + initialValue = 0.75f, + targetValue = 1.25f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1800, + easing = LinearOutSlowInEasing + ), + repeatMode = RepeatMode.Restart + ), + label = "ring1" + ) + + val scale2 by infiniteTransition.animateFloat( + initialValue = 0.65f, + targetValue = 1.15f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1800, + delayMillis = 450, + easing = LinearOutSlowInEasing + ), + repeatMode = RepeatMode.Restart + ), + label = "ring2" + ) + + val scale3 by infiniteTransition.animateFloat( + initialValue = 0.55f, + targetValue = 1.05f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1800, + delayMillis = 900, + easing = LinearOutSlowInEasing + ), + repeatMode = RepeatMode.Restart + ), + label = "ring3" + ) + + AnimatedVisibility( + visible = showRings, + enter = fadeIn(tween(250)), + exit = fadeOut(tween(700)) + ) { + Box(contentAlignment = Alignment.Center) { + Ring( + scale = scale1, + modifier = Modifier.size(190.dp) + ) + + Ring( + scale = scale2, + modifier = Modifier.size(190.dp) + ) + + Ring( + scale = scale3, + modifier = Modifier.size(190.dp) + ) + } + } + + Image( + painter = painterResource(R.drawable.ic_speaker), + contentDescription = null, + modifier = Modifier.size(iconSize), + colorFilter = ColorFilter.tint(Color.White) + ) + } +} + +@Composable +private fun Ring( + scale: Float, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .scale(scale) + .clip(shape = CircleShape) + .background(color = Color.White.copy(alpha = 0.055f)) + .border( + width = 1.dp, + color = Color.White.copy(alpha = 0.15f), + shape = CircleShape + ) + ) +} + +private data class SportsIcon( + @DrawableRes val drawable: Int, + val x: Float, + val y: Float, + val delay: Int, + val rotation: Float = 0f +) + +private val sportsIcons = listOf( + // Top row + SportsIcon( + drawable = R.drawable.ic_rowing_lightweight, + x = -0.22f, + y = -0.43f, + delay = 0 + ), + SportsIcon( + drawable = R.drawable.ic_field_hockey, + x = 0.10f, + y = -0.47f, + delay = 100 + ), + SportsIcon( + drawable = R.drawable.ic_wrestling, + x = 0.38f, + y = -0.40f, + delay = 200 + ), + + // Upper-middle + SportsIcon( + drawable = R.drawable.ic_basketball, + x = -0.03f, + y = -0.29f, + delay = 250 + ), + SportsIcon( + drawable = R.drawable.ic_tennis, + x = 0.27f, + y = -0.27f, + delay = 350 + ), + SportsIcon( + drawable = R.drawable.ic_volleyball, + x = -0.40f, + y = -0.22f, + delay = 400 + ), + SportsIcon( + drawable = R.drawable.ic_soccer, + x = 0.47f, + y = -0.17f, + delay = 500 + ), + + // Middle + SportsIcon( + drawable = R.drawable.ic_track_field, + x = -0.48f, + y = 0.02f, + delay = 550 + ), + SportsIcon( + drawable = R.drawable.ic_lacrosse, + x = 0.48f, + y = 0.05f, + delay = 650 + ), + + // Lower-middle + SportsIcon( + drawable = R.drawable.ic_swim_dive, + x = -0.40f, + y = 0.22f, + delay = 700 + ), + SportsIcon( + drawable = R.drawable.ic_field_hockey, + x = 0.23f, + y = 0.22f, + delay = 800 + ), + SportsIcon( + drawable = R.drawable.ic_baseball, + x = 0.43f, + y = 0.27f, + delay = 900 + ), + + // Bottom + SportsIcon( + drawable = R.drawable.ic_tennis, + x = -0.34f, + y = 0.40f, + delay = 1000 + ), + SportsIcon( + drawable = R.drawable.ic_golf, + x = 0.02f, + y = 0.42f, + delay = 1100 + ), + SportsIcon( + drawable = R.drawable.ic_football, + x = 0.35f, + y = 0.43f, + delay = 1200 + ) +) + +@Composable +fun SportsIconAnimation( + modifier: Modifier = Modifier, + visible: Boolean = true +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + val animationSize = with(LocalDensity.current) { maxWidth.toPx() } + + sportsIcons.forEach { icon -> + AnimatedSportsIcon( + icon = icon, + animationSize = animationSize, + visible = visible + ) + } + } +} + +@Composable +private fun AnimatedSportsIcon( + icon: SportsIcon, + animationSize: Float, + previewProgress: Float? = null, + visible: Boolean = true +) { + var started by remember { mutableStateOf(false) } + + LaunchedEffect(visible) { + if (visible && !started) { + delay(icon.delay.toLong()) + started = true + } + } + + val iconState = when { + !started -> 0 + visible -> 1 + else -> 2 + } + + val transition = updateTransition( + targetState = iconState, + label = "iconTransition" + ) + + val animatedProgress by transition.animateFloat( + transitionSpec = { + if (targetState == 1) { + spring(dampingRatio = 0.55f, stiffness = 500f) + } else { + tween( + durationMillis = if (targetState == 2) 1300 else 900, + easing = FastOutSlowInEasing + ) + } + }, + label = "iconProgress" + ) { state -> + when (state) { + 0 -> 0f + 1 -> 1f + else -> 1.6f + } + } + + val entranceWiggle = if ((icon.delay / 100) % 2 == 0) -18f else 18f + + val animatedScale by transition.animateFloat( + transitionSpec = { + if (targetState == 1) { + spring(dampingRatio = 0.45f, stiffness = 600f) + } else { + tween(durationMillis = 250) + } + }, + label = "iconScale" + ) { state -> + when (state) { + 0 -> 0.35f + else -> 1f + } + } + + val animatedRotation by transition.animateFloat( + transitionSpec = { + if (targetState == 1) { + spring(dampingRatio = 0.4f, stiffness = 500f) + } else { + tween(durationMillis = 250) + } + }, + label = "iconRotation" + ) { state -> + if (state == 0) icon.rotation + entranceWiggle else icon.rotation + } + + val animatedAlpha by transition.animateFloat( + transitionSpec = { + if (targetState == 2) { + keyframes { + durationMillis = 1300 + 1f at 0 + 1f at 350 + 0f at 1300 + } + } else { + tween(durationMillis = 500) + } + }, + label = "iconAlpha" + ) { state -> + if (state == 1) 1f else 0f + } + + val progress = previewProgress ?: animatedProgress + val alpha = if (previewProgress != null) { + if (previewProgress > 0f) 1f else 0f + } else { + animatedAlpha + } + val scale = if (previewProgress != null) { + if (previewProgress > 0f) 1f else 0.35f + } else { + animatedScale + } + val rotation = if (previewProgress != null) icon.rotation else animatedRotation + + val x = icon.x * animationSize * progress + val y = icon.y * 1.15f * animationSize * progress + + Image( + painter = painterResource(icon.drawable), + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(28.5.dp) + .alpha(alpha) + .scale(scale) + .rotate(rotation) + .offset { + IntOffset( + x = x.roundToInt(), + y = y.roundToInt() + ) + } + ) +} + +@Preview( + name = "Speaker - Center", + showBackground = true, + backgroundColor = 0xFFA3200C +) +@Composable +private fun SpeakerPreview() { + Box( + modifier = Modifier + .fillMaxSize() + .background(CrimsonPrimary), + contentAlignment = Alignment.Center + ) { + Speaker( + moveUp = false + ) + } +} + +@Preview( + name = "Speaker - Moved Up", + showBackground = true, + backgroundColor = 0xFFA3200C +) +@Composable +private fun SpeakerMovedPreview() { + Box( + modifier = Modifier + .fillMaxSize() + .background(CrimsonPrimary), + contentAlignment = Alignment.Center + ) { + Speaker( + moveUp = true + ) + } +} + +@Preview( + name = "All Sports Icons", + showBackground = true, + backgroundColor = 0xFFA3200C, + widthDp = 394, + heightDp = 852 +) +@Composable +private fun SportsIconAnimationPreview() { + Box( + modifier = Modifier + .fillMaxSize() + .background(CrimsonPrimary), + contentAlignment = Alignment.Center + ) { + sportsIcons.forEach { icon -> + AnimatedSportsIcon( + icon = icon, + 1f + ) + } + + Image( + painter = painterResource(R.drawable.ic_speaker), + contentDescription = null, + modifier = Modifier.size(132.dp), + colorFilter = ColorFilter.tint(Color.White) + ) + } +} \ No newline at end of file From 1c59b90dc2323e35c174a3718fd7606bd62359db Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 14 Oct 2026 18:14:06 -0400 Subject: [PATCH 16/40] Complete animation --- .../score/components/OnboardingButtons.kt | 27 +- .../OnboardingAnimationPreviewScreen.kt | 111 +-- .../score/screen/OnboardingScreen.kt | 459 +++++++++++++ .../score/screen/OnboardingScreen.kt.kt | 645 ------------------ 4 files changed, 486 insertions(+), 756 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt delete mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt diff --git a/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt index 9619b37..a03f591 100644 --- a/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt +++ b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt @@ -49,9 +49,9 @@ fun OnboardingButtons( LaunchedEffect(Unit) { showSignUp = true - delay(120.milliseconds) + delay(160.milliseconds) showLogIn = true - delay(120.milliseconds) + delay(180.milliseconds) showLater = true } @@ -85,7 +85,9 @@ private fun OnboardingButtonsContent( OnboardingButtonSlot( modifier = Modifier.fillMaxWidth().height(50.dp), visible = showSignUp, - animateEntrance = animateEntrance + animateEntrance = animateEntrance, + initialOffset = 26.dp, + durationMillis = 340 ) { Button( onClick = onSignUp, @@ -109,7 +111,9 @@ private fun OnboardingButtonsContent( OnboardingButtonSlot( modifier = Modifier.fillMaxWidth().height(48.dp), visible = showLogIn, - animateEntrance = animateEntrance + animateEntrance = animateEntrance, + initialOffset = 24.dp, + durationMillis = 340 ) { OutlinedButton( onClick = onLogIn, @@ -136,7 +140,9 @@ private fun OnboardingButtonsContent( OnboardingButtonSlot( modifier = Modifier.fillMaxWidth().height(40.dp), visible = showLater, - animateEntrance = animateEntrance + animateEntrance = animateEntrance, + initialOffset = 18.dp, + durationMillis = 320 ) { TextButton( onClick = onLater, @@ -155,9 +161,14 @@ private fun OnboardingButtonsContent( private fun OnboardingButtonSlot( visible: Boolean, animateEntrance: Boolean, + initialOffset: androidx.compose.ui.unit.Dp, + durationMillis: Int, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { + val initialOffsetPx = with(androidx.compose.ui.platform.LocalDensity.current) { + initialOffset.roundToPx() + } Box( modifier = modifier, contentAlignment = Alignment.Center @@ -165,9 +176,9 @@ private fun OnboardingButtonSlot( if (animateEntrance) { AnimatedVisibility( visible = visible, - enter = fadeIn(tween(350)) + slideInVertically( - initialOffsetY = { it / 3 }, - animationSpec = tween(400) + enter = fadeIn(tween(durationMillis)) + slideInVertically( + initialOffsetY = { initialOffsetPx }, + animationSpec = tween(durationMillis) ) ) { content() diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt index 25998ca..a2505d8 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt @@ -1,35 +1,22 @@ package com.cornellappdev.score.screen -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -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.unit.dp -import com.cornellappdev.score.components.OnboardingButtons -import com.cornellappdev.score.theme.CrimsonPrimary -import com.yourpackage.ui.onboarding.Speaker -import com.yourpackage.ui.onboarding.SportsIconAnimation -import kotlinx.coroutines.delay +/** + * Preview host for replaying the same onboarding animation used by the real screen. + */ @Composable fun OnboardingAnimationPreviewScreen( onSignUp: () -> Unit, @@ -40,14 +27,15 @@ fun OnboardingAnimationPreviewScreen( var animationKey by remember { mutableIntStateOf(0) } Box(modifier = modifier.fillMaxSize()) { - OnboardingAnimation( + OnboardingScreen( + onSignUp = onSignUp, + onLogIn = onLogIn, + onLater = onLater, animationKey = animationKey ) Button( - onClick = { - animationKey++ - }, + onClick = { animationKey++ }, modifier = Modifier .align(Alignment.TopEnd) .padding(top = 56.dp, end = 16.dp) @@ -56,86 +44,3 @@ fun OnboardingAnimationPreviewScreen( } } } - -@Composable -private fun OnboardingAnimation( - animationKey: Int -) { - var showIcons by remember(animationKey) { - mutableStateOf(false) - } - - var showRings by remember(animationKey) { - mutableStateOf(true) - } - - var showButtons by remember(animationKey) { - mutableStateOf(false) - } - - var moveSpeakerUp by remember(animationKey) { - mutableStateOf(false) - } - - LaunchedEffect(animationKey) { - showIcons = false - showRings = true - showButtons = false - moveSpeakerUp = false - - delay(350) - showIcons = true - - delay(2550) - showIcons = false - showRings = false - - delay(1300) - moveSpeakerUp = true - - delay(700) - showButtons = true - } - - // Your existing Box containing: - // - // SportsIconAnimation(...) - // Megaphone(...) - // OnboardingButtons(...) - Box( - modifier = Modifier - .fillMaxSize() - .background( - color = CrimsonPrimary - ) - ) { - - // Sports icons spread outward and fade as the megaphone grows. - SportsIconAnimation( - modifier = Modifier.fillMaxSize(), - visible = showIcons - ) - - // Speaker + expanding circles - Speaker( - moveUp = moveSpeakerUp, - showRings = showRings, - modifier = Modifier.align(Alignment.Center) - ) - - if (showButtons) { - OnboardingButtons( - onSignUp = { }, - onLogIn = { }, - onLater = { }, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding( - start = 24.dp, - end = 24.dp, - bottom = 48.dp - ) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt new file mode 100644 index 0000000..eb5f254 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt @@ -0,0 +1,459 @@ +package com.cornellappdev.score.screen + +import androidx.annotation.DrawableRes +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Easing +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R +import com.cornellappdev.score.components.OnboardingButtons +import com.cornellappdev.score.theme.CrimsonPrimary +import kotlin.math.min + +private const val TIMELINE_DURATION_MS = 3_850 +private const val ICONS_HOLD_END_MS = 2_420 +private const val ICONS_FADE_END_MS = 3_020 +private const val ICONS_EXIT_END_MS = 3_080 + +private data class MotionKey(val timeMs: Float, val value: Float, val easing: Easing = LinearEasing) + +private fun sampleTrack(timeMs: Float, keys: List): Float { + if (timeMs <= keys.first().timeMs) return keys.first().value + for (index in 0 until keys.lastIndex) { + val start = keys[index] + val end = keys[index + 1] + if (timeMs <= end.timeMs) { + val fraction = ((timeMs - start.timeMs) / (end.timeMs - start.timeMs)).coerceIn(0f, 1f) + return start.value + (end.value - start.value) * start.easing.transform(fraction) + } + } + return keys.last().value +} + +private data class SportMotion( + @DrawableRes val drawable: Int, + val left: Float, + val top: Float, + val enterAtMs: Float, + val peakAlpha: Float, + val entryX: Float, + val entryY: Float, + val bounceX: Float, + val bounceY: Float, + val settleX: Float, + val settleY: Float, + val initialRotation: Float +) + +private val sportMotions = listOf( + SportMotion(R.drawable.ic_basketball, 172f, 237f, 340f, .94f, 5f, 141f, 13.441f, -16.006f, -6.071f, 6.568f, -18f), + SportMotion(R.drawable.ic_baseball, 285f, 299f, 385f, .94f, -108f, 79f, 3.615f, -19.99f, -1.141f, 8.64f, 18f), + SportMotion(R.drawable.ic_cross_country, 20f, 331f, 430f, .94f, 157f, 47f, -13.255f, -18.582f, 5.258f, 8.15f, -18f), + SportMotion(R.drawable.ic_football, 316f, 441f, 475f, .94f, -139f, -63f, 21.069f, -5.821f, -8.856f, 2.903f, 18f), + SportMotion(R.drawable.ic_golf, 176f, 526f, 520f, .94f, 1f, -148f, -14.11f, 16.185f, 6.345f, -6.617f, -18f), + SportMotion(R.drawable.ic_volleyball, 20f, 482f, 565f, .94f, 157f, -104f, -9.539f, 23.112f, 3.586f, -9.932f, 18f), + SportMotion(R.drawable.ic_ice_hockey, 220f, 179f, 580f, .8f, -43f, 199f, 18.414f, -18.933f, -8.093f, 7.624f, -18f), + SportMotion(R.drawable.ic_swim_dive, 259f, 500f, 610f, .94f, -82f, -122f, -2.599f, 21.23f, 1.539f, -9.004f, -18f), + SportMotion(R.drawable.ic_soccer, 251f, 240f, 625f, .8f, -74f, 138f, -4.198f, -21.796f, 2.222f, 9.187f, 18f), + SportMotion(R.drawable.ic_tennis, 334f, 526f, 670f, .8f, -157f, -148f, 7.667f, 26.467f, -2.744f, -11.244f, -18f), + SportMotion(R.drawable.ic_lacrosse, 255f, 582f, 715f, .8f, -78f, -204f, 21.657f, 17.44f, -9.395f, -6.93f, 18f), + SportMotion(R.drawable.ic_wrestling, 94f, 508f, 760f, .8f, 83f, -130f, -20.93f, 6.766f, 9.045f, -2.46f, -18f), + SportMotion(R.drawable.ic_fencing, 23f, 223f, 805f, .8f, 154f, 155f, -26.871f, -7.183f, 11.399f, 2.535f, 18f), + SportMotion(R.drawable.ic_softball, 41f, 575f, 820f, .66f, 136f, -197f, -26.481f, 13.716f, 11.305f, -5.286f, -18f), + SportMotion(R.drawable.ic_sailing, 72f, 263f, 850f, .8f, 105f, 115f, -1.211f, -22.09f, .073f, 9.423f, -18f), + SportMotion(R.drawable.ic_equestrian, 142f, 591f, 865f, .66f, 35f, -213f, 9.965f, 25.7f, -4.642f, -10.607f, 18f), + SportMotion(R.drawable.ic_field_hockey, 334f, 355f, 910f, .66f, -157f, 23f, 19.299f, 11.322f, -7.978f, -5.198f, -18f), + SportMotion(R.drawable.ic_gymnastics, 338f, 257f, 955f, .66f, -161f, 121f, 9.299f, -24.502f, -3.46f, 10.481f, 18f), + SportMotion(R.drawable.ic_squash, 61f, 415f, 1_000f, .66f, 116f, -37f, -17.014f, -9.268f, 7.134f, 4.337f, -18f), + SportMotion(R.drawable.ic_rowing_lightweight, 101f, 191f, 1_045f, .66f, 76f, 187f, -21.33f, -15.299f, 9.256f, 6.043f, 18f), + SportMotion(R.drawable.ic_track_field, 302f, 201f, 1_090f, .66f, -125f, 177f, 25.186f, -11.394f, -10.771f, 4.331f, -18f) +) + +@Composable +fun OnboardingScreen( + onSignUp: () -> Unit, + onLogIn: () -> Unit, + onLater: () -> Unit, + modifier: Modifier = Modifier, + animationKey: Int = 0 +) { + val playhead = remember(animationKey) { Animatable(0f) } + LaunchedEffect(animationKey) { + playhead.snapTo(0f) + playhead.animateTo( + TIMELINE_DURATION_MS.toFloat(), + animationSpec = tween(TIMELINE_DURATION_MS, easing = LinearEasing) + ) + } + val timeMs = playhead.value + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(CrimsonPrimary) + ) { + val designScale = min(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + FigmaSpeaker( + timeMs = timeMs, + left = 98f, + top = 229f, + size = 197f, + designDp = designDp, + alphaKeys = listOf(MotionKey(0f, 0f), MotionKey(2_760f, 0f), MotionKey(3_280f, 1f)), + scaleKeys = listOf(MotionKey(0f, .67f), MotionKey(2_760f, .67f), MotionKey(3_280f, 1f)), + yKeys = listOf(MotionKey(0f, 68.5f), MotionKey(2_760f, 68.5f), MotionKey(3_280f, 0f, FastOutSlowInEasing)) + ) + + if (timeMs >= 3_020f) { + OnboardingButtons( + onSignUp = onSignUp, + onLogIn = onLogIn, + onLater = onLater, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(start = 24.dp, end = 24.dp, bottom = 40.dp) + ) + } + + FigmaHalo( + timeMs = timeMs, + designDp = designDp + ) + FigmaRing( + timeMs = timeMs, + designDp = designDp, + delayMs = 240f, + opacityPeak = .72f, + opacityInEnd = 420f, + opacityOutStart = 420f, + opacityOutEnd = 1_020f, + scalePeak = 1.55f, + scalePeakTime = 1_020f, + scaleExit = 2.092f + ) + FigmaRing( + timeMs = timeMs, + designDp = designDp, + delayMs = 420f, + opacityPeak = .5f, + opacityInEnd = 620f, + opacityOutStart = 620f, + opacityOutEnd = 1_260f, + scalePeak = 1.72f, + scalePeakTime = 1_260f, + scaleExit = 2.322f + ) + + FigmaSpeaker( + timeMs = timeMs, + left = 129f, + top = 330f, + size = 132f, + designDp = designDp, + alphaKeys = listOf( + MotionKey(0f, 0f), + MotionKey(180f, 1f, FastOutSlowInEasing), + MotionKey(3_120f, 1f), + MotionKey(3_340f, 0f, FastOutLinearInEasing) + ), + scaleKeys = listOf( + MotionKey(0f, .54f), + MotionKey(420f, 1.17f, FastOutSlowInEasing), + MotionKey(630f, .94f, FastOutSlowInEasing), + MotionKey(820f, 1f, FastOutSlowInEasing), + MotionKey(2_420f, 1f), + MotionKey(3_220f, 1.492f, FastOutSlowInEasing) + ), + yKeys = listOf( + MotionKey(0f, 34f), + MotionKey(420f, -9f, FastOutSlowInEasing), + MotionKey(630f, 4f, FastOutSlowInEasing), + MotionKey(820f, 0f, FastOutSlowInEasing), + MotionKey(2_420f, 0f), + MotionKey(3_220f, -68.5f, FastOutSlowInEasing) + ), + rotationKeys = listOf( + MotionKey(0f, 9f), + MotionKey(420f, -6f, FastOutSlowInEasing), + MotionKey(630f, 3f, FastOutSlowInEasing), + MotionKey(820f, 0f, FastOutSlowInEasing) + ) + ) + + sportMotions.forEach { sport -> + FigmaSportIcon(timeMs = timeMs, motion = sport, designDp = designDp) + } + } +} + +@Composable +private fun FigmaSpeaker( + timeMs: Float, + left: Float, + top: Float, + size: Float, + designDp: (Float) -> Dp, + alphaKeys: List, + scaleKeys: List, + yKeys: List, + rotationKeys: List = listOf(MotionKey(0f, 0f)) +) { + val y = sampleTrack(timeMs, yKeys) + val alpha = sampleTrack(timeMs, alphaKeys).coerceIn(0f, 1f) + val scale = sampleTrack(timeMs, scaleKeys) + val rotation = sampleTrack(timeMs, rotationKeys) + + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(R.drawable.ic_speaker), + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .align(Alignment.Center) + .offset( + x = designDp(left + size / 2f - 196.5f), + y = designDp(top + size / 2f - 426f + y) + ) + .size(designDp(size)) + .graphicsLayer { + this.alpha = alpha + scaleX = scale + scaleY = scale + rotationZ = rotation + } + ) + } +} + +@Composable +private fun FigmaHalo(timeMs: Float, designDp: (Float) -> Dp) { + val alpha = sampleTrack( + timeMs, + listOf( + MotionKey(0f, 0f), + MotionKey(240f, 1f, FastOutSlowInEasing), + MotionKey(1_160f, .72f), + MotionKey(2_420f, .72f), + MotionKey(2_920f, 0f, FastOutLinearInEasing) + ) + ) + val scale = sampleTrack( + timeMs, + listOf( + MotionKey(0f, .62f), + MotionKey(460f, 1.12f, FastOutSlowInEasing), + MotionKey(840f, 1f, FastOutSlowInEasing), + MotionKey(2_420f, 1f), + MotionKey(2_980f, 1.35f, FastOutSlowInEasing) + ) + ) + Box(modifier = Modifier.fillMaxSize()) { + Canvas( + modifier = Modifier + .align(Alignment.Center) + .offset(x = designDp(-1.5f), y = designDp(-30f)) + .size(designDp(188f)) + .graphicsLayer { + this.alpha = alpha + scaleX = scale + scaleY = scale + } + ) { + drawCircle(Color.White.copy(alpha = .055f)) + drawCircle(Color.White.copy(alpha = .16f), style = Stroke(width = 1.dp.toPx())) + } + } +} + +@Composable +private fun FigmaRing( + timeMs: Float, + designDp: (Float) -> Dp, + delayMs: Float, + opacityPeak: Float, + opacityInEnd: Float, + opacityOutStart: Float, + opacityOutEnd: Float, + scalePeak: Float, + scalePeakTime: Float, + scaleExit: Float +) { + val alpha = sampleTrack( + timeMs, + listOf( + MotionKey(0f, 0f), + MotionKey(delayMs, 0f), + MotionKey(opacityInEnd, opacityPeak, FastOutSlowInEasing), + MotionKey(opacityOutEnd, 0f, FastOutLinearInEasing) + ) + ) + val scale = sampleTrack( + timeMs, + listOf( + MotionKey(0f, .28f), + MotionKey(delayMs, .28f), + MotionKey(scalePeakTime, scalePeak, FastOutSlowInEasing), + MotionKey(2_420f, scalePeak), + MotionKey(2_980f, scaleExit, FastOutSlowInEasing) + ) + ) + Box(modifier = Modifier.fillMaxSize()) { + Canvas( + modifier = Modifier + .align(Alignment.Center) + .offset(x = designDp(-1.5f), y = designDp(-30f)) + .size(designDp(188f)) + .graphicsLayer { + this.alpha = alpha.coerceIn(0f, 1f) + scaleX = scale + scaleY = scale + } + ) { + drawCircle( + color = Color.White.copy(alpha = .18f), + style = Stroke(width = 1.dp.toPx()) + ) + } + } +} + +@Composable +private fun FigmaSportIcon( + timeMs: Float, + motion: SportMotion, + designDp: (Float) -> Dp +) { + val peakTime = motion.enterAtMs + 160f + val bounceTime = motion.enterAtMs + 310f + val settleTime = motion.enterAtMs + 520f + val landedTime = motion.enterAtMs + 700f + val wiggleTime = motion.enterAtMs + 860f + val settledTime = motion.enterAtMs + 1_020f + val centerX = motion.left + 18f - 196.5f + val centerY = motion.top + 18f - 426f + val radius = kotlin.math.hypot(centerX, centerY).coerceAtLeast(1f) + val entryX = centerX / radius * 450f + val entryY = centerY / radius * 450f + val exitX = centerX / radius * 58f + val exitY = centerY / radius * 58f + + val alpha = sampleTrack( + timeMs, + listOf( + MotionKey(0f, 0f), + MotionKey(motion.enterAtMs, 0f), + MotionKey(peakTime, motion.peakAlpha), + MotionKey(ICONS_HOLD_END_MS.toFloat(), motion.peakAlpha), + MotionKey(ICONS_FADE_END_MS.toFloat(), 0f, FastOutLinearInEasing) + ) + ) + val x = sampleTrack( + timeMs, + listOf( + MotionKey(0f, entryX), + MotionKey(motion.enterAtMs, entryX), + MotionKey(bounceTime, motion.bounceX, FastOutSlowInEasing), + MotionKey(settleTime, motion.settleX, FastOutSlowInEasing), + MotionKey(landedTime, 0f, FastOutSlowInEasing), + MotionKey(ICONS_HOLD_END_MS.toFloat(), 0f), + MotionKey(ICONS_EXIT_END_MS.toFloat(), exitX, FastOutLinearInEasing) + ) + ) + val y = sampleTrack( + timeMs, + listOf( + MotionKey(0f, entryY), + MotionKey(motion.enterAtMs, entryY), + MotionKey(bounceTime, motion.bounceY, FastOutSlowInEasing), + MotionKey(settleTime, motion.settleY, FastOutSlowInEasing), + MotionKey(landedTime, 0f, FastOutSlowInEasing), + MotionKey(wiggleTime, -5f, FastOutSlowInEasing), + MotionKey(settledTime, 0f, FastOutSlowInEasing), + MotionKey(ICONS_HOLD_END_MS.toFloat(), 0f), + MotionKey(ICONS_EXIT_END_MS.toFloat(), exitY, FastOutLinearInEasing) + ) + ) + val scale = sampleTrack( + timeMs, + listOf( + MotionKey(0f, .45f), + MotionKey(motion.enterAtMs, .45f), + MotionKey(bounceTime, 1.13f, FastOutSlowInEasing), + MotionKey(settleTime, .94f, FastOutSlowInEasing), + MotionKey(landedTime, 1f, FastOutSlowInEasing), + MotionKey(wiggleTime, 1.04f, FastOutSlowInEasing), + MotionKey(settledTime, 1f, FastOutSlowInEasing), + MotionKey(ICONS_HOLD_END_MS.toFloat(), 1f), + MotionKey(ICONS_EXIT_END_MS.toFloat(), .68f, FastOutLinearInEasing) + ) + ) + val rotation = sampleTrack( + timeMs, + listOf( + MotionKey(0f, motion.initialRotation), + MotionKey(motion.enterAtMs, motion.initialRotation), + MotionKey(bounceTime, motion.initialRotation * -.5f, FastOutSlowInEasing), + MotionKey(settleTime, motion.initialRotation * .22f, FastOutSlowInEasing), + MotionKey(landedTime, 0f, FastOutSlowInEasing), + MotionKey(wiggleTime, motion.initialRotation * .22f, FastOutSlowInEasing), + MotionKey(settledTime, 0f, FastOutSlowInEasing), + MotionKey(ICONS_HOLD_END_MS.toFloat(), 0f), + MotionKey(ICONS_EXIT_END_MS.toFloat(), motion.initialRotation * .55f, FastOutLinearInEasing) + ) + ) + + Box( + modifier = Modifier + .offset(x = designDp(motion.left), y = designDp(motion.top)) + .size(designDp(36f)), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(motion.drawable), + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(36.dp) + .graphicsLayer { + this.alpha = alpha.coerceIn(0f, 1f) + scaleX = scale + scaleY = scale + rotationZ = rotation + translationX = designDp(x).toPx() + translationY = designDp(y).toPx() + } + ) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun OnboardingScreenPreview() { + OnboardingScreen(onSignUp = {}, onLogIn = {}, onLater = {}) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt deleted file mode 100644 index a00f500..0000000 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt.kt +++ /dev/null @@ -1,645 +0,0 @@ -package com.yourpackage.ui.onboarding - -import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.keyframes -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -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.platform.LocalDensity -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.sp -import com.cornellappdev.score.R -import com.cornellappdev.score.components.OnboardingButtons -import com.cornellappdev.score.theme.CrimsonPrimary -import kotlinx.coroutines.delay -import kotlin.math.cos -import kotlin.math.roundToInt -import kotlin.math.sin - -private enum class OnboardingAnimationPhase { - Initial, - Icons, - IconsFading, - SpeakerTransition, - Complete -} - -@Composable -fun OnboardingScreen( - onSignUp: () -> Unit, - onLogIn: () -> Unit, - onLater: () -> Unit, - modifier: Modifier = Modifier -) { - var showIcons by remember { mutableStateOf(false) } - var showRings by remember { mutableStateOf(true) } - var showButtons by remember { mutableStateOf(false) } - var moveSpeakerUp by remember { mutableStateOf(false) } - - /* - * Timeline based on the Figma animation: - * - * 0ms -> red background - * ~300ms -> megaphone/rings start - * ~400ms -> sports icons start appearing - * ~2500ms -> icons finish - * ~3000ms -> icons fade out - * ~3200ms -> megaphone moves upward - * ~3400ms -> buttons appear - */ - LaunchedEffect(Unit) { - delay(350) - showIcons = true - - delay(2550) - showIcons = false - showRings = false - - delay(1300) - moveSpeakerUp = true - - delay(700) - showButtons = true - } - - Box( - modifier = modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color(0xFFB52A12), - CrimsonPrimary, - Color(0xFF941B0A) - ) - ) - ) - ) { - - // Sports icons spread outward and fade as the megaphone grows. - SportsIconAnimation( - modifier = Modifier.fillMaxSize(), - visible = showIcons - ) - - // Speaker + expanding circles - Speaker( - moveUp = moveSpeakerUp, - showRings = showRings, - modifier = Modifier.align(Alignment.Center) - ) - - if (showButtons) { - OnboardingButtons( - onSignUp = onSignUp, - onLogIn = onLogIn, - onLater = onLater, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding( - start = 24.dp, - end = 24.dp, - bottom = 48.dp - ) - ) - } - } -} - -@Composable -fun Speaker( - moveUp: Boolean, - modifier: Modifier = Modifier, - showRings: Boolean = true -) { - val transition = updateTransition( - targetState = moveUp, - label = "speakerTransition" - ) - - val verticalOffset by transition.animateFloat( - transitionSpec = { - tween( - durationMillis = 700, - easing = FastOutSlowInEasing - ) - }, - label = "speakerY" - ) { isUp -> - if (isUp) -120f else 0f - } - - val scale by transition.animateFloat( - transitionSpec = { - tween( - durationMillis = 700, - easing = FastOutSlowInEasing - ) - }, - label = "speakerScale" - ) { isUp -> - if (isUp) 1.45f else 1f // this is centered - } - - val iconSize by transition.animateDp( - transitionSpec = { - tween( - durationMillis = 700, - easing = FastOutSlowInEasing - ) - }, - label = "speakerIconSize" - ) { isUp -> - if (isUp) 136.dp else 132.dp - } - - Box( - modifier = modifier - .offset { - IntOffset( - x = 0, - y = verticalOffset.roundToInt() - ) - } - .scale(scale) - .size(190.dp), - contentAlignment = Alignment.Center - ) { - - // Expanding circles - val infiniteTransition = rememberInfiniteTransition( - label = "rings" - ) - - val scale1 by infiniteTransition.animateFloat( - initialValue = 0.75f, - targetValue = 1.25f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1800, - easing = LinearOutSlowInEasing - ), - repeatMode = RepeatMode.Restart - ), - label = "ring1" - ) - - val scale2 by infiniteTransition.animateFloat( - initialValue = 0.65f, - targetValue = 1.15f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1800, - delayMillis = 450, - easing = LinearOutSlowInEasing - ), - repeatMode = RepeatMode.Restart - ), - label = "ring2" - ) - - val scale3 by infiniteTransition.animateFloat( - initialValue = 0.55f, - targetValue = 1.05f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1800, - delayMillis = 900, - easing = LinearOutSlowInEasing - ), - repeatMode = RepeatMode.Restart - ), - label = "ring3" - ) - - AnimatedVisibility( - visible = showRings, - enter = fadeIn(tween(250)), - exit = fadeOut(tween(700)) - ) { - Box(contentAlignment = Alignment.Center) { - Ring( - scale = scale1, - modifier = Modifier.size(190.dp) - ) - - Ring( - scale = scale2, - modifier = Modifier.size(190.dp) - ) - - Ring( - scale = scale3, - modifier = Modifier.size(190.dp) - ) - } - } - - Image( - painter = painterResource(R.drawable.ic_speaker), - contentDescription = null, - modifier = Modifier.size(iconSize), - colorFilter = ColorFilter.tint(Color.White) - ) - } -} - -@Composable -private fun Ring( - scale: Float, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .scale(scale) - .clip(shape = CircleShape) - .background(color = Color.White.copy(alpha = 0.055f)) - .border( - width = 1.dp, - color = Color.White.copy(alpha = 0.15f), - shape = CircleShape - ) - ) -} - -private data class SportsIcon( - @DrawableRes val drawable: Int, - val x: Float, - val y: Float, - val delay: Int, - val rotation: Float = 0f -) - -private val sportsIcons = listOf( - // Top row - SportsIcon( - drawable = R.drawable.ic_rowing_lightweight, - x = -0.22f, - y = -0.43f, - delay = 0 - ), - SportsIcon( - drawable = R.drawable.ic_field_hockey, - x = 0.10f, - y = -0.47f, - delay = 100 - ), - SportsIcon( - drawable = R.drawable.ic_wrestling, - x = 0.38f, - y = -0.40f, - delay = 200 - ), - - // Upper-middle - SportsIcon( - drawable = R.drawable.ic_basketball, - x = -0.03f, - y = -0.29f, - delay = 250 - ), - SportsIcon( - drawable = R.drawable.ic_tennis, - x = 0.27f, - y = -0.27f, - delay = 350 - ), - SportsIcon( - drawable = R.drawable.ic_volleyball, - x = -0.40f, - y = -0.22f, - delay = 400 - ), - SportsIcon( - drawable = R.drawable.ic_soccer, - x = 0.47f, - y = -0.17f, - delay = 500 - ), - - // Middle - SportsIcon( - drawable = R.drawable.ic_track_field, - x = -0.48f, - y = 0.02f, - delay = 550 - ), - SportsIcon( - drawable = R.drawable.ic_lacrosse, - x = 0.48f, - y = 0.05f, - delay = 650 - ), - - // Lower-middle - SportsIcon( - drawable = R.drawable.ic_swim_dive, - x = -0.40f, - y = 0.22f, - delay = 700 - ), - SportsIcon( - drawable = R.drawable.ic_field_hockey, - x = 0.23f, - y = 0.22f, - delay = 800 - ), - SportsIcon( - drawable = R.drawable.ic_baseball, - x = 0.43f, - y = 0.27f, - delay = 900 - ), - - // Bottom - SportsIcon( - drawable = R.drawable.ic_tennis, - x = -0.34f, - y = 0.40f, - delay = 1000 - ), - SportsIcon( - drawable = R.drawable.ic_golf, - x = 0.02f, - y = 0.42f, - delay = 1100 - ), - SportsIcon( - drawable = R.drawable.ic_football, - x = 0.35f, - y = 0.43f, - delay = 1200 - ) -) - -@Composable -fun SportsIconAnimation( - modifier: Modifier = Modifier, - visible: Boolean = true -) { - BoxWithConstraints( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - val animationSize = with(LocalDensity.current) { maxWidth.toPx() } - - sportsIcons.forEach { icon -> - AnimatedSportsIcon( - icon = icon, - animationSize = animationSize, - visible = visible - ) - } - } -} - -@Composable -private fun AnimatedSportsIcon( - icon: SportsIcon, - animationSize: Float, - previewProgress: Float? = null, - visible: Boolean = true -) { - var started by remember { mutableStateOf(false) } - - LaunchedEffect(visible) { - if (visible && !started) { - delay(icon.delay.toLong()) - started = true - } - } - - val iconState = when { - !started -> 0 - visible -> 1 - else -> 2 - } - - val transition = updateTransition( - targetState = iconState, - label = "iconTransition" - ) - - val animatedProgress by transition.animateFloat( - transitionSpec = { - if (targetState == 1) { - spring(dampingRatio = 0.55f, stiffness = 500f) - } else { - tween( - durationMillis = if (targetState == 2) 1300 else 900, - easing = FastOutSlowInEasing - ) - } - }, - label = "iconProgress" - ) { state -> - when (state) { - 0 -> 0f - 1 -> 1f - else -> 1.6f - } - } - - val entranceWiggle = if ((icon.delay / 100) % 2 == 0) -18f else 18f - - val animatedScale by transition.animateFloat( - transitionSpec = { - if (targetState == 1) { - spring(dampingRatio = 0.45f, stiffness = 600f) - } else { - tween(durationMillis = 250) - } - }, - label = "iconScale" - ) { state -> - when (state) { - 0 -> 0.35f - else -> 1f - } - } - - val animatedRotation by transition.animateFloat( - transitionSpec = { - if (targetState == 1) { - spring(dampingRatio = 0.4f, stiffness = 500f) - } else { - tween(durationMillis = 250) - } - }, - label = "iconRotation" - ) { state -> - if (state == 0) icon.rotation + entranceWiggle else icon.rotation - } - - val animatedAlpha by transition.animateFloat( - transitionSpec = { - if (targetState == 2) { - keyframes { - durationMillis = 1300 - 1f at 0 - 1f at 350 - 0f at 1300 - } - } else { - tween(durationMillis = 500) - } - }, - label = "iconAlpha" - ) { state -> - if (state == 1) 1f else 0f - } - - val progress = previewProgress ?: animatedProgress - val alpha = if (previewProgress != null) { - if (previewProgress > 0f) 1f else 0f - } else { - animatedAlpha - } - val scale = if (previewProgress != null) { - if (previewProgress > 0f) 1f else 0.35f - } else { - animatedScale - } - val rotation = if (previewProgress != null) icon.rotation else animatedRotation - - val x = icon.x * animationSize * progress - val y = icon.y * 1.15f * animationSize * progress - - Image( - painter = painterResource(icon.drawable), - contentDescription = null, - colorFilter = ColorFilter.tint(Color.White), - modifier = Modifier - .size(28.5.dp) - .alpha(alpha) - .scale(scale) - .rotate(rotation) - .offset { - IntOffset( - x = x.roundToInt(), - y = y.roundToInt() - ) - } - ) -} - -@Preview( - name = "Speaker - Center", - showBackground = true, - backgroundColor = 0xFFA3200C -) -@Composable -private fun SpeakerPreview() { - Box( - modifier = Modifier - .fillMaxSize() - .background(CrimsonPrimary), - contentAlignment = Alignment.Center - ) { - Speaker( - moveUp = false - ) - } -} - -@Preview( - name = "Speaker - Moved Up", - showBackground = true, - backgroundColor = 0xFFA3200C -) -@Composable -private fun SpeakerMovedPreview() { - Box( - modifier = Modifier - .fillMaxSize() - .background(CrimsonPrimary), - contentAlignment = Alignment.Center - ) { - Speaker( - moveUp = true - ) - } -} - -@Preview( - name = "All Sports Icons", - showBackground = true, - backgroundColor = 0xFFA3200C, - widthDp = 394, - heightDp = 852 -) -@Composable -private fun SportsIconAnimationPreview() { - Box( - modifier = Modifier - .fillMaxSize() - .background(CrimsonPrimary), - contentAlignment = Alignment.Center - ) { - sportsIcons.forEach { icon -> - AnimatedSportsIcon( - icon = icon, - 1f - ) - } - - Image( - painter = painterResource(R.drawable.ic_speaker), - contentDescription = null, - modifier = Modifier.size(132.dp), - colorFilter = ColorFilter.tint(Color.White) - ) - } -} \ No newline at end of file From 72ae47472ae1d7e5d6f156d485705f0ea095ce1d Mon Sep 17 00:00:00 2001 From: amjiao Date: Fri, 16 Oct 2026 13:32:52 -0400 Subject: [PATCH 17/40] Complete onboarding animation --- .../com/cornellappdev/score/MainActivity.kt | 43 ++++++++++++++++--- .../score/components/OnboardingButtons.kt | 24 +++++++---- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index a9b44ff..512793e 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -4,13 +4,21 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation -import com.cornellappdev.score.screen.OnboardingAnimationPreviewScreen +import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.analytics import dagger.hilt.android.AndroidEntryPoint +private const val APP_PREFERENCES = "score_app_preferences" +private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" + private lateinit var firebaseAnalytics: FirebaseAnalytics @AndroidEntryPoint @@ -18,14 +26,35 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + val preferences = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE) + val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) + setContent { - //RootNavigation() - OnboardingAnimationPreviewScreen( - onSignUp = {}, - onLogIn = {}, - onLater = {} - ) + var showOnboarding by rememberSaveable { + mutableStateOf(!onboardingCompleted) + } + + val completeOnboarding = remember { + { + preferences.edit() + .putBoolean(KEY_ONBOARDING_COMPLETED, true) + .apply() + showOnboarding = false + } + } + + if (showOnboarding) { + OnboardingScreen( + onSignUp = completeOnboarding, + onLogIn = completeOnboarding, + onLater = completeOnboarding + ) + } else { + RootNavigation() + } } + firebaseAnalytics = Firebase.analytics } } diff --git a/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt index a03f591..97ec48c 100644 --- a/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt +++ b/app/src/main/java/com/cornellappdev/score/components/OnboardingButtons.kt @@ -1,9 +1,9 @@ package com.cornellappdev.score.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically -import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,10 +25,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.Style.heading3 import com.cornellappdev.score.theme.Style.labelsNormal @@ -83,7 +81,9 @@ private fun OnboardingButtonsContent( horizontalAlignment = Alignment.CenterHorizontally ) { OnboardingButtonSlot( - modifier = Modifier.fillMaxWidth().height(50.dp), + modifier = Modifier + .fillMaxWidth() + .height(50.dp), visible = showSignUp, animateEntrance = animateEntrance, initialOffset = 26.dp, @@ -91,7 +91,9 @@ private fun OnboardingButtonsContent( ) { Button( onClick = onSignUp, - modifier = Modifier.fillMaxWidth().height(48.dp), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), shape = RoundedCornerShape(100), colors = ButtonDefaults.buttonColors( containerColor = Color.White, @@ -109,7 +111,9 @@ private fun OnboardingButtonsContent( Spacer(modifier = Modifier.height(16.dp)) OnboardingButtonSlot( - modifier = Modifier.fillMaxWidth().height(48.dp), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), visible = showLogIn, animateEntrance = animateEntrance, initialOffset = 24.dp, @@ -117,7 +121,9 @@ private fun OnboardingButtonsContent( ) { OutlinedButton( onClick = onLogIn, - modifier = Modifier.fillMaxWidth().height(48.dp), + modifier = Modifier + .fillMaxWidth() + .height(48.dp), shape = RoundedCornerShape(100), border = BorderStroke( width = 1.dp, @@ -138,7 +144,9 @@ private fun OnboardingButtonsContent( Spacer(modifier = Modifier.height(24.dp)) OnboardingButtonSlot( - modifier = Modifier.fillMaxWidth().height(40.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp), visible = showLater, animateEntrance = animateEntrance, initialOffset = 18.dp, From 9150768236aa31c11752b4ab66f0c3efb52d0f5c Mon Sep 17 00:00:00 2001 From: amjiao Date: Fri, 16 Oct 2026 13:45:21 -0400 Subject: [PATCH 18/40] remove onboarding preview screen from commit --- .../OnboardingAnimationPreviewScreen.kt | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt deleted file mode 100644 index a2505d8..0000000 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingAnimationPreviewScreen.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.cornellappdev.score.screen - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -/** - * Preview host for replaying the same onboarding animation used by the real screen. - */ -@Composable -fun OnboardingAnimationPreviewScreen( - onSignUp: () -> Unit, - onLogIn: () -> Unit, - onLater: () -> Unit, - modifier: Modifier = Modifier -) { - var animationKey by remember { mutableIntStateOf(0) } - - Box(modifier = modifier.fillMaxSize()) { - OnboardingScreen( - onSignUp = onSignUp, - onLogIn = onLogIn, - onLater = onLater, - animationKey = animationKey - ) - - Button( - onClick = { animationKey++ }, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 56.dp, end = 16.dp) - ) { - Text("Replay") - } - } -} From 9d5dbc468e4608bc93eb3a4bd92d12ff8c0694a2 Mon Sep 17 00:00:00 2001 From: amjiao Date: Fri, 16 Oct 2026 13:55:43 -0400 Subject: [PATCH 19/40] Clean up OnboardingScreen.kt --- .../score/screen/OnboardingScreen.kt | 356 ++++++++++++++++-- 1 file changed, 319 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt index eb5f254..aeaba8c 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt @@ -70,27 +70,300 @@ private data class SportMotion( ) private val sportMotions = listOf( - SportMotion(R.drawable.ic_basketball, 172f, 237f, 340f, .94f, 5f, 141f, 13.441f, -16.006f, -6.071f, 6.568f, -18f), - SportMotion(R.drawable.ic_baseball, 285f, 299f, 385f, .94f, -108f, 79f, 3.615f, -19.99f, -1.141f, 8.64f, 18f), - SportMotion(R.drawable.ic_cross_country, 20f, 331f, 430f, .94f, 157f, 47f, -13.255f, -18.582f, 5.258f, 8.15f, -18f), - SportMotion(R.drawable.ic_football, 316f, 441f, 475f, .94f, -139f, -63f, 21.069f, -5.821f, -8.856f, 2.903f, 18f), - SportMotion(R.drawable.ic_golf, 176f, 526f, 520f, .94f, 1f, -148f, -14.11f, 16.185f, 6.345f, -6.617f, -18f), - SportMotion(R.drawable.ic_volleyball, 20f, 482f, 565f, .94f, 157f, -104f, -9.539f, 23.112f, 3.586f, -9.932f, 18f), - SportMotion(R.drawable.ic_ice_hockey, 220f, 179f, 580f, .8f, -43f, 199f, 18.414f, -18.933f, -8.093f, 7.624f, -18f), - SportMotion(R.drawable.ic_swim_dive, 259f, 500f, 610f, .94f, -82f, -122f, -2.599f, 21.23f, 1.539f, -9.004f, -18f), - SportMotion(R.drawable.ic_soccer, 251f, 240f, 625f, .8f, -74f, 138f, -4.198f, -21.796f, 2.222f, 9.187f, 18f), - SportMotion(R.drawable.ic_tennis, 334f, 526f, 670f, .8f, -157f, -148f, 7.667f, 26.467f, -2.744f, -11.244f, -18f), - SportMotion(R.drawable.ic_lacrosse, 255f, 582f, 715f, .8f, -78f, -204f, 21.657f, 17.44f, -9.395f, -6.93f, 18f), - SportMotion(R.drawable.ic_wrestling, 94f, 508f, 760f, .8f, 83f, -130f, -20.93f, 6.766f, 9.045f, -2.46f, -18f), - SportMotion(R.drawable.ic_fencing, 23f, 223f, 805f, .8f, 154f, 155f, -26.871f, -7.183f, 11.399f, 2.535f, 18f), - SportMotion(R.drawable.ic_softball, 41f, 575f, 820f, .66f, 136f, -197f, -26.481f, 13.716f, 11.305f, -5.286f, -18f), - SportMotion(R.drawable.ic_sailing, 72f, 263f, 850f, .8f, 105f, 115f, -1.211f, -22.09f, .073f, 9.423f, -18f), - SportMotion(R.drawable.ic_equestrian, 142f, 591f, 865f, .66f, 35f, -213f, 9.965f, 25.7f, -4.642f, -10.607f, 18f), - SportMotion(R.drawable.ic_field_hockey, 334f, 355f, 910f, .66f, -157f, 23f, 19.299f, 11.322f, -7.978f, -5.198f, -18f), - SportMotion(R.drawable.ic_gymnastics, 338f, 257f, 955f, .66f, -161f, 121f, 9.299f, -24.502f, -3.46f, 10.481f, 18f), - SportMotion(R.drawable.ic_squash, 61f, 415f, 1_000f, .66f, 116f, -37f, -17.014f, -9.268f, 7.134f, 4.337f, -18f), - SportMotion(R.drawable.ic_rowing_lightweight, 101f, 191f, 1_045f, .66f, 76f, 187f, -21.33f, -15.299f, 9.256f, 6.043f, 18f), - SportMotion(R.drawable.ic_track_field, 302f, 201f, 1_090f, .66f, -125f, 177f, 25.186f, -11.394f, -10.771f, 4.331f, -18f) + SportMotion( + R.drawable.ic_basketball, + 172f, + 237f, + 340f, + .94f, + 5f, + 141f, + 13.441f, + -16.006f, + -6.071f, + 6.568f, + -18f + ), + SportMotion( + R.drawable.ic_baseball, + 285f, + 299f, + 385f, + .94f, + -108f, + 79f, + 3.615f, + -19.99f, + -1.141f, + 8.64f, + 18f + ), + SportMotion( + R.drawable.ic_cross_country, + 20f, + 331f, + 430f, + .94f, + 157f, + 47f, + -13.255f, + -18.582f, + 5.258f, + 8.15f, + -18f + ), + SportMotion( + R.drawable.ic_football, + 316f, + 441f, + 475f, + .94f, + -139f, + -63f, + 21.069f, + -5.821f, + -8.856f, + 2.903f, + 18f + ), + SportMotion( + R.drawable.ic_golf, + 176f, + 526f, + 520f, + .94f, + 1f, + -148f, + -14.11f, + 16.185f, + 6.345f, + -6.617f, + -18f + ), + SportMotion( + R.drawable.ic_volleyball, + 20f, + 482f, + 565f, + .94f, + 157f, + -104f, + -9.539f, + 23.112f, + 3.586f, + -9.932f, + 18f + ), + SportMotion( + R.drawable.ic_ice_hockey, + 220f, + 179f, + 580f, + .8f, + -43f, + 199f, + 18.414f, + -18.933f, + -8.093f, + 7.624f, + -18f + ), + SportMotion( + R.drawable.ic_swim_dive, + 259f, + 500f, + 610f, + .94f, + -82f, + -122f, + -2.599f, + 21.23f, + 1.539f, + -9.004f, + -18f + ), + SportMotion( + R.drawable.ic_soccer, + 251f, + 240f, + 625f, + .8f, + -74f, + 138f, + -4.198f, + -21.796f, + 2.222f, + 9.187f, + 18f + ), + SportMotion( + R.drawable.ic_tennis, + 334f, + 526f, + 670f, + .8f, + -157f, + -148f, + 7.667f, + 26.467f, + -2.744f, + -11.244f, + -18f + ), + SportMotion( + R.drawable.ic_lacrosse, + 255f, + 582f, + 715f, + .8f, + -78f, + -204f, + 21.657f, + 17.44f, + -9.395f, + -6.93f, + 18f + ), + SportMotion( + R.drawable.ic_wrestling, + 94f, + 508f, + 760f, + .8f, + 83f, + -130f, + -20.93f, + 6.766f, + 9.045f, + -2.46f, + -18f + ), + SportMotion( + R.drawable.ic_fencing, + 23f, + 223f, + 805f, + .8f, + 154f, + 155f, + -26.871f, + -7.183f, + 11.399f, + 2.535f, + 18f + ), + SportMotion( + R.drawable.ic_softball, + 41f, + 575f, + 820f, + .66f, + 136f, + -197f, + -26.481f, + 13.716f, + 11.305f, + -5.286f, + -18f + ), + SportMotion( + R.drawable.ic_sailing, + 72f, + 263f, + 850f, + .8f, + 105f, + 115f, + -1.211f, + -22.09f, + .073f, + 9.423f, + -18f + ), + SportMotion( + R.drawable.ic_equestrian, + 142f, + 591f, + 865f, + .66f, + 35f, + -213f, + 9.965f, + 25.7f, + -4.642f, + -10.607f, + 18f + ), + SportMotion( + R.drawable.ic_field_hockey, + 334f, + 355f, + 910f, + .66f, + -157f, + 23f, + 19.299f, + 11.322f, + -7.978f, + -5.198f, + -18f + ), + SportMotion( + R.drawable.ic_gymnastics, + 338f, + 257f, + 955f, + .66f, + -161f, + 121f, + 9.299f, + -24.502f, + -3.46f, + 10.481f, + 18f + ), + SportMotion( + R.drawable.ic_squash, + 61f, + 415f, + 1_000f, + .66f, + 116f, + -37f, + -17.014f, + -9.268f, + 7.134f, + 4.337f, + -18f + ), + SportMotion( + R.drawable.ic_rowing_lightweight, + 101f, + 191f, + 1_045f, + .66f, + 76f, + 187f, + -21.33f, + -15.299f, + 9.256f, + 6.043f, + 18f + ), + SportMotion( + R.drawable.ic_track_field, + 302f, + 201f, + 1_090f, + .66f, + -125f, + 177f, + 25.186f, + -11.394f, + -10.771f, + 4.331f, + -18f + ) ) @Composable @@ -101,25 +374,26 @@ fun OnboardingScreen( modifier: Modifier = Modifier, animationKey: Int = 0 ) { - val playhead = remember(animationKey) { Animatable(0f) } + val animationTimeMs = remember(animationKey) { Animatable(0f) } LaunchedEffect(animationKey) { - playhead.snapTo(0f) - playhead.animateTo( + animationTimeMs.snapTo(0f) + animationTimeMs.animateTo( TIMELINE_DURATION_MS.toFloat(), animationSpec = tween(TIMELINE_DURATION_MS, easing = LinearEasing) ) } - val timeMs = playhead.value + val timeMs = animationTimeMs.value BoxWithConstraints( modifier = modifier .fillMaxSize() .background(CrimsonPrimary) ) { + /*designScale and designDp scale from Figma units to Compose units*/ val designScale = min(maxWidth.value / 393f, maxHeight.value / 852f) val designDp: (Float) -> Dp = { (it * designScale).dp } - FigmaSpeaker( + AnimatedSpeaker( timeMs = timeMs, left = 98f, top = 229f, @@ -127,7 +401,11 @@ fun OnboardingScreen( designDp = designDp, alphaKeys = listOf(MotionKey(0f, 0f), MotionKey(2_760f, 0f), MotionKey(3_280f, 1f)), scaleKeys = listOf(MotionKey(0f, .67f), MotionKey(2_760f, .67f), MotionKey(3_280f, 1f)), - yKeys = listOf(MotionKey(0f, 68.5f), MotionKey(2_760f, 68.5f), MotionKey(3_280f, 0f, FastOutSlowInEasing)) + yKeys = listOf( + MotionKey(0f, 68.5f), + MotionKey(2_760f, 68.5f), + MotionKey(3_280f, 0f, FastOutSlowInEasing) + ) ) if (timeMs >= 3_020f) { @@ -141,11 +419,11 @@ fun OnboardingScreen( ) } - FigmaHalo( + AnimatedHalo( timeMs = timeMs, designDp = designDp ) - FigmaRing( + AnimatedRing( timeMs = timeMs, designDp = designDp, delayMs = 240f, @@ -157,7 +435,7 @@ fun OnboardingScreen( scalePeakTime = 1_020f, scaleExit = 2.092f ) - FigmaRing( + AnimatedRing( timeMs = timeMs, designDp = designDp, delayMs = 420f, @@ -170,7 +448,7 @@ fun OnboardingScreen( scaleExit = 2.322f ) - FigmaSpeaker( + AnimatedSpeaker( timeMs = timeMs, left = 129f, top = 330f, @@ -207,13 +485,13 @@ fun OnboardingScreen( ) sportMotions.forEach { sport -> - FigmaSportIcon(timeMs = timeMs, motion = sport, designDp = designDp) + AnimatedSportIcon(timeMs = timeMs, motion = sport, designDp = designDp) } } } @Composable -private fun FigmaSpeaker( +private fun AnimatedSpeaker( timeMs: Float, left: Float, top: Float, @@ -252,7 +530,7 @@ private fun FigmaSpeaker( } @Composable -private fun FigmaHalo(timeMs: Float, designDp: (Float) -> Dp) { +private fun AnimatedHalo(timeMs: Float, designDp: (Float) -> Dp) { val alpha = sampleTrack( timeMs, listOf( @@ -292,7 +570,7 @@ private fun FigmaHalo(timeMs: Float, designDp: (Float) -> Dp) { } @Composable -private fun FigmaRing( +private fun AnimatedRing( timeMs: Float, designDp: (Float) -> Dp, delayMs: Float, @@ -344,7 +622,7 @@ private fun FigmaRing( } @Composable -private fun FigmaSportIcon( +private fun AnimatedSportIcon( timeMs: Float, motion: SportMotion, designDp: (Float) -> Dp @@ -424,7 +702,11 @@ private fun FigmaSportIcon( MotionKey(wiggleTime, motion.initialRotation * .22f, FastOutSlowInEasing), MotionKey(settledTime, 0f, FastOutSlowInEasing), MotionKey(ICONS_HOLD_END_MS.toFloat(), 0f), - MotionKey(ICONS_EXIT_END_MS.toFloat(), motion.initialRotation * .55f, FastOutLinearInEasing) + MotionKey( + ICONS_EXIT_END_MS.toFloat(), + motion.initialRotation * .55f, + FastOutLinearInEasing + ) ) ) From e075ff8a7780f8a80cc09d4f8f48e846f66c690e Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 18:31:07 -0400 Subject: [PATCH 20/40] fix nav padding per coderabbit comment --- .../java/com/cornellappdev/score/screen/OnboardingScreen.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt index aeaba8c..368a0f6 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt @@ -12,10 +12,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -415,6 +418,7 @@ fun OnboardingScreen( onLater = onLater, modifier = Modifier .align(Alignment.BottomCenter) + .windowInsetsPadding(WindowInsets.navigationBars) .padding(start = 24.dp, end = 24.dp, bottom = 40.dp) ) } From 08ebb0b49db6e6bcfda4db0e019098212a697c56 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 18:47:34 -0400 Subject: [PATCH 21/40] make common onboarding screen origin per coderabbit review --- .../score/screen/OnboardingScreen.kt | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt index 368a0f6..b92aa85 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingScreen.kt @@ -714,27 +714,33 @@ private fun AnimatedSportIcon( ) ) - Box( - modifier = Modifier - .offset(x = designDp(motion.left), y = designDp(motion.top)) - .size(designDp(36f)), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(motion.drawable), - contentDescription = null, - colorFilter = ColorFilter.tint(Color.White), + Box(modifier = Modifier.fillMaxSize()) { + Box( modifier = Modifier - .size(36.dp) - .graphicsLayer { - this.alpha = alpha.coerceIn(0f, 1f) - scaleX = scale - scaleY = scale - rotationZ = rotation - translationX = designDp(x).toPx() - translationY = designDp(y).toPx() - } - ) + .align(Alignment.Center) + .offset( + x = designDp(motion.left + 18f - 196.5f), + y = designDp(motion.top + 18f - 426f) + ) + .size(designDp(36f)), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(motion.drawable), + contentDescription = null, + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(designDp(36f)) + .graphicsLayer { + this.alpha = alpha.coerceIn(0f, 1f) + scaleX = scale + scaleY = scale + rotationZ = rotation + translationX = designDp(x).toPx() + translationY = designDp(y).toPx() + } + ) + } } } From d580f98b3fa9c37ad7f0fdd0a1aaba9053c29252 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 18:42:48 -0400 Subject: [PATCH 22/40] Onboarding profile picture selection --- .../com/cornellappdev/score/MainActivity.kt | 27 ++- .../score/screen/EditProfileScreen.kt | 40 +---- .../score/screen/OnboardingProfileScreen.kt | 170 ++++++++++++++++++ .../score/screen/ProfilePictureOptions.kt | 87 +++++++++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt create mode 100644 app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 512793e..f1153cd 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -5,11 +5,12 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.OnboardingProfileScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics @@ -18,6 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" +private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -31,8 +33,8 @@ class MainActivity : AppCompatActivity() { val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) setContent { - var showOnboarding by rememberSaveable { - mutableStateOf(!onboardingCompleted) + var onboardingStep by rememberSaveable { + mutableIntStateOf(if (onboardingCompleted) 2 else 0) } val completeOnboarding = remember { @@ -40,18 +42,25 @@ class MainActivity : AppCompatActivity() { preferences.edit() .putBoolean(KEY_ONBOARDING_COMPLETED, true) .apply() - showOnboarding = false + onboardingStep = 2 } } - if (showOnboarding) { - OnboardingScreen( - onSignUp = completeOnboarding, + when (onboardingStep) { + 0 -> OnboardingScreen( + onSignUp = { onboardingStep = 1 }, onLogIn = completeOnboarding, onLater = completeOnboarding ) - } else { - RootNavigation() + 1 -> OnboardingProfileScreen( + onNext = { selectedPicture -> + preferences.edit() + .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) + .apply() + completeOnboarding() + } + ) + else -> RootNavigation() } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt index f81b4b0..4c93ff0 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/EditProfileScreen.kt @@ -4,23 +4,14 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -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 -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,11 +21,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.R @@ -89,31 +78,10 @@ fun EditProfileScreen( modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(Modifier.height(24.dp)) - Row( - modifier = Modifier.widthIn(max = 393.dp).fillMaxWidth() - .padding(horizontal = 24.dp).selectableGroup(), - horizontalArrangement = Arrangement.spacedBy(20.dp) - ) { - ProfilePicture.entries.forEach { picture -> - val selected = picture == selectedPicture - Box( - modifier = Modifier.weight(1f).aspectRatio(1f).clip(CircleShape) - .selectable(selected = selected, role = Role.RadioButton, - onClick = { onPictureSelected(picture) }), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(picture.drawable), - contentDescription = stringResource(picture.label), - modifier = Modifier.fillMaxSize() - ) - if (selected) { - Box(Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF4DBB35), CircleShape)) - } - } - } - } + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = onPictureSelected + ) } } } diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt new file mode 100644 index 0000000..a9062a7 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -0,0 +1,170 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.R +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.Style + +@Composable +fun OnboardingProfileScreen( + onNext: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier, + displayName: String? = null +) { + var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.TENNIS) } + val name = displayName ?: stringResource(R.string.onboarding_default_name) + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.edit_profile), + style = Style.heading1, + color = Color(0xFF333333) + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.onboarding_profile_subtitle), + style = Style.bodyMedium, + color = Color(0xFF333333) + ) + + Spacer(Modifier.height(designDp(47f))) + Image( + painter = painterResource(selectedPicture.drawable), + contentDescription = stringResource( + R.string.selected_profile_picture, + stringResource(selectedPicture.label) + ), + modifier = Modifier.size(designDp(100f)) + ) + Spacer(Modifier.height(designDp(16f))) + Text( + text = name, + style = Style.bodyMedium, + color = Color.Black + ) + + Spacer(Modifier.height(designDp(60f))) + Text( + text = stringResource(R.string.onboarding_select_profile_picture), + style = Style.metricMedium, + color = Color.Black, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + Spacer(Modifier.height(designDp(24f))) + ProfilePictureOptions( + selectedPicture = selectedPicture, + onPictureSelected = { selectedPicture = it }, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(Modifier.weight(1f)) + OnboardingProfileFooter( + onNext = { onNext(selectedPicture) }, + designDp = designDp + ) + } + } +} + +@Composable +private fun OnboardingProfileFooter( + onNext: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onNext, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = Color.White + ) + ) { + Text( + text = stringResource(R.string.next), + style = Style.heading3, + color = Color.White + ) + } + + Spacer(Modifier.height(designDp(16f))) + RowProgressDots() + } +} + +@Composable +private fun RowProgressDots() { + androidx.compose.foundation.layout.Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun OnboardingProfileScreenPreview() { + OnboardingProfileScreen(onNext = {}) +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt new file mode 100644 index 0000000..ed5fcc2 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -0,0 +1,87 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.components.ScorePreview + +/** + * Shared profile picture choices used by profile editing and first-time onboarding. + */ +@Composable +fun ProfilePictureOptions( + selectedPicture: ProfilePicture, + onPictureSelected: (ProfilePicture) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .widthIn(max = 393.dp) + .fillMaxWidth() + .padding(horizontal = 24.dp) + .selectableGroup(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + ProfilePicture.entries.forEach { picture -> + val selected = picture == selectedPicture + Box( + modifier = Modifier + .size(70.dp) + .clip(CircleShape) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onPictureSelected(picture) } + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(picture.drawable), + contentDescription = stringResource(picture.label), + modifier = Modifier.fillMaxSize() + ) + if (selected) { + Box( + Modifier + .fillMaxSize() + .background(Color.White.copy(alpha = 0.6f), CircleShape) + .border(3.dp, Color(0xFF55B63C), CircleShape) + ) + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 393) +@Composable +private fun ProfilePictureOptionsPreview() { + ScorePreview(backgroundColor = Color.White) { + ProfilePictureOptions( + selectedPicture = ProfilePicture.TENNIS, + onPictureSelected = {}, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c264cc3..474c141 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,4 +7,8 @@ Tennis Baseball Soccer + Claire Yu + You can change this later in the profile page. + Select profile picture: + Next \ No newline at end of file From 3ebccad04b267cd912a82921f445a0c0a6c42f0c Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 22:53:12 -0400 Subject: [PATCH 23/40] Make screen name more informative --- app/src/main/java/com/cornellappdev/score/MainActivity.kt | 4 ++-- .../cornellappdev/score/screen/OnboardingProfileScreen.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index f1153cd..aaf0da5 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -10,7 +10,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.cornellappdev.score.nav.root.RootNavigation -import com.cornellappdev.score.screen.OnboardingProfileScreen +import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase import com.google.firebase.analytics.FirebaseAnalytics @@ -52,7 +52,7 @@ class MainActivity : AppCompatActivity() { onLogIn = completeOnboarding, onLater = completeOnboarding ) - 1 -> OnboardingProfileScreen( + 1 -> OnboardingProfileSelectScreen( onNext = { selectedPicture -> preferences.edit() .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt index a9062a7..1dfa4a7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt @@ -35,7 +35,7 @@ import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.Style @Composable -fun OnboardingProfileScreen( +fun OnboardingProfileSelectScreen( onNext: (ProfilePicture) -> Unit, modifier: Modifier = Modifier, displayName: String? = null @@ -165,6 +165,6 @@ private fun RowProgressDots() { @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun OnboardingProfileScreenPreview() { - OnboardingProfileScreen(onNext = {}) +private fun OnboardingProfileSelectScreenPreview() { + OnboardingProfileSelectScreen(onNext = {}) } From 62db4ebcaf1dc29f5b273d9a8bfb013b9555d0da Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:06:53 -0400 Subject: [PATCH 24/40] Rename file --- ...nboardingProfileScreen.kt => OnboardingProfileSelectScreen.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/cornellappdev/score/screen/{OnboardingProfileScreen.kt => OnboardingProfileSelectScreen.kt} (100%) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt similarity index 100% rename from app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileScreen.kt rename to app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt From 247fd88b9fea9015692f8e3db1c5c809f05a7806 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 11:32:48 -0400 Subject: [PATCH 25/40] Fix profile selector defaults --- .../cornellappdev/score/screen/OnboardingProfileSelectScreen.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 1dfa4a7..9a72ffd 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -40,7 +40,7 @@ fun OnboardingProfileSelectScreen( modifier: Modifier = Modifier, displayName: String? = null ) { - var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.TENNIS) } + var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.BLANK) } val name = displayName ?: stringResource(R.string.onboarding_default_name) BoxWithConstraints( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 474c141..9ef2641 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ Tennis Baseball Soccer - Claire Yu + Your Name You can change this later in the profile page. Select profile picture: Next From 39a605e4a92c39a43d2f17756d15db9802139227 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 11:38:07 -0400 Subject: [PATCH 26/40] Change onboarding state representation from int to enum --- .../com/cornellappdev/score/MainActivity.kt | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index aaf0da5..bed9942 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -5,10 +5,11 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.core.content.edit import com.cornellappdev.score.nav.root.RootNavigation import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen @@ -23,6 +24,12 @@ private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" private lateinit var firebaseAnalytics: FirebaseAnalytics +private enum class OnboardingStep { + WELCOME, + PROFILE_SELECT, + COMPLETE +} + @AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -34,33 +41,35 @@ class MainActivity : AppCompatActivity() { setContent { var onboardingStep by rememberSaveable { - mutableIntStateOf(if (onboardingCompleted) 2 else 0) + mutableStateOf(if (onboardingCompleted) OnboardingStep.COMPLETE else OnboardingStep.WELCOME) } val completeOnboarding = remember { { - preferences.edit() - .putBoolean(KEY_ONBOARDING_COMPLETED, true) - .apply() - onboardingStep = 2 + preferences.edit { + putBoolean(KEY_ONBOARDING_COMPLETED, true) + } + onboardingStep = OnboardingStep.COMPLETE } } when (onboardingStep) { - 0 -> OnboardingScreen( - onSignUp = { onboardingStep = 1 }, + OnboardingStep.WELCOME -> OnboardingScreen( + onSignUp = { onboardingStep = OnboardingStep.PROFILE_SELECT }, onLogIn = completeOnboarding, onLater = completeOnboarding ) - 1 -> OnboardingProfileSelectScreen( + + OnboardingStep.PROFILE_SELECT -> OnboardingProfileSelectScreen( onNext = { selectedPicture -> - preferences.edit() - .putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) - .apply() + preferences.edit { + putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) + } completeOnboarding() } ) - else -> RootNavigation() + + OnboardingStep.COMPLETE -> RootNavigation() } } From dda86eb4079c364d06b77a3b41a679cf56349708 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:00:31 -0400 Subject: [PATCH 27/40] Change manual scaling to adaptive --- .../screen/OnboardingProfileSelectScreen.kt | 36 +++++++++---------- .../score/screen/ProfilePictureOptions.kt | 6 ++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 9a72ffd..30d0223 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable @@ -43,19 +44,16 @@ fun OnboardingProfileSelectScreen( var selectedPicture by rememberSaveable { mutableStateOf(ProfilePicture.BLANK) } val name = displayName ?: stringResource(R.string.onboarding_default_name) - BoxWithConstraints( + Box( modifier = modifier .fillMaxSize() .background(Color.White) ) { - val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) - val designDp: (Float) -> Dp = { (it * designScale).dp } - Column(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxWidth() - .padding(top = designDp(79f)), + .padding(top = 80.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -63,30 +61,30 @@ fun OnboardingProfileSelectScreen( style = Style.heading1, color = Color(0xFF333333) ) - Spacer(Modifier.height(designDp(4f))) + Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.onboarding_profile_subtitle), style = Style.bodyMedium, color = Color(0xFF333333) ) - Spacer(Modifier.height(designDp(47f))) + Spacer(Modifier.height(40.dp)) Image( painter = painterResource(selectedPicture.drawable), contentDescription = stringResource( R.string.selected_profile_picture, stringResource(selectedPicture.label) ), - modifier = Modifier.size(designDp(100f)) + modifier = Modifier.size(100.dp) ) - Spacer(Modifier.height(designDp(16f))) + Spacer(Modifier.height(16.dp)) Text( text = name, style = Style.bodyMedium, color = Color.Black ) - Spacer(Modifier.height(designDp(60f))) + Spacer(Modifier.height(60.dp)) Text( text = stringResource(R.string.onboarding_select_profile_picture), style = Style.metricMedium, @@ -96,7 +94,7 @@ fun OnboardingProfileSelectScreen( .fillMaxWidth() .padding(horizontal = 24.dp) ) - Spacer(Modifier.height(designDp(24f))) + Spacer(Modifier.height(24.dp)) ProfilePictureOptions( selectedPicture = selectedPicture, onPictureSelected = { selectedPicture = it }, @@ -106,8 +104,7 @@ fun OnboardingProfileSelectScreen( Spacer(Modifier.weight(1f)) OnboardingProfileFooter( - onNext = { onNext(selectedPicture) }, - designDp = designDp + onNext = { onNext(selectedPicture) } ) } } @@ -116,7 +113,6 @@ fun OnboardingProfileSelectScreen( @Composable private fun OnboardingProfileFooter( onNext: () -> Unit, - designDp: (Float) -> Dp ) { Column( modifier = Modifier @@ -124,8 +120,8 @@ private fun OnboardingProfileFooter( .padding( start = 24.dp, end = 24.dp, - top = designDp(24f), - bottom = designDp(60f) + top = 24.dp, + bottom = 60.dp ), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -133,7 +129,7 @@ private fun OnboardingProfileFooter( onClick = onNext, modifier = Modifier .fillMaxWidth() - .height(designDp(48f)), + .height(48.dp), shape = CircleShape, colors = ButtonDefaults.buttonColors( containerColor = CrimsonPrimary, @@ -147,7 +143,7 @@ private fun OnboardingProfileFooter( ) } - Spacer(Modifier.height(designDp(16f))) + Spacer(Modifier.height(16.dp)) RowProgressDots() } } @@ -159,11 +155,11 @@ private fun RowProgressDots() { verticalAlignment = Alignment.CenterVertically ) { Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(8.dp).background(Color(0xFFEAEAEA), CircleShape)) + Box(Modifier.size(8.dp).background(GrayStroke, CircleShape)) } } -@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Preview(showBackground = true) @Composable private fun OnboardingProfileSelectScreenPreview() { OnboardingProfileSelectScreen(onNext = {}) diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt index ed5fcc2..2caa255 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.theme.saturatedGreen /** * Shared profile picture choices used by profile editing and first-time onboarding. @@ -37,7 +38,6 @@ fun ProfilePictureOptions( ) { Row( modifier = modifier - .widthIn(max = 393.dp) .fillMaxWidth() .padding(horizontal = 24.dp) .selectableGroup(), @@ -66,7 +66,7 @@ fun ProfilePictureOptions( Modifier .fillMaxSize() .background(Color.White.copy(alpha = 0.6f), CircleShape) - .border(3.dp, Color(0xFF55B63C), CircleShape) + .border(3.dp, saturatedGreen, CircleShape) ) } } @@ -74,7 +74,7 @@ fun ProfilePictureOptions( } } -@Preview(showBackground = true, widthDp = 393) +@Preview(showBackground = true) @Composable private fun ProfilePictureOptionsPreview() { ScorePreview(backgroundColor = Color.White) { From dc3f8a9690fd15cd869b095dab21a310ce948014 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 19:12:18 -0400 Subject: [PATCH 28/40] first draft onboarding profile screen # Conflicts: # app/src/main/java/com/cornellappdev/score/MainActivity.kt # Conflicts: # app/src/main/java/com/cornellappdev/score/MainActivity.kt # app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt --- .../com/cornellappdev/score/MainActivity.kt | 5 + .../score/screen/FavoriteSportsScreen.kt | 420 ++++++++++++++++++ .../screen/OnboardingProfileSelectScreen.kt | 7 +- .../com/cornellappdev/score/theme/Color.kt | 1 + app/src/main/res/values/strings.xml | 9 + 5 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index bed9942..31a1c25 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -21,6 +21,7 @@ import dagger.hilt.android.AndroidEntryPoint private const val APP_PREFERENCES = "score_app_preferences" private const val KEY_ONBOARDING_COMPLETED = "onboarding_completed" private const val KEY_ONBOARDING_PROFILE_PICTURE = "onboarding_profile_picture" +private const val KEY_ONBOARDING_FAVORITE_SPORTS = "onboarding_favorite_sports" private lateinit var firebaseAnalytics: FirebaseAnalytics @@ -38,6 +39,10 @@ class MainActivity : AppCompatActivity() { val preferences = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE) val onboardingCompleted = preferences.getBoolean(KEY_ONBOARDING_COMPLETED, false) + val savedFavoriteSportKeys = preferences + .getStringSet(KEY_ONBOARDING_FAVORITE_SPORTS, emptySet()) + .orEmpty() + .joinToString("|") setContent { var onboardingStep by rememberSaveable { diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt new file mode 100644 index 0000000..6696eee --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt @@ -0,0 +1,420 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.GreenWash +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White +import com.cornellappdev.score.theme.saturatedGreen +import com.cornellappdev.score.components.ScorePreview + +data class FavoriteSport( + val sport: Sport, + val gender: GenderDivision +) { + val storageKey: String get() = "${sport.name}:${gender.name}" + + companion object { + fun fromStorageKey(key: String): FavoriteSport? { + val parts = key.split(':', limit = 2) + if (parts.size != 2) return null + val sport = runCatching { Sport.valueOf(parts[0]) }.getOrNull() ?: return null + val gender = runCatching { GenderDivision.valueOf(parts[1]) }.getOrNull() + ?: return null + if (gender == GenderDivision.ALL) return null + return FavoriteSport(sport, gender) + } + } +} + +@Composable +fun FavoriteSportsScreen( + onFinish: (Set) -> Unit, + modifier: Modifier = Modifier, + initiallySelectedKeys: String = "" +) { + var selectedGender by rememberSaveable { mutableStateOf(GenderDivision.ALL) } + var selectedKeys by rememberSaveable { + mutableStateOf(initiallySelectedKeys) + } + val selectedSports = selectedKeys.split('|').filter(String::isNotBlank).toSet() + val allOptions = rememberFavoriteSportOptions() + val visibleOptions = when (selectedGender) { + GenderDivision.ALL -> allOptions + else -> allOptions.filter { it.gender == selectedGender } + } + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.select_favorite_sports), + style = Style.heading1, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.favorite_sports_subtitle), + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(32f))) + FavoriteSportsGenderFilter( + selectedGender = selectedGender, + onGenderSelected = { selectedGender = it }, + designDp = designDp + ) + } + + Spacer(Modifier.height(designDp(45f))) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues( + start = 24.dp, + end = 24.dp, + bottom = designDp(8f) + ), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items(visibleOptions, key = { it.storageKey }) { option -> + FavoriteSportCard( + option = option, + selected = option.storageKey in selectedSports, + onClick = { + selectedKeys = if (option.storageKey in selectedSports) { + selectedSports - option.storageKey + } else { + selectedSports + option.storageKey + }.joinToString("|") + }, + designDp = designDp + ) + } + } + + FavoriteSportsFooter( + selectedCount = selectedSports.size, + onFinish = { + onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) + }, + designDp = designDp + ) + } + } +} + +@Composable +private fun rememberFavoriteSportOptions(): List { + return remember { + Sport.entries + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } + } + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) + } +} + +@Composable +private fun FavoriteSportsGenderFilter( + selectedGender: GenderDivision, + onGenderSelected: (GenderDivision) -> Unit, + designDp: (Float) -> Dp +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .height(designDp(48f)) + .border( + BorderStroke(1.dp, GrayLight), + CircleShape + ) + .padding(designDp(5f)) + .selectableGroup(), + verticalAlignment = Alignment.CenterVertically + ) { + listOf( + GenderDivision.ALL to R.string.all, + GenderDivision.MALE to R.string.onboarding_mens, + GenderDivision.FEMALE to R.string.onboarding_womens + ).forEach { (gender, label) -> + val selected = gender == selectedGender + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .clip(CircleShape) + .background(if (selected) CrimsonPrimary else Color.Transparent) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onGenderSelected(gender) } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(label), + style = if (selected) Style.bodySemibold else Style.bodyMedium, + color = if (selected) White else GrayMedium + ) + } + } + } +} + +@Composable +private fun FavoriteSportCard( + option: FavoriteSport, + selected: Boolean, + onClick: () -> Unit, + designDp: (Float) -> Dp +) { + val cardShape = RoundedCornerShape(designDp(20f)) + Column( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selected, + role = Role.Checkbox, + onClick = onClick + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(cardShape) + .background(if (selected) GreenWash else GrayStroke) + .border( + width = if (selected) designDp(3f) else designDp(1f), + color = if (selected) saturatedGreen else GrayStroke, + shape = cardShape + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(option.sport.emptyIcon), + contentDescription = null, + modifier = Modifier.size(designDp(45f)), + colorFilter = ColorFilter.tint( + if (selected) saturatedGreen else GrayPrimary + ) + ) + } + + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = designDp(-8f), y = designDp(-8f)) + .size(designDp(32f)) + .background(CrimsonPrimary, CircleShape), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(R.drawable.ic_round_minus), + contentDescription = stringResource(R.string.remove_favorite_sport), + modifier = Modifier.size(designDp(24f)), + colorFilter = ColorFilter.tint(White) + ) + } + } + } + + Spacer(Modifier.height(designDp(8f))) + Text( + text = option.sport.displayName, + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(designDp(2f))) + FavoriteSportGenderTag(option.gender, designDp) + } +} + +@Composable +private fun FavoriteSportGenderTag( + gender: GenderDivision, + designDp: (Float) -> Dp +) { + val label = if (gender == GenderDivision.FEMALE) "W" else "M" + val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + + Row( + modifier = Modifier + .background(White, RoundedCornerShape(100)) + .padding(horizontal = designDp(2f)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + ) { + Box( + modifier = Modifier + .size(designDp(18f)) + .background(GrayStroke, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol, + color = GrayMedium, + fontSize = 10.sp, + lineHeight = 10.sp + ) + } + Text( + text = label, + style = Style.labelsNormal, + color = GrayPrimary + ) + } +} + +@Composable +private fun FavoriteSportsFooter( + selectedCount: Int, + onFinish: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onFinish, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + val buttonText = when (selectedCount) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedCount) + } + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(designDp(16f))) + Row( + horizontalArrangement = Arrangement.spacedBy(designDp(4f)), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + } + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsScreenPreview() { + ScorePreview { + FavoriteSportsScreen(onFinish = {}) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsSelectedScreenPreview() { + ScorePreview { + FavoriteSportsScreen( + onFinish = {}, + initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 30d0223..5cd9c70 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -32,6 +31,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @@ -59,13 +60,13 @@ fun OnboardingProfileSelectScreen( Text( text = stringResource(R.string.edit_profile), style = Style.heading1, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.onboarding_profile_subtitle), style = Style.bodyMedium, - color = Color(0xFF333333) + color = GrayPrimary ) Spacer(Modifier.height(40.dp)) diff --git a/app/src/main/java/com/cornellappdev/score/theme/Color.kt b/app/src/main/java/com/cornellappdev/score/theme/Color.kt index a4f8fa5..799aaee 100644 --- a/app/src/main/java/com/cornellappdev/score/theme/Color.kt +++ b/app/src/main/java/com/cornellappdev/score/theme/Color.kt @@ -11,6 +11,7 @@ val GrayStroke = Color(0xFFEAEAEA) val GreenAccent = Color(0xFF247211) val RedAccent = Color(0xFFC41616) val GreenLight = Color(0xFF88D598) +val GreenWash = Color(0xFFF6FFF3) val BlueLight = Color(0XFF5AA8D5) val CrimsonPrimary = Color(0xFFA5210D) val PeachPrimary = Color(0xFFE1A69F) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ef2641..75e707e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,4 +11,13 @@ You can change this later in the profile page. Select profile picture: Next + Select Favorite Sports + You can change your picks at any time! + All + Men’s + Women’s + Finish + Finish with %1$d sports + Finish with 1 sport + Remove favorite sport \ No newline at end of file From d520726dd37531a62d0121e89e2060182ad76433 Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:05:56 -0400 Subject: [PATCH 29/40] Make name more informative --- .../main/java/com/cornellappdev/score/MainActivity.kt | 2 ++ ...ortsScreen.kt => OnboardingFavoriteSportsScreen.kt} | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) rename app/src/main/java/com/cornellappdev/score/screen/{FavoriteSportsScreen.kt => OnboardingFavoriteSportsScreen.kt} (98%) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 31a1c25..483fcd6 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -11,6 +11,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.content.edit import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.FavoriteSport +import com.cornellappdev.score.screen.OnboardingFavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt similarity index 98% rename from app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt rename to app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index 6696eee..d11b4d7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -81,7 +81,7 @@ data class FavoriteSport( } @Composable -fun FavoriteSportsScreen( +fun OnboardingFavoriteSportsScreen( onFinish: (Set) -> Unit, modifier: Modifier = Modifier, initiallySelectedKeys: String = "" @@ -402,17 +402,17 @@ private fun FavoriteSportsFooter( @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsScreenPreview() { +private fun OnboardingFavoriteSportsScreenPreview() { ScorePreview { - FavoriteSportsScreen(onFinish = {}) + OnboardingFavoriteSportsScreen(onFinish = {}) } } @Preview(showBackground = true, widthDp = 393, heightDp = 852) @Composable -private fun FavoriteSportsSelectedScreenPreview() { +private fun OnboardingFavoriteSportsSelectedScreenPreview() { ScorePreview { - FavoriteSportsScreen( + OnboardingFavoriteSportsScreen( onFinish = {}, initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" ) From 2e36a6a5995f61ce51169b15a2d3837379b84faa Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 17:22:48 -0400 Subject: [PATCH 30/40] fix page dot indicator --- .../screen/OnboardingFavoriteSportsScreen.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index d11b4d7..0e16a6e 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -25,10 +25,13 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -358,6 +361,11 @@ private fun FavoriteSportsFooter( onFinish: () -> Unit, designDp: (Float) -> Dp ) { + var showActiveProgressDot by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showActiveProgressDot = true + } + Column( modifier = Modifier .fillMaxWidth() @@ -394,8 +402,13 @@ private fun FavoriteSportsFooter( horizontalArrangement = Arrangement.spacedBy(designDp(4f)), verticalAlignment = Alignment.CenterVertically ) { - Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + val activeColor by animateColorAsState( + targetValue = if (showActiveProgressDot) CrimsonPrimary else GrayStroke, + animationSpec = tween(durationMillis = 250), + label = "favoriteSportsActiveDot" + ) Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) } } } From 70bfaba534b4b14c73658507352a46c62b0dc449 Mon Sep 17 00:00:00 2001 From: amjiao Date: Sun, 18 Oct 2026 19:12:18 -0400 Subject: [PATCH 31/40] first draft onboarding profile screen # Conflicts: # app/src/main/java/com/cornellappdev/score/MainActivity.kt --- .../com/cornellappdev/score/MainActivity.kt | 2 - .../score/screen/FavoriteSportsScreen.kt | 420 ++++++++++++++++++ 2 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 483fcd6..31a1c25 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -11,8 +11,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.content.edit import com.cornellappdev.score.nav.root.RootNavigation -import com.cornellappdev.score.screen.FavoriteSport -import com.cornellappdev.score.screen.OnboardingFavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt new file mode 100644 index 0000000..6696eee --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt @@ -0,0 +1,420 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +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.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cornellappdev.score.R +import com.cornellappdev.score.model.GenderDivision +import com.cornellappdev.score.model.Sport +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayLight +import com.cornellappdev.score.theme.GrayMedium +import com.cornellappdev.score.theme.GrayPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.GreenWash +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White +import com.cornellappdev.score.theme.saturatedGreen +import com.cornellappdev.score.components.ScorePreview + +data class FavoriteSport( + val sport: Sport, + val gender: GenderDivision +) { + val storageKey: String get() = "${sport.name}:${gender.name}" + + companion object { + fun fromStorageKey(key: String): FavoriteSport? { + val parts = key.split(':', limit = 2) + if (parts.size != 2) return null + val sport = runCatching { Sport.valueOf(parts[0]) }.getOrNull() ?: return null + val gender = runCatching { GenderDivision.valueOf(parts[1]) }.getOrNull() + ?: return null + if (gender == GenderDivision.ALL) return null + return FavoriteSport(sport, gender) + } + } +} + +@Composable +fun FavoriteSportsScreen( + onFinish: (Set) -> Unit, + modifier: Modifier = Modifier, + initiallySelectedKeys: String = "" +) { + var selectedGender by rememberSaveable { mutableStateOf(GenderDivision.ALL) } + var selectedKeys by rememberSaveable { + mutableStateOf(initiallySelectedKeys) + } + val selectedSports = selectedKeys.split('|').filter(String::isNotBlank).toSet() + val allOptions = rememberFavoriteSportOptions() + val visibleOptions = when (selectedGender) { + GenderDivision.ALL -> allOptions + else -> allOptions.filter { it.gender == selectedGender } + } + + BoxWithConstraints( + modifier = modifier + .fillMaxSize() + .background(White) + ) { + val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) + val designDp: (Float) -> Dp = { (it * designScale).dp } + + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = designDp(79f)), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.select_favorite_sports), + style = Style.heading1, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(4f))) + Text( + text = stringResource(R.string.favorite_sports_subtitle), + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center + ) + Spacer(Modifier.height(designDp(32f))) + FavoriteSportsGenderFilter( + selectedGender = selectedGender, + onGenderSelected = { selectedGender = it }, + designDp = designDp + ) + } + + Spacer(Modifier.height(designDp(45f))) + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues( + start = 24.dp, + end = 24.dp, + bottom = designDp(8f) + ), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items(visibleOptions, key = { it.storageKey }) { option -> + FavoriteSportCard( + option = option, + selected = option.storageKey in selectedSports, + onClick = { + selectedKeys = if (option.storageKey in selectedSports) { + selectedSports - option.storageKey + } else { + selectedSports + option.storageKey + }.joinToString("|") + }, + designDp = designDp + ) + } + } + + FavoriteSportsFooter( + selectedCount = selectedSports.size, + onFinish = { + onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) + }, + designDp = designDp + ) + } + } +} + +@Composable +private fun rememberFavoriteSportOptions(): List { + return remember { + Sport.entries + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } + } + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) + } +} + +@Composable +private fun FavoriteSportsGenderFilter( + selectedGender: GenderDivision, + onGenderSelected: (GenderDivision) -> Unit, + designDp: (Float) -> Dp +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .height(designDp(48f)) + .border( + BorderStroke(1.dp, GrayLight), + CircleShape + ) + .padding(designDp(5f)) + .selectableGroup(), + verticalAlignment = Alignment.CenterVertically + ) { + listOf( + GenderDivision.ALL to R.string.all, + GenderDivision.MALE to R.string.onboarding_mens, + GenderDivision.FEMALE to R.string.onboarding_womens + ).forEach { (gender, label) -> + val selected = gender == selectedGender + Box( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .clip(CircleShape) + .background(if (selected) CrimsonPrimary else Color.Transparent) + .selectable( + selected = selected, + role = Role.RadioButton, + onClick = { onGenderSelected(gender) } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(label), + style = if (selected) Style.bodySemibold else Style.bodyMedium, + color = if (selected) White else GrayMedium + ) + } + } + } +} + +@Composable +private fun FavoriteSportCard( + option: FavoriteSport, + selected: Boolean, + onClick: () -> Unit, + designDp: (Float) -> Dp +) { + val cardShape = RoundedCornerShape(designDp(20f)) + Column( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selected, + role = Role.Checkbox, + onClick = onClick + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box( + modifier = Modifier + .fillMaxSize() + .clip(cardShape) + .background(if (selected) GreenWash else GrayStroke) + .border( + width = if (selected) designDp(3f) else designDp(1f), + color = if (selected) saturatedGreen else GrayStroke, + shape = cardShape + ), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(option.sport.emptyIcon), + contentDescription = null, + modifier = Modifier.size(designDp(45f)), + colorFilter = ColorFilter.tint( + if (selected) saturatedGreen else GrayPrimary + ) + ) + } + + if (selected) { + Box( + modifier = Modifier + .align(Alignment.TopStart) + .offset(x = designDp(-8f), y = designDp(-8f)) + .size(designDp(32f)) + .background(CrimsonPrimary, CircleShape), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(R.drawable.ic_round_minus), + contentDescription = stringResource(R.string.remove_favorite_sport), + modifier = Modifier.size(designDp(24f)), + colorFilter = ColorFilter.tint(White) + ) + } + } + } + + Spacer(Modifier.height(designDp(8f))) + Text( + text = option.sport.displayName, + style = Style.bodyMedium, + color = GrayPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(designDp(2f))) + FavoriteSportGenderTag(option.gender, designDp) + } +} + +@Composable +private fun FavoriteSportGenderTag( + gender: GenderDivision, + designDp: (Float) -> Dp +) { + val label = if (gender == GenderDivision.FEMALE) "W" else "M" + val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + + Row( + modifier = Modifier + .background(White, RoundedCornerShape(100)) + .padding(horizontal = designDp(2f)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + ) { + Box( + modifier = Modifier + .size(designDp(18f)) + .background(GrayStroke, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol, + color = GrayMedium, + fontSize = 10.sp, + lineHeight = 10.sp + ) + } + Text( + text = label, + style = Style.labelsNormal, + color = GrayPrimary + ) + } +} + +@Composable +private fun FavoriteSportsFooter( + selectedCount: Int, + onFinish: () -> Unit, + designDp: (Float) -> Dp +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = designDp(24f), + bottom = designDp(60f) + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onFinish, + modifier = Modifier + .fillMaxWidth() + .height(designDp(48f)), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + val buttonText = when (selectedCount) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedCount) + } + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(designDp(16f))) + Row( + horizontalArrangement = Arrangement.spacedBy(designDp(4f)), + verticalAlignment = Alignment.CenterVertically + ) { + Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) + Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) + } + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsScreenPreview() { + ScorePreview { + FavoriteSportsScreen(onFinish = {}) + } +} + +@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Composable +private fun FavoriteSportsSelectedScreenPreview() { + ScorePreview { + FavoriteSportsScreen( + onFinish = {}, + initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" + ) + } +} From 4fa2f2679994782b2611b91e7b397656502ecd5f Mon Sep 17 00:00:00 2001 From: amjiao Date: Mon, 19 Oct 2026 00:05:56 -0400 Subject: [PATCH 32/40] Make name more informative --- .../com/cornellappdev/score/MainActivity.kt | 2 + .../score/screen/FavoriteSportsScreen.kt | 420 ------------------ .../screen/OnboardingFavoriteSportsScreen.kt | 10 - 3 files changed, 2 insertions(+), 430 deletions(-) delete mode 100644 app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 31a1c25..483fcd6 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -11,6 +11,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.core.content.edit import com.cornellappdev.score.nav.root.RootNavigation +import com.cornellappdev.score.screen.FavoriteSport +import com.cornellappdev.score.screen.OnboardingFavoriteSportsScreen import com.cornellappdev.score.screen.OnboardingProfileSelectScreen import com.cornellappdev.score.screen.OnboardingScreen import com.google.firebase.Firebase diff --git a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt deleted file mode 100644 index 6696eee..0000000 --- a/app/src/main/java/com/cornellappdev/score/screen/FavoriteSportsScreen.kt +++ /dev/null @@ -1,420 +0,0 @@ -package com.cornellappdev.score.screen - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -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.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.cornellappdev.score.R -import com.cornellappdev.score.model.GenderDivision -import com.cornellappdev.score.model.Sport -import com.cornellappdev.score.theme.CrimsonPrimary -import com.cornellappdev.score.theme.GrayLight -import com.cornellappdev.score.theme.GrayMedium -import com.cornellappdev.score.theme.GrayPrimary -import com.cornellappdev.score.theme.GrayStroke -import com.cornellappdev.score.theme.GreenWash -import com.cornellappdev.score.theme.Style -import com.cornellappdev.score.theme.White -import com.cornellappdev.score.theme.saturatedGreen -import com.cornellappdev.score.components.ScorePreview - -data class FavoriteSport( - val sport: Sport, - val gender: GenderDivision -) { - val storageKey: String get() = "${sport.name}:${gender.name}" - - companion object { - fun fromStorageKey(key: String): FavoriteSport? { - val parts = key.split(':', limit = 2) - if (parts.size != 2) return null - val sport = runCatching { Sport.valueOf(parts[0]) }.getOrNull() ?: return null - val gender = runCatching { GenderDivision.valueOf(parts[1]) }.getOrNull() - ?: return null - if (gender == GenderDivision.ALL) return null - return FavoriteSport(sport, gender) - } - } -} - -@Composable -fun FavoriteSportsScreen( - onFinish: (Set) -> Unit, - modifier: Modifier = Modifier, - initiallySelectedKeys: String = "" -) { - var selectedGender by rememberSaveable { mutableStateOf(GenderDivision.ALL) } - var selectedKeys by rememberSaveable { - mutableStateOf(initiallySelectedKeys) - } - val selectedSports = selectedKeys.split('|').filter(String::isNotBlank).toSet() - val allOptions = rememberFavoriteSportOptions() - val visibleOptions = when (selectedGender) { - GenderDivision.ALL -> allOptions - else -> allOptions.filter { it.gender == selectedGender } - } - - BoxWithConstraints( - modifier = modifier - .fillMaxSize() - .background(White) - ) { - val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) - val designDp: (Float) -> Dp = { (it * designScale).dp } - - Column(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = designDp(79f)), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.select_favorite_sports), - style = Style.heading1, - color = GrayPrimary, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(designDp(4f))) - Text( - text = stringResource(R.string.favorite_sports_subtitle), - style = Style.bodyMedium, - color = GrayPrimary, - textAlign = TextAlign.Center - ) - Spacer(Modifier.height(designDp(32f))) - FavoriteSportsGenderFilter( - selectedGender = selectedGender, - onGenderSelected = { selectedGender = it }, - designDp = designDp - ) - } - - Spacer(Modifier.height(designDp(45f))) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - contentPadding = PaddingValues( - start = 24.dp, - end = 24.dp, - bottom = designDp(8f) - ), - horizontalArrangement = Arrangement.spacedBy(24.dp), - verticalArrangement = Arrangement.spacedBy(24.dp) - ) { - items(visibleOptions, key = { it.storageKey }) { option -> - FavoriteSportCard( - option = option, - selected = option.storageKey in selectedSports, - onClick = { - selectedKeys = if (option.storageKey in selectedSports) { - selectedSports - option.storageKey - } else { - selectedSports + option.storageKey - }.joinToString("|") - }, - designDp = designDp - ) - } - } - - FavoriteSportsFooter( - selectedCount = selectedSports.size, - onFinish = { - onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) - }, - designDp = designDp - ) - } - } -} - -@Composable -private fun rememberFavoriteSportOptions(): List { - return remember { - Sport.entries - .flatMap { sport -> - when (sport.gender) { - GenderDivision.ALL -> listOf( - FavoriteSport(sport, GenderDivision.MALE), - FavoriteSport(sport, GenderDivision.FEMALE) - ) - GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) - GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) - } - } - .sortedWith( - compareBy { it.sport.displayName } - .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } - ) - } -} - -@Composable -private fun FavoriteSportsGenderFilter( - selectedGender: GenderDivision, - onGenderSelected: (GenderDivision) -> Unit, - designDp: (Float) -> Dp -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .height(designDp(48f)) - .border( - BorderStroke(1.dp, GrayLight), - CircleShape - ) - .padding(designDp(5f)) - .selectableGroup(), - verticalAlignment = Alignment.CenterVertically - ) { - listOf( - GenderDivision.ALL to R.string.all, - GenderDivision.MALE to R.string.onboarding_mens, - GenderDivision.FEMALE to R.string.onboarding_womens - ).forEach { (gender, label) -> - val selected = gender == selectedGender - Box( - modifier = Modifier - .weight(1f) - .fillMaxSize() - .clip(CircleShape) - .background(if (selected) CrimsonPrimary else Color.Transparent) - .selectable( - selected = selected, - role = Role.RadioButton, - onClick = { onGenderSelected(gender) } - ), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(label), - style = if (selected) Style.bodySemibold else Style.bodyMedium, - color = if (selected) White else GrayMedium - ) - } - } - } -} - -@Composable -private fun FavoriteSportCard( - option: FavoriteSport, - selected: Boolean, - onClick: () -> Unit, - designDp: (Float) -> Dp -) { - val cardShape = RoundedCornerShape(designDp(20f)) - Column( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = selected, - role = Role.Checkbox, - onClick = onClick - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(cardShape) - .background(if (selected) GreenWash else GrayStroke) - .border( - width = if (selected) designDp(3f) else designDp(1f), - color = if (selected) saturatedGreen else GrayStroke, - shape = cardShape - ), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(option.sport.emptyIcon), - contentDescription = null, - modifier = Modifier.size(designDp(45f)), - colorFilter = ColorFilter.tint( - if (selected) saturatedGreen else GrayPrimary - ) - ) - } - - if (selected) { - Box( - modifier = Modifier - .align(Alignment.TopStart) - .offset(x = designDp(-8f), y = designDp(-8f)) - .size(designDp(32f)) - .background(CrimsonPrimary, CircleShape), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(R.drawable.ic_round_minus), - contentDescription = stringResource(R.string.remove_favorite_sport), - modifier = Modifier.size(designDp(24f)), - colorFilter = ColorFilter.tint(White) - ) - } - } - } - - Spacer(Modifier.height(designDp(8f))) - Text( - text = option.sport.displayName, - style = Style.bodyMedium, - color = GrayPrimary, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(designDp(2f))) - FavoriteSportGenderTag(option.gender, designDp) - } -} - -@Composable -private fun FavoriteSportGenderTag( - gender: GenderDivision, - designDp: (Float) -> Dp -) { - val label = if (gender == GenderDivision.FEMALE) "W" else "M" - val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" - - Row( - modifier = Modifier - .background(White, RoundedCornerShape(100)) - .padding(horizontal = designDp(2f)), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(designDp(2f)) - ) { - Box( - modifier = Modifier - .size(designDp(18f)) - .background(GrayStroke, CircleShape), - contentAlignment = Alignment.Center - ) { - Text( - text = symbol, - color = GrayMedium, - fontSize = 10.sp, - lineHeight = 10.sp - ) - } - Text( - text = label, - style = Style.labelsNormal, - color = GrayPrimary - ) - } -} - -@Composable -private fun FavoriteSportsFooter( - selectedCount: Int, - onFinish: () -> Unit, - designDp: (Float) -> Dp -) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(White) - .padding( - start = 24.dp, - end = 24.dp, - top = designDp(24f), - bottom = designDp(60f) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - onClick = onFinish, - modifier = Modifier - .fillMaxWidth() - .height(designDp(48f)), - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = CrimsonPrimary, - contentColor = White - ) - ) { - val buttonText = when (selectedCount) { - 0 -> stringResource(R.string.finish) - 1 -> stringResource(R.string.finish_with_one_sport) - else -> stringResource(R.string.finish_with_sports, selectedCount) - } - Text(buttonText, style = Style.heading3, color = White) - } - - Spacer(Modifier.height(designDp(16f))) - Row( - horizontalArrangement = Arrangement.spacedBy(designDp(4f)), - verticalAlignment = Alignment.CenterVertically - ) { - Box(Modifier.size(designDp(8f)).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) - } - } -} - -@Preview(showBackground = true, widthDp = 393, heightDp = 852) -@Composable -private fun FavoriteSportsScreenPreview() { - ScorePreview { - FavoriteSportsScreen(onFinish = {}) - } -} - -@Preview(showBackground = true, widthDp = 393, heightDp = 852) -@Composable -private fun FavoriteSportsSelectedScreenPreview() { - ScorePreview { - FavoriteSportsScreen( - onFinish = {}, - initiallySelectedKeys = "BASEBALL:MALE|CROSS_COUNTRY:FEMALE" - ) - } -} diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index 0e16a6e..e1d06c4 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -361,11 +361,6 @@ private fun FavoriteSportsFooter( onFinish: () -> Unit, designDp: (Float) -> Dp ) { - var showActiveProgressDot by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - showActiveProgressDot = true - } - Column( modifier = Modifier .fillMaxWidth() @@ -402,11 +397,6 @@ private fun FavoriteSportsFooter( horizontalArrangement = Arrangement.spacedBy(designDp(4f)), verticalAlignment = Alignment.CenterVertically ) { - val activeColor by animateColorAsState( - targetValue = if (showActiveProgressDot) CrimsonPrimary else GrayStroke, - animationSpec = tween(durationMillis = 250), - label = "favoriteSportsActiveDot" - ) Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) } From 1e2ea237766c679bf56a184cb0db2dad7ef1546a Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 21 Oct 2026 17:22:48 -0400 Subject: [PATCH 33/40] fix page dot indicator --- .../score/screen/OnboardingFavoriteSportsScreen.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index e1d06c4..ecc6eee 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -361,6 +361,11 @@ private fun FavoriteSportsFooter( onFinish: () -> Unit, designDp: (Float) -> Dp ) { + var showActiveProgressDot by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + showActiveProgressDot = true + } + Column( modifier = Modifier .fillMaxWidth() From b4613686c2fc88ce830727bcef1f7508b8579f92 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:56:40 -0400 Subject: [PATCH 34/40] Onboarding footer component --- .../score/screen/OnboardingFooter.kt | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt new file mode 100644 index 0000000..4af4f06 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFooter.kt @@ -0,0 +1,94 @@ +package com.cornellappdev.score.screen + +import androidx.compose.foundation.background +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.score.components.ScorePreview +import com.cornellappdev.score.theme.CrimsonPrimary +import com.cornellappdev.score.theme.GrayStroke +import com.cornellappdev.score.theme.Style +import com.cornellappdev.score.theme.White + +enum class OnboardingState{ + PROFILE_SELECT, SPORT_SELECT +} + +@Composable +fun OnboardingFooter( + buttonText: String, + onClick: () -> Unit, + activeStep: OnboardingState +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding( + start = 24.dp, + end = 24.dp, + top = 24.dp, + bottom = 60.dp + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = CrimsonPrimary, + contentColor = White + ) + ) { + Text(buttonText, style = Style.heading3, color = White) + } + + Spacer(Modifier.height(16.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(2) { index -> + val isActive = when (activeStep) { + OnboardingState.PROFILE_SELECT -> index == 0 + OnboardingState.SPORT_SELECT -> index == 1 + } + Box( + Modifier + .size(8.dp) + .background(if (isActive) CrimsonPrimary else GrayStroke, CircleShape) + ) + } + } + } +} + +@Preview +@Composable +private fun OnboardingFooterPreview() { + ScorePreview { + OnboardingFooter( + buttonText = "Next", + onClick = {}, + activeStep = OnboardingState.PROFILE_SELECT + ) + } +} From 76f0f2f89f1bb4f8e573da15049e09be4b1f4ad3 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:56:57 -0400 Subject: [PATCH 35/40] Add sport select icons --- .../res/drawable/ic_light_gray_female_gender.xml | 16 ++++++++++++++++ .../res/drawable/ic_light_gray_male_gender.xml | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/src/main/res/drawable/ic_light_gray_female_gender.xml create mode 100644 app/src/main/res/drawable/ic_light_gray_male_gender.xml diff --git a/app/src/main/res/drawable/ic_light_gray_female_gender.xml b/app/src/main/res/drawable/ic_light_gray_female_gender.xml new file mode 100644 index 0000000..81e066a --- /dev/null +++ b/app/src/main/res/drawable/ic_light_gray_female_gender.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_light_gray_male_gender.xml b/app/src/main/res/drawable/ic_light_gray_male_gender.xml new file mode 100644 index 0000000..3039fdb --- /dev/null +++ b/app/src/main/res/drawable/ic_light_gray_male_gender.xml @@ -0,0 +1,16 @@ + + + + + + + From 4059984758edb0f3a5d9d8dde8ab3731171cc4b3 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:58:03 -0400 Subject: [PATCH 36/40] Change SportSelectScreen to not use manual scaling --- .../screen/OnboardingFavoriteSportsScreen.kt | 181 ++++++------------ 1 file changed, 62 insertions(+), 119 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt index ecc6eee..0c8967a 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingFavoriteSportsScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -25,13 +24,8 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -47,10 +41,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.cornellappdev.score.R +import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.model.GenderDivision import com.cornellappdev.score.model.Sport import com.cornellappdev.score.theme.CrimsonPrimary @@ -62,7 +55,6 @@ import com.cornellappdev.score.theme.GreenWash import com.cornellappdev.score.theme.Style import com.cornellappdev.score.theme.White import com.cornellappdev.score.theme.saturatedGreen -import com.cornellappdev.score.components.ScorePreview data class FavoriteSport( val sport: Sport, @@ -100,19 +92,16 @@ fun OnboardingFavoriteSportsScreen( else -> allOptions.filter { it.gender == selectedGender } } - BoxWithConstraints( + Box( modifier = modifier .fillMaxSize() .background(White) ) { - val designScale = minOf(maxWidth.value / 393f, maxHeight.value / 852f) - val designDp: (Float) -> Dp = { (it * designScale).dp } - Column(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .fillMaxWidth() - .padding(top = designDp(79f)), + .padding(top = 79.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -121,22 +110,21 @@ fun OnboardingFavoriteSportsScreen( color = GrayPrimary, textAlign = TextAlign.Center ) - Spacer(Modifier.height(designDp(4f))) + Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.favorite_sports_subtitle), style = Style.bodyMedium, color = GrayPrimary, textAlign = TextAlign.Center ) - Spacer(Modifier.height(designDp(32f))) + Spacer(Modifier.height(32.dp)) FavoriteSportsGenderFilter( selectedGender = selectedGender, - onGenderSelected = { selectedGender = it }, - designDp = designDp + onGenderSelected = { selectedGender = it } ) } - Spacer(Modifier.height(designDp(45f))) + Spacer(Modifier.height(32.dp)) LazyVerticalGrid( columns = GridCells.Fixed(3), modifier = Modifier @@ -145,7 +133,8 @@ fun OnboardingFavoriteSportsScreen( contentPadding = PaddingValues( start = 24.dp, end = 24.dp, - bottom = designDp(8f) + top = 8.dp, + bottom = 8.dp ), horizontalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp) @@ -160,18 +149,21 @@ fun OnboardingFavoriteSportsScreen( } else { selectedSports + option.storageKey }.joinToString("|") - }, - designDp = designDp + } ) } } - FavoriteSportsFooter( - selectedCount = selectedSports.size, - onFinish = { + OnboardingFooter( + buttonText = when (selectedSports.size) { + 0 -> stringResource(R.string.finish) + 1 -> stringResource(R.string.finish_with_one_sport) + else -> stringResource(R.string.finish_with_sports, selectedSports.size) + }, + onClick = { onFinish(selectedSports.mapNotNull(FavoriteSport::fromStorageKey).toSet()) }, - designDp = designDp + activeStep = OnboardingState.SPORT_SELECT ) } } @@ -181,39 +173,39 @@ fun OnboardingFavoriteSportsScreen( private fun rememberFavoriteSportOptions(): List { return remember { Sport.entries - .flatMap { sport -> - when (sport.gender) { - GenderDivision.ALL -> listOf( - FavoriteSport(sport, GenderDivision.MALE), - FavoriteSport(sport, GenderDivision.FEMALE) - ) - GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) - GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + .flatMap { sport -> + when (sport.gender) { + GenderDivision.ALL -> listOf( + FavoriteSport(sport, GenderDivision.MALE), + FavoriteSport(sport, GenderDivision.FEMALE) + ) + + GenderDivision.MALE -> listOf(FavoriteSport(sport, GenderDivision.MALE)) + GenderDivision.FEMALE -> listOf(FavoriteSport(sport, GenderDivision.FEMALE)) + } } - } - .sortedWith( - compareBy { it.sport.displayName } - .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } - ) + .sortedWith( + compareBy { it.sport.displayName } + .thenBy { if (it.gender == GenderDivision.MALE) 0 else 1 } + ) } } @Composable private fun FavoriteSportsGenderFilter( selectedGender: GenderDivision, - onGenderSelected: (GenderDivision) -> Unit, - designDp: (Float) -> Dp + onGenderSelected: (GenderDivision) -> Unit ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) - .height(designDp(48f)) + .height(48.dp) .border( BorderStroke(1.dp, GrayLight), CircleShape ) - .padding(designDp(5f)) + .padding(5.dp) .selectableGroup(), verticalAlignment = Alignment.CenterVertically ) { @@ -250,10 +242,9 @@ private fun FavoriteSportsGenderFilter( private fun FavoriteSportCard( option: FavoriteSport, selected: Boolean, - onClick: () -> Unit, - designDp: (Float) -> Dp + onClick: () -> Unit ) { - val cardShape = RoundedCornerShape(designDp(20f)) + val cardShape = RoundedCornerShape(20.dp) Column( modifier = Modifier .fillMaxWidth() @@ -264,14 +255,16 @@ private fun FavoriteSportCard( ), horizontalAlignment = Alignment.CenterHorizontally ) { - Box(modifier = Modifier.fillMaxWidth().aspectRatio(1f)) { + Box(modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f)) { Box( modifier = Modifier .fillMaxSize() .clip(cardShape) .background(if (selected) GreenWash else GrayStroke) .border( - width = if (selected) designDp(3f) else designDp(1f), + width = if (selected) 3.dp else 1.dp, color = if (selected) saturatedGreen else GrayStroke, shape = cardShape ), @@ -280,7 +273,7 @@ private fun FavoriteSportCard( Image( painter = painterResource(option.sport.emptyIcon), contentDescription = null, - modifier = Modifier.size(designDp(45f)), + modifier = Modifier.size(45.dp), colorFilter = ColorFilter.tint( if (selected) saturatedGreen else GrayPrimary ) @@ -291,22 +284,22 @@ private fun FavoriteSportCard( Box( modifier = Modifier .align(Alignment.TopStart) - .offset(x = designDp(-8f), y = designDp(-8f)) - .size(designDp(32f)) + .offset(x = (-8).dp, y = (-8).dp) + .size(32.dp) .background(CrimsonPrimary, CircleShape), contentAlignment = Alignment.Center ) { Image( painter = painterResource(R.drawable.ic_round_minus), contentDescription = stringResource(R.string.remove_favorite_sport), - modifier = Modifier.size(designDp(24f)), + modifier = Modifier.size(24.dp), colorFilter = ColorFilter.tint(White) ) } } } - Spacer(Modifier.height(designDp(8f))) + Spacer(Modifier.height(8.dp)) Text( text = option.sport.displayName, style = Style.bodyMedium, @@ -314,37 +307,39 @@ private fun FavoriteSportCard( textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) - Spacer(Modifier.height(designDp(2f))) - FavoriteSportGenderTag(option.gender, designDp) + Spacer(Modifier.height(2.dp)) + FavoriteSportGenderTag(option.gender) } } @Composable private fun FavoriteSportGenderTag( gender: GenderDivision, - designDp: (Float) -> Dp ) { val label = if (gender == GenderDivision.FEMALE) "W" else "M" - val symbol = if (gender == GenderDivision.FEMALE) "♀" else "♂" + val genderIcon = if (gender == GenderDivision.FEMALE) { + R.drawable.ic_light_gray_female_gender + } else { + R.drawable.ic_light_gray_male_gender + } Row( modifier = Modifier .background(White, RoundedCornerShape(100)) - .padding(horizontal = designDp(2f)), + .padding(horizontal = 2.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(designDp(2f)) + horizontalArrangement = Arrangement.spacedBy(2.dp) ) { Box( modifier = Modifier - .size(designDp(18f)) + .size(18.dp) .background(GrayStroke, CircleShape), contentAlignment = Alignment.Center ) { - Text( - text = symbol, - color = GrayMedium, - fontSize = 10.sp, - lineHeight = 10.sp + Image( + painter = painterResource(genderIcon), + contentDescription = null, + modifier = Modifier.size(12.dp) ) } Text( @@ -355,60 +350,8 @@ private fun FavoriteSportGenderTag( } } -@Composable -private fun FavoriteSportsFooter( - selectedCount: Int, - onFinish: () -> Unit, - designDp: (Float) -> Dp -) { - var showActiveProgressDot by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - showActiveProgressDot = true - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(White) - .padding( - start = 24.dp, - end = 24.dp, - top = designDp(24f), - bottom = designDp(60f) - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - onClick = onFinish, - modifier = Modifier - .fillMaxWidth() - .height(designDp(48f)), - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = CrimsonPrimary, - contentColor = White - ) - ) { - val buttonText = when (selectedCount) { - 0 -> stringResource(R.string.finish) - 1 -> stringResource(R.string.finish_with_one_sport) - else -> stringResource(R.string.finish_with_sports, selectedCount) - } - Text(buttonText, style = Style.heading3, color = White) - } - - Spacer(Modifier.height(designDp(16f))) - Row( - horizontalArrangement = Arrangement.spacedBy(designDp(4f)), - verticalAlignment = Alignment.CenterVertically - ) { - Box(Modifier.size(designDp(8f)).background(GrayStroke, CircleShape)) - Box(Modifier.size(designDp(8f)).background(activeColor, CircleShape)) - } - } -} -@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Preview @Composable private fun OnboardingFavoriteSportsScreenPreview() { ScorePreview { @@ -416,7 +359,7 @@ private fun OnboardingFavoriteSportsScreenPreview() { } } -@Preview(showBackground = true, widthDp = 393, heightDp = 852) +@Preview @Composable private fun OnboardingFavoriteSportsSelectedScreenPreview() { ScorePreview { From 5e961d36df520aadd700881942ffe495ed921cf1 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 12:58:29 -0400 Subject: [PATCH 37/40] Use new onboarding footer component --- .../screen/OnboardingProfileSelectScreen.kt | 61 ++----------------- 1 file changed, 5 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 5cd9c70..77c30e7 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -2,6 +2,7 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,9 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -30,10 +28,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R -import com.cornellappdev.score.theme.CrimsonPrimary import com.cornellappdev.score.theme.GrayPrimary import com.cornellappdev.score.theme.GrayStroke -import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable @@ -104,59 +100,12 @@ fun OnboardingProfileSelectScreen( } Spacer(Modifier.weight(1f)) - OnboardingProfileFooter( - onNext = { onNext(selectedPicture) } - ) - } - } -} - -@Composable -private fun OnboardingProfileFooter( - onNext: () -> Unit, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 24.dp, - end = 24.dp, - top = 24.dp, - bottom = 60.dp - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button( - onClick = onNext, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - containerColor = CrimsonPrimary, - contentColor = Color.White - ) - ) { - Text( - text = stringResource(R.string.next), - style = Style.heading3, - color = Color.White + OnboardingFooter( + buttonText = stringResource(R.string.next), + onClick = { onNext(selectedPicture) }, + activeStep = OnboardingState.PROFILE_SELECT ) } - - Spacer(Modifier.height(16.dp)) - RowProgressDots() - } -} - -@Composable -private fun RowProgressDots() { - androidx.compose.foundation.layout.Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Box(Modifier.size(8.dp).background(CrimsonPrimary, CircleShape)) - Box(Modifier.size(8.dp).background(GrayStroke, CircleShape)) } } From ef4433ffa412b151da69d0f947a41eb9b68846a9 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 17:51:49 -0400 Subject: [PATCH 38/40] Restore favorite sports onboarding step --- .../java/com/cornellappdev/score/MainActivity.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/com/cornellappdev/score/MainActivity.kt b/app/src/main/java/com/cornellappdev/score/MainActivity.kt index 483fcd6..bea491f 100644 --- a/app/src/main/java/com/cornellappdev/score/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/score/MainActivity.kt @@ -30,6 +30,7 @@ private lateinit var firebaseAnalytics: FirebaseAnalytics private enum class OnboardingStep { WELCOME, PROFILE_SELECT, + SPORT_SELECT, COMPLETE } @@ -72,6 +73,19 @@ class MainActivity : AppCompatActivity() { preferences.edit { putString(KEY_ONBOARDING_PROFILE_PICTURE, selectedPicture.name) } + onboardingStep = OnboardingStep.SPORT_SELECT + } + ) + + OnboardingStep.SPORT_SELECT -> OnboardingFavoriteSportsScreen( + initiallySelectedKeys = savedFavoriteSportKeys, + onFinish = { selectedSports -> + preferences.edit { + putStringSet( + KEY_ONBOARDING_FAVORITE_SPORTS, + selectedSports.map(FavoriteSport::storageKey).toSet() + ) + } completeOnboarding() } ) From ddd821d31bba9afd9392b3a9258bfdd7055515de Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 17:58:39 -0400 Subject: [PATCH 39/40] Clean up --- .../score/screen/OnboardingProfileSelectScreen.kt | 4 ---- .../com/cornellappdev/score/screen/ProfilePictureOptions.kt | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt index 77c30e7..bca920f 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/OnboardingProfileSelectScreen.kt @@ -2,8 +2,6 @@ package com.cornellappdev.score.screen import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -25,11 +23,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.cornellappdev.score.R import com.cornellappdev.score.theme.GrayPrimary -import com.cornellappdev.score.theme.GrayStroke import com.cornellappdev.score.theme.Style @Composable diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt index 2caa255..dcae5c4 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -10,19 +10,18 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.cornellappdev.score.components.ScorePreview import com.cornellappdev.score.theme.saturatedGreen From 0c4cea2118349c3dc7dde1265b108a80d0c60659 Mon Sep 17 00:00:00 2001 From: amjiao Date: Wed, 28 Oct 2026 18:15:02 -0400 Subject: [PATCH 40/40] Clean up --- .../com/cornellappdev/score/screen/ProfilePictureOptions.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt index 234073a..dcae5c4 100644 --- a/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt +++ b/app/src/main/java/com/cornellappdev/score/screen/ProfilePictureOptions.kt @@ -10,13 +10,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color