Add a FAB on the Lexicon

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-16 15:53:16 +02:00
parent c5fb8bf99e
commit 54e09e5f1d
6 changed files with 262 additions and 11 deletions

View file

@ -0,0 +1,182 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun FloatingActionButton(
modifier: Modifier = Modifier,
expended: Boolean,
enabled: Boolean = true,
innerSpacing: Dp = 16.dp,
contentPadding: PaddingValues = FlyingBlueFloatingActionButtonDefault.ContentPadding,
elevation: ButtonElevation? = ButtonDefaults.elevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp,
disabledElevation = 0.dp,
),
shape: Shape = CircleShape,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
colors: ButtonColors = ButtonDefaults.buttonColors(),
onClick: () -> Unit,
icon: @Composable (RowScope.() -> Unit),
text: @Composable (RowScope.() -> Unit),
) {
LocalButton(
modifier = modifier,
onClick = onClick,
enabled = enabled,
interactionSource = interactionSource,
elevation = elevation,
shape = shape,
colors = colors,
contentPadding = contentPadding,
content = {
FabContent(
expended = expended,
innerSpacing = innerSpacing,
icon = icon,
text = text,
)
},
)
}
@Composable
private fun BoxWithConstraintsScope.FabContent(
expended: Boolean,
innerSpacing: Dp,
icon: @Composable (RowScope.() -> Unit),
text: @Composable (RowScope.() -> Unit),
) {
val width by animateDpAsState(
label = "FabContentWidth",
targetValue = if (expended) maxWidth else minWidth,
animationSpec = when (expended) {
true -> tween(durationMillis = 300, easing = FastOutSlowInEasing)
else -> tween(durationMillis = 300, delayMillis = 100, easing = FastOutSlowInEasing)
},
)
Row(
modifier = Modifier.size(
height = 56.dp,
width = width
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
icon()
AnimatedVisibility(
visible = expended,
enter = fadeIn(
tween(durationMillis = 300, delayMillis = 100, easing = FastOutSlowInEasing)
),
exit = fadeOut(
tween(durationMillis = 300, easing = FastOutSlowInEasing)
) + shrinkHorizontally(
tween(durationMillis = 300, easing = FastOutSlowInEasing)
),
) {
Row {
Spacer(modifier = Modifier.width(innerSpacing))
text()
}
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun LocalButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = FlyingBlueFloatingActionButtonDefault.ContentPadding,
content: @Composable BoxWithConstraintsScope.() -> Unit
) {
val contentColor by colors.contentColor(enabled)
Surface(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
color = colors.backgroundColor(enabled).value,
contentColor = contentColor.copy(alpha = 1f),
border = border,
elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp,
interactionSource = interactionSource,
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(
value = MaterialTheme.typography.button
) {
BoxWithConstraints(
Modifier
.defaultMinSize(
minWidth = FlyingBlueFloatingActionButtonDefault.MinWidth,
minHeight = FlyingBlueFloatingActionButtonDefault.MinHeight
)
.padding(contentPadding),
content = content
)
}
}
}
}
object FlyingBlueFloatingActionButtonDefault {
val ContentPadding = PaddingValues(all = 0.dp)
val MinWidth = 56.dp
val MinHeight = 56.dp
}
@Composable
@Preview
private fun FloatingActionButtonPreview(
@PreviewParameter(FloatingActionButtonPreviewProvider::class) expended: Boolean
) {
LexiconTheme {
FloatingActionButton(
expended = expended,
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = null,
)
},
text = {
Text(text = "Floating Action Button")
},
onClick = { }
)
}
}
private class FloatingActionButtonPreviewProvider : PreviewParameterProvider<Boolean> {
override val values: Sequence<Boolean> = sequenceOf(false, true)
}

View file

