Add a FAB on the Lexicon
This commit is contained in:
parent
c5fb8bf99e
commit
54e09e5f1d
6 changed files with 262 additions and 11 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
app/src/main/res/drawable/ic_baseline_search_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_search_24.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue