Fix crash on home & add indicator on CharacterScreen

This commit is contained in:
Thomas Andres Gomez 2023-09-27 16:23:27 +02:00
parent e28d9a5dad
commit 5f8a218ac3
19 changed files with 186 additions and 75 deletions

View file

@ -0,0 +1,127 @@
package com.pixelized.rplexicon.ui.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun IndicatorStep(
modifier: Modifier = Modifier,
count: Int,
selectedIndex: Int?,
defaultSize: Dp = 4.dp,
selectedWidth: Dp = 16.dp,
indicatorSpacing: Dp = 8.dp,
defaultColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f),
selectedColor: Color = MaterialTheme.colorScheme.primary,
) {
Row(modifier = modifier) {
repeat(count) { index ->
val transition = rememberIndicatorTransition(
selected = selectedIndex == index,
defaultSize = defaultSize,
selectedWidth = selectedWidth,
defaultColor = defaultColor,
selectedColor = selectedColor,
)
Box(
modifier = Modifier
.size(size = transition.size)
.background(color = transition.color, shape = CircleShape)
)
if (index != count - 1) {
Spacer(modifier = Modifier.width(indicatorSpacing))
}
}
}
}
@Composable
private fun rememberIndicatorTransition(
selected: Boolean,
defaultSize: Dp,
selectedWidth: Dp,
defaultColor: Color,
selectedColor: Color,
): IndicatorStyle {
val transition = updateTransition(
label = "IndicatorStep selection transition",
targetState = selected
)
val color = transition.animateColor(
label = "IndicatorStep background animation",
targetValueByState = {
when (it) {
true -> selectedColor
else -> defaultColor
}
}
)
val width by transition.animateDp(
label = "IndicatorStep size animation",
targetValueByState = {
when (it) {
true -> selectedWidth
else -> defaultSize
}
}
)
return remember {
IndicatorStyle(
color = color,
size = derivedStateOf {
DpSize(width = width, height = defaultSize)
}
)
}
}
@Stable
class IndicatorStyle(
color: State<Color>,
size: State<DpSize>,
) {
val color: Color by color
val size: DpSize by size
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun IndicatorStepPreview() {
LexiconTheme {
Surface {
IndicatorStep(
modifier = Modifier.padding(all = 4.dp),
count = 3,
selectedIndex = 0,
)
}
}
}

View file

@ -48,9 +48,6 @@ val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
fun HomeNavHost( fun HomeNavHost(
navHostController: NavHostController = rememberNavController(), navHostController: NavHostController = rememberNavController(),
bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController), bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController),
lexiconListState: LazyListState,
questListState: LazyListState,
locationListState: LazyListState,
startDestination: String = LEXICON_LIST_ROUTE, startDestination: String = LEXICON_LIST_ROUTE,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
@ -108,9 +105,9 @@ fun HomeNavHost(
navController = navHostController, navController = navHostController,
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableLexicon(lazyListState = lexiconListState) composableLexicon()
composableQuests(lazyListState = questListState) composableQuests()
composableLocations(lazyListState = locationListState) composableLocations()
} }
} }
} }

View file