@ -1,5 +1,7 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
@ -9,12 +11,16 @@ private const val ROUTE = "lexicon"
const val LEXICON_ROUTE = ROUTE
fun NavGraphBuilder.composableLexicon() {
fun NavGraphBuilder.composableLexicon(
lazyListState: LazyListState,
) {
animatedComposable(
route = LEXICON_ROUTE,
animation = NavigationAnimation.Fade,
) {
LexiconScreen()
LexiconScreen(
lazyListState = lazyListState
)
}
}

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
@ -19,6 +20,8 @@ fun ScreenNavHost(
navHostController: NavHostController = rememberAnimatedNavController(),
startDestination: String = AUTHENTICATION_ROUTE,
) {
val lexiconListState = rememberLazyListState()
CompositionLocalProvider(
LocalScreenNavHost provides navHostController,
) {
@ -27,7 +30,7 @@ fun ScreenNavHost(
startDestination = startDestination,
) {
composableAuthentication()
composableLexicon()
composableLexicon(lazyListState = lexiconListState)
composableCharacterDetail()
}
}

View file

@ -13,13 +13,17 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -28,17 +32,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.FloatingActionButton
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default
@ -60,6 +68,7 @@ sealed class LexiconErrorUio {
@Composable
fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(),
lazyListState: LazyListState,
) {
val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current
@ -73,11 +82,22 @@ fun LexiconScreen(
},
)
val isFabExpended = remember {
derivedStateOf {
lazyListState.canScrollForward.not() && viewModel.items.value.isNotEmpty()
}
}
Surface {
LexiconScreenContent(
items = viewModel.items,
refresh = refresh,
lazyColumnState = lazyListState,
refreshState = refresh,
refreshing = viewModel.isLoading,
isFabExpended = isFabExpended,
onSearch = {
},
onItem = {
screen.navigateToCharacterDetail(id = it.id)
},
@ -96,9 +116,12 @@ fun LexiconScreen(
@Composable
private fun LexiconScreenContent(
modifier: Modifier = Modifier,
refresh: PullRefreshState,
lazyColumnState: LazyListState,
refreshState: PullRefreshState,
refreshing: State<Boolean>,
isFabExpended: State<Boolean>,
items: State<List<LexiconItemUio>>,
onSearch: () -> Unit,
onItem: (LexiconItemUio) -> Unit,
) {
Scaffold(
@ -109,6 +132,28 @@ private fun LexiconScreenContent(
Text(text = stringResource(id = R.string.app_name))
},
)
},
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.padding(start = 32.dp),
expended = isFabExpended.value,
onClick = onSearch,
icon = {
Icon(
tint = MaterialTheme.colorScheme.onPrimary,
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = null,
)
},
text = {
val typography = MaterialTheme.typography
Text(
color = MaterialTheme.colorScheme.onPrimary,
style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) },
text = "Rechercher",
)
},
)
}
) {
Box(
@ -118,8 +163,12 @@ private fun LexiconScreenContent(
LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refresh),
contentPadding = PaddingValues(vertical = 8.dp),
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = PaddingValues(
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp
),
) {
items(items = items.value) { item ->
LexiconItem(
@ -134,7 +183,7 @@ private fun LexiconScreenContent(
PullRefreshIndicator(
refreshing = refreshing.value,
state = refresh,
state = refreshState,
)
}
}
@ -177,11 +226,13 @@ private fun LexiconScreenContentPreview() {
Surface {
LexiconScreenContent(
modifier = Modifier.fillMaxSize(),
refresh = rememberPullRefreshState(
lazyColumnState = rememberLazyListState(),
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = {},
),
refreshing = remember { mutableStateOf(false) },
isFabExpended = remember { mutableStateOf(true) },
items = remember {
mutableStateOf(
listOf(
@ -195,6 +246,7 @@ private fun LexiconScreenContentPreview() {
)
)
},
onSearch = { },
onItem = { },
)
}

View file

@ -7,6 +7,7 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
@ -14,13 +15,15 @@ import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
tertiary = Pink80,
onPrimary = Color.White,
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
tertiary = Pink40,
onPrimary = Color.White,
)
@Composable

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="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>