feature: show onboarding as a modal on large screens, add close functionality
This commit is contained in:
+152
-59
@@ -1,20 +1,27 @@
|
||||
package bzh.ajaury.chombev.onboarding.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Hearing
|
||||
import androidx.compose.material.icons.filled.Speed
|
||||
import androidx.compose.material.icons.filled.Subtitles
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -23,6 +30,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import bzh.ajaury.chombev.onboarding.model.OnboardingLevel
|
||||
import bzh.ajaury.chombev.onboarding.ui.components.OnboardingInfoPage
|
||||
@@ -31,6 +40,7 @@ import bzh.ajaury.chombev.onboarding.ui.components.OnboardingPageIndicator
|
||||
import bzh.ajaury.chombev.onboarding.ui.viewmodel.OnboardingUiState
|
||||
import bzh.ajaury.chombev.onboarding.ui.viewmodel.OnboardingViewModel
|
||||
import bzh.ajaury.chombev.resources.generated.resources.Res
|
||||
import bzh.ajaury.chombev.resources.generated.resources.onboarding_close
|
||||
import bzh.ajaury.chombev.resources.generated.resources.onboarding_goal_description
|
||||
import bzh.ajaury.chombev.resources.generated.resources.onboarding_goal_title
|
||||
import bzh.ajaury.chombev.resources.generated.resources.onboarding_next
|
||||
@@ -46,93 +56,156 @@ import org.koin.compose.viewmodel.koinViewModel
|
||||
private const val PAGE_COUNT = 4
|
||||
private const val LEVEL_PAGE_INDEX = PAGE_COUNT - 1
|
||||
|
||||
private val DIALOG_MAX_WIDTH = 560.dp
|
||||
private val DIALOG_MAX_HEIGHT = 680.dp
|
||||
private const val DIALOG_WINDOW_FRACTION = 0.9f
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(viewModel: OnboardingViewModel = koinViewModel()) {
|
||||
fun OnboardingScreen(
|
||||
showAsDialog: Boolean = false,
|
||||
viewModel: OnboardingViewModel = koinViewModel(),
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingScreen(
|
||||
uiState = uiState,
|
||||
showAsDialog = showAsDialog,
|
||||
onLevelSelected = viewModel::selectLevel,
|
||||
onFinish = viewModel::completeOnboarding,
|
||||
onDismiss = viewModel::skipOnboarding,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
uiState: OnboardingUiState,
|
||||
showAsDialog: Boolean = false,
|
||||
onLevelSelected: (OnboardingLevel) -> Unit = {},
|
||||
onFinish: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {},
|
||||
) {
|
||||
if (showAsDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
// The content sizes itself against the window rather than the platform default width.
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT)
|
||||
.fillMaxSize(fraction = DIALOG_WINDOW_FRACTION),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
OnboardingContent(
|
||||
uiState = uiState,
|
||||
onLevelSelected = onLevelSelected,
|
||||
onFinish = onFinish,
|
||||
onClose = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Scaffold { innerPadding ->
|
||||
OnboardingContent(
|
||||
uiState = uiState,
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onLevelSelected = onLevelSelected,
|
||||
onFinish = onFinish,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnboardingContent(
|
||||
uiState: OnboardingUiState,
|
||||
modifier: Modifier = Modifier,
|
||||
onLevelSelected: (OnboardingLevel) -> Unit = {},
|
||||
onFinish: () -> Unit = {},
|
||||
onClose: (() -> Unit)? = null,
|
||||
) {
|
||||
val pagerState = rememberPagerState(pageCount = { PAGE_COUNT })
|
||||
val scope = rememberCoroutineScope()
|
||||
val isLastPage = pagerState.currentPage == LEVEL_PAGE_INDEX
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Hearing,
|
||||
title = stringResource(Res.string.onboarding_goal_title),
|
||||
description = stringResource(Res.string.onboarding_goal_description),
|
||||
)
|
||||
|
||||
1 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Subtitles,
|
||||
title = stringResource(Res.string.onboarding_subtitles_title),
|
||||
description = stringResource(Res.string.onboarding_subtitles_description),
|
||||
)
|
||||
|
||||
2 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Speed,
|
||||
title = stringResource(Res.string.onboarding_playback_title),
|
||||
description = stringResource(Res.string.onboarding_playback_description),
|
||||
)
|
||||
|
||||
else -> OnboardingLevelPage(
|
||||
selectedLevel = uiState.selectedLevel,
|
||||
onLevelSelected = onLevelSelected,
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (onClose != null) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = onClose) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(Res.string.onboarding_close),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingPageIndicator(
|
||||
pageCount = PAGE_COUNT,
|
||||
currentPage = pagerState.currentPage,
|
||||
)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
) { page ->
|
||||
when (page) {
|
||||
0 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Hearing,
|
||||
title = stringResource(Res.string.onboarding_goal_title),
|
||||
description = stringResource(Res.string.onboarding_goal_description),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
1 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Subtitles,
|
||||
title = stringResource(Res.string.onboarding_subtitles_title),
|
||||
description = stringResource(Res.string.onboarding_subtitles_description),
|
||||
)
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
if (isLastPage) {
|
||||
onFinish()
|
||||
} else {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isLastPage) Res.string.onboarding_start else Res.string.onboarding_next,
|
||||
),
|
||||
2 -> OnboardingInfoPage(
|
||||
icon = Icons.Filled.Speed,
|
||||
title = stringResource(Res.string.onboarding_playback_title),
|
||||
description = stringResource(Res.string.onboarding_playback_description),
|
||||
)
|
||||
|
||||
else -> OnboardingLevelPage(
|
||||
selectedLevel = uiState.selectedLevel,
|
||||
onLevelSelected = onLevelSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingPageIndicator(
|
||||
pageCount = PAGE_COUNT,
|
||||
currentPage = pagerState.currentPage,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
if (isLastPage) {
|
||||
onFinish()
|
||||
} else {
|
||||
scope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (isLastPage) Res.string.onboarding_start else Res.string.onboarding_next,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,3 +221,23 @@ private fun OnboardingScreenPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OnboardingContentAsDialogPreview() {
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
modifier = Modifier.sizeIn(maxWidth = DIALOG_MAX_WIDTH, maxHeight = DIALOG_MAX_HEIGHT),
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
OnboardingContent(
|
||||
uiState = OnboardingUiState(
|
||||
isCompleted = false,
|
||||
selectedLevel = OnboardingLevel.BEGINNER,
|
||||
),
|
||||
onClose = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -36,6 +36,12 @@ class OnboardingViewModel(
|
||||
selectedLevel.value = level
|
||||
}
|
||||
|
||||
fun skipOnboarding() {
|
||||
viewModelScope.launch {
|
||||
preferencesRepository.setOnboardingCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
fun completeOnboarding() {
|
||||
val level = selectedLevel.value
|
||||
viewModelScope.launch {
|
||||
|
||||
Reference in New Issue
Block a user