From 65e392f93bb95b931b3f5d864a877a9186ba0ebd Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Tue, 9 Jul 2024 20:49:11 +0200 Subject: [PATCH 01/12] work in progress tutorials in compose --- .../screens/tutorial/IntroTutorialScreen.kt | 166 ++++++++++++++++++ .../tutorial/OverlaysTutorialScreen.kt | 158 +++++++++++++++++ .../screens/tutorial/TutorialScreen.kt | 114 ++++++++++++ .../user/achievements/AnimatedTadaShine.kt | 6 +- .../streetcomplete/ui/common/MapButton.kt | 44 +++++ .../streetcomplete/ui/common/Pin.kt | 40 +++++ app/src/main/res/values/textStyles.xml | 4 +- 7 files changed, 527 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt create mode 100644 app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/OverlaysTutorialScreen.kt create mode 100644 app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/TutorialScreen.kt create mode 100644 app/src/main/java/de/westnordost/streetcomplete/ui/common/MapButton.kt create mode 100644 app/src/main/java/de/westnordost/streetcomplete/ui/common/Pin.kt diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt new file mode 100644 index 00000000000..568f65899a3 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/IntroTutorialScreen.kt @@ -0,0 +1,166 @@ +package de.westnordost.streetcomplete.screens.tutorial + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.absoluteOffset +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.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.ui.common.MapButton +import de.westnordost.streetcomplete.ui.common.Pin +import de.westnordost.streetcomplete.ui.theme.headlineSmall +import de.westnordost.streetcomplete.ui.theme.titleSmall + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun IntroTutorialScreen( + onFinished: () -> Unit, +) { + TutorialScreen( + pageCount = 3, + onFinished = onFinished, + illustration = { page -> + Box(contentAlignment = Alignment.TopStart) { + IntroTutorialIllustration(page) + } + } + ) { page -> + Column( + modifier = Modifier.fillMaxSize(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (page) { + 0 -> IntroTutorialStep1Text() + 1 -> IntroTutorialStep2Text() + 2 -> IntroTutorialStep3Text() + } + } + } +} + +@Composable +private fun IntroTutorialIllustration( + page: Float +) { + // TODO animate steps + + Box(Modifier.size(width = 226.dp, height = 222.dp)) { + Image( + painter = painterResource(R.drawable.logo_osm_map), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + Image( + painter = painterResource(R.drawable.logo_osm_map_lighting), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + + MapButton( + onClick = {}, + modifier = Modifier.absoluteOffset(x = 200.dp, y = 130.dp) + ) { + Icon(painterResource(R.drawable.ic_location_no_location_24dp), null) + } + + Pin( + iconPainter = painterResource(R.drawable.ic_quest_recycling), + modifier = Modifier.absoluteOffset(x = 120.dp, y = 60.dp) + ) + Pin( + iconPainter = painterResource(R.drawable.ic_quest_street), + modifier = Modifier.absoluteOffset(x = 45.dp, y = 110.dp) + ) + Pin( + iconPainter = painterResource(R.drawable.ic_quest_traffic_lights), + modifier = Modifier.absoluteOffset(x = 0.dp, y = 25.dp) + ) + + // TODO checkmark circle + + Image( + painter = painterResource(R.drawable.logo_osm_magnifier), + contentDescription = null, + modifier = Modifier + .size(185.dp) + .offset(x = 20.dp, y = 16.dp) + ) +} + +@Composable +private fun IntroTutorialStep1Text() { + Text( + text = stringResource(R.string.tutorial_welcome_to_osm), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.tutorial_welcome_to_osm_subtitle), + style = MaterialTheme.typography.titleSmall, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.tutorial_intro), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 24.dp) + ) +} + +@Composable +private fun IntroTutorialStep2Text() { + Text( + text = stringResource(R.string.tutorial_solving_quests), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(R.string.no_location_permission_warning), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 24.dp) + ) +} + +@Composable +private fun IntroTutorialStep3Text() { + Text( + text = stringResource(R.string.tutorial_stay_safe), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(R.string.tutorial_happy_mapping), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 24.dp) + ) +} + +@Preview +@Composable +private fun PreviewIntroTutorialIllustration() { + IntroTutorialIllustration(page = 0f) +} + +@Preview +@Composable +private fun PreviewIntroTutorialScreen() { + IntroTutorialScreen {} +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/OverlaysTutorialScreen.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/OverlaysTutorialScreen.kt new file mode 100644 index 00000000000..3291f2b34bd --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/OverlaysTutorialScreen.kt @@ -0,0 +1,158 @@ +package de.westnordost.streetcomplete.screens.tutorial + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +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 de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.screens.user.achievements.AnimatedTadaShine +import de.westnordost.streetcomplete.ui.common.MapButton +import de.westnordost.streetcomplete.ui.theme.headlineSmall + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun OverlaysTutorialScreen( + onFinished: () -> Unit, +) { + TutorialScreen( + pageCount = 3, + onFinished = onFinished, + illustration = { page -> + Box(contentAlignment = Alignment.TopStart) { + OverlaysTutorialIllustration(page) + } + }, + ) { page -> + Column( + modifier = Modifier.fillMaxSize(1f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (page) { + 0 -> OverlaysTutorialStepIntroText() + 1 -> OverlaysTutorialStepDisplayText() + 2 -> OverlaysTutorialStepEditText() + } + } + } +} + +@Composable +private fun OverlaysTutorialStepIntroText() { + Text( + text = stringResource(R.string.overlays_tutorial_title), + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) + Text( + text = stringResource(R.string.overlays_tutorial_intro), + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(top = 24.dp), + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun OverlaysTutorialStepDisplayText() { + Text( + text = stringResource(R.string.overlays_tutorial_display), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun OverlaysTutorialStepEditText() { + Text( + text = stringResource(R.string.overlays_tutorial_edit), + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun OverlaysTutorialIllustration( + page: Float +) { + // TODO animate steps + + Box(Modifier.size(width = 226.dp, height = 222.dp)) { + Image( + painter = painterResource(R.drawable.logo_osm_map), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + Image( + painter = painterResource(R.drawable.overlay_osm_map), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + + Image( + painter = painterResource(R.drawable.ic_preset_fas_shopping_cart), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .absoluteOffset(80.dp, 40.dp) + ) + Image( + painter = painterResource(R.drawable.ic_preset_maki_fuel), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .absoluteOffset(180.dp, 170.dp) + ) + Image( + painter = painterResource(R.drawable.overlay_osm_map_edit), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + + Image( + painter = painterResource(R.drawable.paint_roller), + contentDescription = null, + modifier = Modifier.rotate(-45f) + ) + + Box( + modifier = Modifier + .size(56.dp) + .absoluteOffset(x = 228.dp, y = 158.dp), + contentAlignment = Alignment.Center, + ) { + AnimatedTadaShine() + + MapButton(onClick = {},) { + Icon(painterResource(R.drawable.ic_overlay_black_24dp), null) + } + } + +} + +@Preview +@Composable +private fun PreviewOverlaysTutorialIllustration() { + OverlaysTutorialIllustration(page = 0f) +} + +@Preview +@Composable +private fun PreviewOverlaysTutorialScreen() { + OverlaysTutorialScreen {} +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/TutorialScreen.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/TutorialScreen.kt new file mode 100644 index 00000000000..a58622352eb --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/tutorial/TutorialScreen.kt @@ -0,0 +1,114 @@ +package de.westnordost.streetcomplete.screens.tutorial + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerScope +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.ContentAlpha +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R +import kotlinx.coroutines.launch + +/** Generic multiple-page tutorial screen */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TutorialScreen( + pageCount: Int, + onFinished: () -> Unit, + illustration: @Composable (pageAnimated: Float) -> Unit, + pageContent: @Composable PagerScope.(page: Int) -> Unit +) { + val state = rememberPagerState { pageCount } + val coroutineScope = rememberCoroutineScope() + val pageAnimated = remember(state.currentPage, state.currentPageOffsetFraction) { + derivedStateOf { state.currentPage + state.currentPageOffsetFraction } + } +// TODO deal with landscape layout? + + Column( + modifier = Modifier + .fillMaxSize() + .padding(bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Box( + modifier = Modifier.weight(0.5f), + contentAlignment = Alignment.BottomCenter + ) { + illustration(pageAnimated.value) + } + HorizontalPager( + state = state, + modifier = Modifier.weight(0.5f), + contentPadding = PaddingValues(horizontal = 16.dp), + pageSpacing = 32.dp, + pageContent = pageContent + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Row { + repeat(pageCount) { page -> + PagerIndicator(isCurrentPage = state.currentPage == page) + } + } + Button(onClick = { + if (state.isOnLastPage()) { + onFinished() + } else { + coroutineScope.launch { + state.animateScrollToPage(state.currentPage + 1) + } + } + }) { + Text(stringResource(if (state.isOnLastPage()) R.string.letsgo else R.string.next)) + } + } + } +} + +@Composable +private fun PagerIndicator( + isCurrentPage: Boolean, + modifier: Modifier = Modifier +) { + val alpha by animateFloatAsState( + if (isCurrentPage) ContentAlpha.high else ContentAlpha.disabled + ) + Box(modifier + .padding(4.dp) + .alpha(alpha) + .background(color = MaterialTheme.colors.onSurface, shape = CircleShape) + .size(12.dp) + ) +} + +@OptIn(ExperimentalFoundationApi::class) +private fun PagerState.isOnLastPage(): Boolean = currentPage >= pageCount - 1 diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AnimatedTadaShine.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AnimatedTadaShine.kt index d1f60425651..2765cfbbdd7 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AnimatedTadaShine.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AnimatedTadaShine.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import de.westnordost.streetcomplete.R @Composable -fun AnimatedTadaShine() { +fun AnimatedTadaShine(modifier: Modifier = Modifier) { val infiniteTransition = rememberInfiniteTransition("ta-da shine rotation") val rotation by infiniteTransition.animateFloat( 0f, 360f, @@ -27,8 +27,8 @@ fun AnimatedTadaShine() { "ta-da shine rotation" ) - TadaShine(Modifier.rotate(rotation * 2f)) - TadaShine(Modifier.rotate(180f - rotation)) + TadaShine(modifier.rotate(rotation * 2f)) + TadaShine(modifier.rotate(180f - rotation)) } @Composable diff --git a/app/src/main/java/de/westnordost/streetcomplete/ui/common/MapButton.kt b/app/src/main/java/de/westnordost/streetcomplete/ui/common/MapButton.kt new file mode 100644 index 00000000000..6bf289f0ef5 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/ui/common/MapButton.kt @@ -0,0 +1,44 @@ +package de.westnordost.streetcomplete.ui.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MapButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable() (BoxScope.() -> Unit) +) { + Surface( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = CircleShape, + color = Color.White, + elevation = 4.dp + ) { + Box(Modifier.padding(16.dp), content = content) + } +} + +@Preview +@Composable +private fun PreviewMapButton() { + MapButton(onClick = {}) { + Icon(painterResource(R.drawable.ic_location_24dp), null) + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/ui/common/Pin.kt b/app/src/main/java/de/westnordost/streetcomplete/ui/common/Pin.kt new file mode 100644 index 00000000000..a09a277a077 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/ui/common/Pin.kt @@ -0,0 +1,40 @@ +package de.westnordost.streetcomplete.ui.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.absoluteOffset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.westnordost.streetcomplete.R + +@Composable +fun Pin( + iconPainter: Painter, + modifier: Modifier = Modifier +) { + Box(modifier) { + Image( + painter = painterResource(R.drawable.pin), + contentDescription = null, + modifier = Modifier.size(66.dp) + ) + Image( + painter = iconPainter, + contentDescription = null, + modifier = Modifier + .absoluteOffset(x = 20.dp, y = 7.dp) + .size(42.dp) + ) + } +} + +@Composable +@Preview +private fun PinPreview() { + Pin(painterResource(R.drawable.ic_quest_recycling)) +} diff --git a/app/src/main/res/values/textStyles.xml b/app/src/main/res/values/textStyles.xml index f6c759d5bb0..f4ddbeeb885 100644 --- a/app/src/main/res/values/textStyles.xml +++ b/app/src/main/res/values/textStyles.xml @@ -1,10 +1,10 @@ - - - - -