Full Screen Content. Jetpack Compose

Vasyl Stetsiuk
3 min read1 day ago

Full screen content which is showed over all content in any point of your app with shadowing and blurring current content

private const val DEFAULT_ANIM_DURATION = 300

@Composable
fun ProvideFullSizeContentHost(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.BottomCenter,
state: FullSizeContentHostState = rememberFullSizeContentHostState(),
backgroundBlur: Dp = 16.dp,
backgroundShadow: Color = Color.Black.copy(0.4f),
animationDuration: Int = DEFAULT_ANIM_DURATION,
enterAnimation: EnterTransition = defaultEnterAnimation(
LocalDensity.current,
animationDuration
),
exitAnimation: ExitTransition = defaultExitAnimation(animationDuration),
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalFullSizeContentHostState provides state
) {
val isAnyVisible = state.values.any { it.state.isVisible }
val animatedRatio by animateFloatAsState(
targetValue = if (isAnyVisible) 1f else 0f,
label = "",
animationSpec = tween(animationDuration)
)
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = contentAlignment
) {
Box(
modifier = Modifier
.matchParentSize()
.background(backgroundShadow.copy(backgroundShadow.alpha * animatedRatio))
.blur(backgroundBlur * animatedRatio)
) {
content()
}
state.values.forEach { data ->
AnimatedVisibility(
visible = data.state.isVisible,
enter = enterAnimation,
exit = exitAnimation
) {
data.content()
}
}
}
}
}

@Composable
fun FullSizeContent(
state: FullSizeContentState,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.background,
shape: Shape = RectangleShape,
contentColor: Color = contentColorFor(color),
shadowElevation: Dp = 0.dp,
border: BorderStroke? = null,
content: @Composable () -> Unit,
) {
val host = LocalFullSizeContentHostState.current
val fullScreenCoverContent = @Composable {
Surface(
modifier = modifier.fillMaxSize(),
color = color,
contentColor = contentColor,
border = border,
shape = shape,
shadowElevation = shadowElevation
) {
content.invoke()
}
}
DisposableEffect(Unit) {
val data = FullSizeContentData(
state = state,
content = fullScreenCoverContent
)
host.add(data)
onDispose { host.remove(data) }
}
}

class FullSizeContentState(
initialIsVisible: Boolean = false,
) {
var isVisible by mutableStateOf(initialIsVisible)

fun show() {
isVisible = true
}

fun hide() {
isVisible = false
}
}

@Composable
fun rememberFullSizeContentState(
initialIsVisible: Boolean = false,
) = remember { FullSizeContentState(initialIsVisible = initialIsVisible) }

data class FullSizeContentData(
val state: FullSizeContentState,
val id: String = UUID.randomUUID().toString(),
val content: @Composable () -> Unit,
)

class FullSizeContentHostState {

internal val values = mutableStateListOf<FullSizeContentData>()

fun add(data: FullSizeContentData) {
if (!values.contains(data)) values.add(data)
}

fun remove(data: FullSizeContentData) {
values.remove(data)
}
}

@Composable
fun rememberFullSizeContentHostState() = remember { FullSizeContentHostState() }

val LocalFullSizeContentHostState = compositionLocalOf { FullSizeContentHostState() }

private fun defaultEnterAnimation(
density: Density,
duration: Int = DEFAULT_ANIM_DURATION,
animation: Dp = 40.dp,
) = slideInVertically(
animationSpec = tween(durationMillis = duration)
) {
with(density) { animation.roundToPx() }
} + expandVertically(
expandFrom = Alignment.Top,
animationSpec = tween(durationMillis = duration)
) + fadeIn(
initialAlpha = 0.3f,
animationSpec = tween(durationMillis = duration)
)

private fun defaultExitAnimation(
duration: Int = DEFAULT_ANIM_DURATION,
) = fadeOut(
animationSpec = tween(durationMillis = duration)
)

@Preview
@Composable
private fun Test() {
ProvideFullSizeContentHost {
Column(
modifier = Modifier.fillMaxSize()
) {
Image(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
imageVector = Icons.Default.Build,
contentDescription = "",
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.tint(Color.Gray)
)
Column(
modifier = Modifier.weight(1f)
) {
val state = rememberFullSizeContentState()
val state2 = rememberFullSizeContentState()

LaunchedEffect(key1 = Unit) {
repeat(10) {
delay(2000)
if (!state.isVisible) state.show() else state.hide()
delay(2000)
if (!state2.isVisible) state2.show() else state2.hide()
}
}

FullSizeContent(
state = state,
color = Color.Transparent
) {
Image(
modifier = Modifier.fillMaxWidth(),
imageVector = Icons.Default.Email,
contentDescription = "",
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.tint(Color.Green)
)
}
FullSizeContent(
state = state2,
color = Color.Transparent
) {
Image(
modifier = Modifier.fillMaxWidth(),
imageVector = Icons.Default.CheckCircle,
contentDescription = "",
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.tint(Color.Blue)
)
}

Image(
modifier = Modifier.fillMaxWidth(),
imageVector = Icons.Default.AccountCircle,
contentDescription = "",
contentScale = ContentScale.FillWidth,
colorFilter = ColorFilter.tint(Color.Cyan)
)
}
}
}
}

--

--