add onboarding pages

This commit is contained in:
Owen LeJeune
2022-09-12 18:05:38 -04:00
parent 885d043b09
commit f8b6b67816
9 changed files with 268 additions and 38 deletions

View File

@@ -13,7 +13,7 @@ class AppRoutingActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (preferences.firstLaunch) {
if (preferences.firstLaunchTesting || preferences.firstLaunch) {
launchActivity(OnboardingActivity::class.java)
} else {
launchActivity(MainActivity::class.java)

View File

@@ -7,15 +7,13 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -63,16 +61,38 @@ class OnboardingActivity: MonetCompatActivity() {
Column(
modifier = Modifier.background(color = MaterialTheme.colorScheme.background)
) {
val isFirstPage = pagerState.currentPage == 0
val isLastPage = pagerState.currentPage == OnboardingPage.values().size - 1
if (!isFirstPage) {
IconButton(
modifier = Modifier.padding(12.dp),
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
}
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onBackground
)
}
}
val nextButtonEnabled = remember { mutableStateOf(true) }
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
// .background(MaterialTheme.colorScheme.primary),
count = OnboardingPage.values().size,
userScrollEnabled = false
) { page ->
OnboardingPageUi(page = OnboardingPage[page])
Box(modifier = Modifier.padding(start = 12.dp, end = 24.dp)) {
OnboardingPageUi(page = OnboardingPage[page], nextButtonEnabled)
}
}
Box(
@@ -80,6 +100,17 @@ class OnboardingActivity: MonetCompatActivity() {
.fillMaxWidth()
.padding(all = 12.dp)
) {
Button(
modifier = Modifier.align(Alignment.CenterStart),
enabled = true,
onClick = {
preferences.firstLaunch = false
launchActivity(MainActivity::class.java)
}
) {
Text(text = "Skip")
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
@@ -88,10 +119,10 @@ class OnboardingActivity: MonetCompatActivity() {
activeColor = MaterialTheme.colorScheme.secondary
)
val isLastPage = pagerState.currentPage == OnboardingPage.values().size - 1
Button(
modifier = Modifier.align(Alignment.CenterEnd),
shape = CircleShape,
enabled = nextButtonEnabled.value,
onClick = {
if (isLastPage) {
preferences.firstLaunch = false
@@ -103,7 +134,7 @@ class OnboardingActivity: MonetCompatActivity() {
}
}
) {
if (isLastPage) {
if (!isLastPage) {
Text(stringResource(id = R.string.get_started))
} else {
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = null)
@@ -114,7 +145,7 @@ class OnboardingActivity: MonetCompatActivity() {
}
@Composable
fun OnboardingPageUi(page: OnboardingPage) {
fun OnboardingPageUi(page: OnboardingPage, nextButtonEnabled: MutableState<Boolean>) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -122,8 +153,8 @@ class OnboardingActivity: MonetCompatActivity() {
Image(
painter = painterResource(id = it),
contentDescription = null,
modifier = Modifier.size(200.dp),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.secondary)
modifier = Modifier.size(100.dp),
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary)
)
Spacer(modifier = Modifier.height(20.dp))
}
@@ -140,8 +171,8 @@ class OnboardingActivity: MonetCompatActivity() {
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(12.dp))
page.additionalContent(this@OnboardingActivity)
Spacer(modifier = Modifier.height(50.dp))
page.additionalContent(this@OnboardingActivity, nextButtonEnabled)
}
}

View File

@@ -126,7 +126,7 @@ class AppPreferences(context: Context) {
set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) }
var firstLaunch: Boolean
get() = if (BuildConfig.DEBUG) firstLaunchTesting else preferences.getBoolean(FIRST_LAUNCH, true)
get() = preferences.getBoolean(FIRST_LAUNCH, true)
set(value) { preferences.put(FIRST_LAUNCH, value) }
val useV4ApiDefault: Boolean = false

View File

@@ -119,6 +119,7 @@ fun RadioButtonPreference(
selected: Boolean,
onClick: () -> Unit,
title: String,
description: String? = null,
icon: ImageVector,
titleTextColor: Color = MaterialTheme.colorScheme.onBackground,
disabledTextColor: Color = MaterialTheme.colorScheme.outline
@@ -142,19 +143,31 @@ fun RadioButtonPreference(
)
val titleColor = if (enabled) titleTextColor else disabledTextColor
Text(
Column(
modifier = Modifier
.align(Alignment.CenterVertically),
.align(Alignment.CenterVertically)
.padding(end = 36.dp)
.fillMaxWidth()
) {
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = titleColor,
fontSize = 20.sp
)
description?.let {
Text(
text = description,
color = titleColor,
fontSize = 14.sp
)
}
}
Spacer(modifier = Modifier.weight(1f))
RadioButton(
modifier = Modifier.align(Alignment.CenterVertically),
modifier = Modifier.align(Alignment.CenterVertically).weight(1f),
selected = selected,
onClick = null
)

View File

@@ -173,10 +173,7 @@ private fun BackdropGallery(
with (pagerState) {
val target = if (currentPage < pageCount - 1) currentPage + 1 else 0
animateScrollToPage(
page = target,
pageOffset = 0f
)
pagerState.animateScrollToPage(target)
}
}
}

View File

@@ -1,28 +1,197 @@
package com.owenlejeune.tvtime.ui.screens.onboarding
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Brightness6
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.outlined.DarkMode
import androidx.compose.material.icons.outlined.LightMode
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kieronquinn.monetcompat.core.MonetCompat
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.preferences.AppPreferences
import com.owenlejeune.tvtime.ui.components.RadioButtonPreference
import com.owenlejeune.tvtime.ui.components.SearchBar
import com.owenlejeune.tvtime.ui.components.SwitchPreference
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
sealed class OnboardingPage(
val order: Int,
@StringRes val title: Int,
@StringRes val description: Int,
@DrawableRes val image: Int? = null,
val additionalContent: @Composable (AppCompatActivity) -> Unit = {}
val additionalContent: @Composable (AppCompatActivity, MutableState<Boolean>) -> Unit = { a, e -> }
) {
companion object {
fun values() = listOf(ExamplePage)
companion object: KoinComponent {
private val preferences: AppPreferences by inject()
fun values() = listOf(IntroPage, SearchViewSettings, SearchTypeSettings, DarkModePage, SystemColorPage).sortedBy { it.order }
operator fun get(i: Int) = values()[i]
}
object ExamplePage: OnboardingPage(
R.string.app_name,
R.string.example_page_desc,
R.drawable.ic_launcher_foreground
object IntroPage: OnboardingPage(
order = 0,
title = R.string.app_name,
description = R.string.intro_page_desc,
image = R.drawable.ic_launcher_foreground
)
object SignInPage: OnboardingPage(
order = 1,
title = R.string.sign_in_page_title,
description = R.string.sign_in_page_description,
image = null,
additionalContent = @Composable { activity, nextButtonEnabled ->
}
)
object SearchViewSettings: OnboardingPage(
order = 2,
title = R.string.search_page_title,
description = R.string.search_page_description,
image = R.drawable.ic_search,
additionalContent = @Composable { _, _ ->
val persistentSearch = remember { mutableStateOf(preferences.showSearchBar) }
if (persistentSearch.value) {
SearchBar(placeholder = "") {}
} else {
FloatingActionButton(onClick = {}) {
Icon(
Icons.Filled.Search,
stringResource(id = R.string.preference_heading_search)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
SwitchPreference(
titleText = stringResource(R.string.preferences_persistent_search_title),
subtitleText = stringResource(R.string.preferences_persistent_search_subtitle),
checkState = persistentSearch.value,
onCheckedChange = { isChecked ->
persistentSearch.value = isChecked
preferences.showSearchBar = isChecked
}
)
}
)
object SearchTypeSettings: OnboardingPage(
order = 3,
title = R.string.search_type_title,
description = R.string.search_type_description,
image = R.drawable.ic_search,
additionalContent = @Composable { _, _ ->
val multiSearch = remember { mutableStateOf(preferences.multiSearch) }
SwitchPreference(
titleText = stringResource(R.string.preference_multi_search_title),
subtitleText = stringResource(R.string.preference_multi_search_subtitle),
checkState = multiSearch.value,
onCheckedChange = { isChecked ->
multiSearch.value = isChecked
preferences.multiSearch = isChecked
}
)
}
)
object DarkModePage: OnboardingPage(
order = 4,
title = R.string.dark_mode_page_title,
description = R.string.dark_mode_page_description,
image = R.drawable.ic_baseline_dark_mode,
additionalContent = @Composable { activity, _ ->
val selectedValue = remember { mutableStateOf(preferences.darkTheme) }
val isSelected: (Int) -> Boolean = { selectedValue.value == it }
val onChangeState: (Int) -> Unit = {
selectedValue.value = it
preferences.darkTheme = it
activity.recreate()
}
RadioButtonPreference(
selected = isSelected(AppPreferences.DarkMode.Automatic.ordinal),
title = stringResource(R.string.preference_dark_mode_follow_system_label),
description = "Automatically change between dark and light mode",
icon = Icons.Filled.Brightness6,
onClick = {
onChangeState(AppPreferences.DarkMode.Automatic.ordinal)
}
)
RadioButtonPreference(
selected = isSelected(AppPreferences.DarkMode.Light.ordinal),
title = stringResource(R.string.preference_dark_mode_light_mode_label),
icon = Icons.Outlined.LightMode,
onClick = {
onChangeState(AppPreferences.DarkMode.Light.ordinal)
}
)
RadioButtonPreference(
selected = isSelected(AppPreferences.DarkMode.Dark.ordinal),
title = stringResource(R.string.preference_dark_mode_dark_mode_label),
icon = Icons.Outlined.DarkMode,
onClick = {
onChangeState(AppPreferences.DarkMode.Dark.ordinal)
}
)
}
)
object SystemColorPage: OnboardingPage(
order = 4,
title = R.string.system_color_page_title,
description = R.string.system_color_page_description,
image = R.drawable.ic_baseline_palette,
additionalContent = @Composable { activity, _ ->
val useWallpaperColors = remember { mutableStateOf(preferences.useWallpaperColors) }
SwitchPreference(
titleText = stringResource(R.string.preferences_use_wallpaper_colors_title),
subtitleText = stringResource(R.string.preferences_use_wallpaper_colors_subtitle),
checkState = useWallpaperColors.value,
onCheckedChange = { isChecked ->
useWallpaperColors.value = isChecked
preferences.useWallpaperColors = isChecked
activity.recreate()
}
)
val isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
if (isSystemColorsSupported) {
val useSystemColors = remember { mutableStateOf(preferences.useSystemColors) }
SwitchPreference(
titleText = stringResource(id = R.string.preference_system_colors_heading),
subtitleText = stringResource(R.string.preference_system_colors_subtitle),
checkState = useSystemColors.value,
onCheckedChange = { isChecked ->
useSystemColors.value = isChecked
preferences.useSystemColors = isChecked
MonetCompat.useSystemColorsOnAndroid12 = isChecked
MonetCompat.getInstance().updateMonetColors()
},
enabled = useWallpaperColors.value
)
}
}
)
}

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View File

@@ -132,8 +132,18 @@
<string name="intent_route_auth_return">tvtime.auth.return</string>
<!-- onboarding -->
<string name="get_started">Get started</string>
<string name="example_page_desc">This is an example</string>
<string name="get_started">Start</string>
<string name="intro_page_desc">This is an example</string>
<string name="sign_in_page_title">Sign into TMDb</string>
<string name="sign_in_page_description">Sign into TMDb to add content to your favourites or watchlist and leave reviews</string>
<string name="search_page_title">Search type</string>
<string name="search_page_description">Show search view with a search bar or fab</string>
<string name="search_type_title">Search mode</string>
<string name="search_type_description">Search across individual media types, or across all at once</string>
<string name="dark_mode_page_title">Dark mode</string>
<string name="dark_mode_page_description"> </string>
<string name="system_color_page_title">Theme management</string>
<string name="system_color_page_description">Theme the app using colors pulled from your wallpaper or the color theme built into the app</string>
<!-- search results -->
<string name="search_result_tv_series">TV Series</string>