@ -41,11 +41,7 @@ fun ScreenNavHost(
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableAuthentication() composableAuthentication()
composableHome( composableHome()
lexiconListState = lexiconListState,
questListState = questListState,
locationListState = locationListState
)
composableLexiconDetail() composableLexiconDetail()
composableLexiconSearch() composableLexiconSearch()
composableQuestDetail() composableQuestDetail()

View file

@ -12,16 +12,12 @@ private const val ROUTE = "lexicon"
const val LEXICON_LIST_ROUTE = ROUTE const val LEXICON_LIST_ROUTE = ROUTE
fun NavGraphBuilder.composableLexicon( fun NavGraphBuilder.composableLexicon() {
lazyListState: LazyListState,
) {
animatedComposable( animatedComposable(
route = LEXICON_LIST_ROUTE, route = LEXICON_LIST_ROUTE,
animation = NavigationAnimation.Fade, animation = NavigationAnimation.Fade,
) { ) {
LexiconScreen( LexiconScreen()
lazyListState = lazyListState
)
} }
} }

View file

@ -12,16 +12,12 @@ private const val ROUTE = "locations"
const val LOCATION_LIST_ROUTE = ROUTE const val LOCATION_LIST_ROUTE = ROUTE
fun NavGraphBuilder.composableLocations( fun NavGraphBuilder.composableLocations() {
lazyListState: LazyListState,
) {
animatedComposable( animatedComposable(
route = LOCATION_LIST_ROUTE, route = LOCATION_LIST_ROUTE,
animation = NavigationAnimation.Fade, animation = NavigationAnimation.Fade,
) { ) {
LocationScreen( LocationScreen()
lazyListState = lazyListState,
)
} }
} }

View file

@ -12,16 +12,12 @@ private const val ROUTE = "quests"
const val QUEST_LIST_ROUTE = ROUTE const val QUEST_LIST_ROUTE = ROUTE
fun NavGraphBuilder.composableQuests( fun NavGraphBuilder.composableQuests() {
lazyListState: LazyListState,
) {
animatedComposable( animatedComposable(
route = QUEST_LIST_ROUTE, route = QUEST_LIST_ROUTE,
animation = NavigationAnimation.Fade, animation = NavigationAnimation.Fade,
) { ) {
QuestListScreen( QuestListScreen()
lazyListState = lazyListState,
)
} }
} }

View file

@ -1,6 +1,5 @@
package com.pixelized.rplexicon.ui.navigation.screens package com.pixelized.rplexicon.ui.navigation.screens
import androidx.compose.foundation.lazy.LazyListState
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
@ -12,20 +11,12 @@ private const val ROUTE = "home"
const val HOME_ROUTE = ROUTE const val HOME_ROUTE = ROUTE
fun NavGraphBuilder.composableHome( fun NavGraphBuilder.composableHome() {
lexiconListState: LazyListState,
questListState: LazyListState,
locationListState: LazyListState,
) {
animatedComposable( animatedComposable(
route = HOME_ROUTE, route = HOME_ROUTE,
animation = NavigationAnimation.Push, animation = NavigationAnimation.Push,
) { ) {
HomeNavHost( HomeNavHost()
lexiconListState = lexiconListState,
questListState = questListState,
locationListState = locationListState
)
} }
} }

View file

@ -89,17 +89,18 @@ fun CharacterSheetScreen(
refreshing = false, refreshing = false,
onRefresh = { scope.launch { viewModel.update(force = true) } }, onRefresh = { scope.launch { viewModel.update(force = true) } },
) )
val pagerState = rememberPagerState {
val haveSheet = proficiencyViewModel.sheet.value != null
val haveAction = attackViewModel.attacks.value.isNotEmpty()
val haveSpell = spellsViewModel.spells.value.isNotEmpty()
(if (haveSheet) 1 else 0) + (if (haveAction || haveSpell) 1 else 0)
}
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
CharacterSheetContent( CharacterSheetContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
pagerState = rememberPagerState { pagerState = pagerState,
val haveSheet = proficiencyViewModel.sheet.value != null
val haveAction = attackViewModel.attacks.value.isNotEmpty()
val haveSpell = spellsViewModel.spells.value.isNotEmpty()
(if (haveSheet) 1 else 0) + (if (haveAction || haveSpell) 1 else 0)
},
sheetState = sheetState, sheetState = sheetState,
refreshState = refresh, refreshState = refresh,
onRefresh = { onRefresh = {
@ -111,6 +112,7 @@ fun CharacterSheetScreen(
header = { header = {
CharacterSheetHeader( CharacterSheetHeader(
modifier = Modifier.zIndex(1f), modifier = Modifier.zIndex(1f),
pagerState = pagerState,
header = viewModel.header, header = viewModel.header,
) )
}, },
@ -223,7 +225,9 @@ private fun CharacterSheetContent(
header() header()
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.weight(1f),
) { ) {
HorizontalPager( HorizontalPager(
modifier = Modifier modifier = Modifier

View file

@ -2,10 +2,14 @@ package com.pixelized.rplexicon.ui.screens.character.composable.character
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -14,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.composable.IndicatorStep
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
@ -24,32 +29,45 @@ data class CharacterSheetHeaderUio(
val speed: LabelPointUio, val speed: LabelPointUio,
) )
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CharacterSheetHeader( fun CharacterSheetHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
pagerState: PagerState,
header: State<CharacterSheetHeaderUio?>, header: State<CharacterSheetHeaderUio?>,
) { ) {
Surface( Surface(
modifier = modifier, modifier = modifier,
shadowElevation = 4.dp, shadowElevation = 4.dp,
) { ) {
Row( Column(
modifier = Modifier horizontalAlignment = Alignment.CenterHorizontally,
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally
),
) { ) {
header.value?.armorClass?.let { LabelPoint(label = it) } Row(
header.value?.hitPoint?.let { LabelPoint(label = it) } modifier = Modifier
header.value?.speed?.let { LabelPoint(label = it) } .fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally
),
) {
header.value?.armorClass?.let { LabelPoint(label = it) }
header.value?.hitPoint?.let { LabelPoint(label = it) }
header.value?.speed?.let { LabelPoint(label = it) }
}
IndicatorStep(
modifier = Modifier.padding(vertical = 4.dp),
count = pagerState.pageCount,
selectedIndex = pagerState.currentPage,
)
} }
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
@ -57,7 +75,8 @@ fun CharacterSheetHeaderPreview() {
LexiconTheme { LexiconTheme {
Surface { Surface {
CharacterSheetHeader( CharacterSheetHeader(
header = rememberCharacterHeaderStatePreview() header = rememberCharacterHeaderStatePreview(),
pagerState = rememberPagerState { 2 },
) )
} }
} }

View file

@ -37,7 +37,6 @@ fun LabelPoint(
label: LabelPointUio, label: LabelPointUio,
) { ) {
Column( Column(
modifier = modifier.padding(all = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {

View file

@ -14,8 +14,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State

View file

@ -2,7 +2,6 @@ package com.pixelized.rplexicon.ui.screens.character.pages.chooser
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
@ -14,7 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellLevelItem import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellLevelItem

View file

@ -52,11 +52,11 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun LexiconScreen( fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(), viewModel: LexiconViewModel = hiltViewModel()
lazyListState: LazyListState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val lazyListState = rememberLazyListState()
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(
refreshing = false, refreshing = false,

View file

@ -38,9 +38,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun LocationScreen( fun LocationScreen(
viewModel: LocationViewModel = hiltViewModel(), viewModel: LocationViewModel = hiltViewModel(),
lazyListState: LazyListState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.repository.data.LocationRepository import com.pixelized.rplexicon.repository.data.LocationRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure

View file

@ -38,10 +38,10 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun QuestListScreen( fun QuestListScreen(
viewModel: QuestListViewModel = hiltViewModel(), viewModel: QuestListViewModel = hiltViewModel(),
lazyListState: LazyListState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val lazyListState = rememberLazyListState()
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(
refreshing = false, refreshing = false,

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.repository.data.QuestRepository import com.pixelized.rplexicon.repository.data.QuestRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure

View file

@ -4,13 +4,10 @@ import android.app.Application
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Alteration
import com.pixelized.rplexicon.model.CharacterSheet import com.pixelized.rplexicon.model.CharacterSheet
import com.pixelized.rplexicon.model.Roll import com.pixelized.rplexicon.model.Roll
import com.pixelized.rplexicon.repository.data.AlterationRepository
import com.pixelized.rplexicon.repository.data.CharacterSheetRepository import com.pixelized.rplexicon.repository.data.CharacterSheetRepository
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollAlterationUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio

View file

@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface