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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (preferences.firstLaunch) { if (preferences.firstLaunchTesting || preferences.firstLaunch) {
launchActivity(OnboardingActivity::class.java) launchActivity(OnboardingActivity::class.java)
} else { } else {
launchActivity(MainActivity::class.java) launchActivity(MainActivity::class.java)

View File

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

View File

@@ -126,7 +126,7 @@ class AppPreferences(context: Context) {
set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) } set(value) { preferences.put(FIRST_LAUNCH_TESTING, value) }
var firstLaunch: Boolean 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) } set(value) { preferences.put(FIRST_LAUNCH, value) }
val useV4ApiDefault: Boolean = false val useV4ApiDefault: Boolean = false

View File

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

View File

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

View File

@@ -1,28 +1,197 @@
package com.owenlejeune.tvtime.ui.screens.onboarding package com.owenlejeune.tvtime.ui.screens.onboarding
import android.os.Build
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity 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.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.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( sealed class OnboardingPage(
val order: Int,
@StringRes val title: Int, @StringRes val title: Int,
@StringRes val description: Int, @StringRes val description: Int,
@DrawableRes val image: Int? = null, @DrawableRes val image: Int? = null,
val additionalContent: @Composable (AppCompatActivity) -> Unit = {} val additionalContent: @Composable (AppCompatActivity, MutableState<Boolean>) -> Unit = { a, e -> }
) { ) {
companion object { companion object: KoinComponent {
fun values() = listOf(ExamplePage) private val preferences: AppPreferences by inject()
fun values() = listOf(IntroPage, SearchViewSettings, SearchTypeSettings, DarkModePage, SystemColorPage).sortedBy { it.order }
operator fun get(i: Int) = values()[i] operator fun get(i: Int) = values()[i]
} }
object ExamplePage: OnboardingPage( object IntroPage: OnboardingPage(
R.string.app_name, order = 0,
R.string.example_page_desc, title = R.string.app_name,
R.drawable.ic_launcher_foreground 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> <string name="intent_route_auth_return">tvtime.auth.return</string>
<!-- onboarding --> <!-- onboarding -->
<string name="get_started">Get started</string> <string name="get_started">Start</string>
<string name="example_page_desc">This is an example</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 --> <!-- search results -->
<string name="search_result_tv_series">TV Series</string> <string name="search_result_tv_series">TV Series</string>