Skip to content

Instantly share code, notes, and snippets.

@fabriciovergara
Created July 11, 2023 12:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fabriciovergara/f5f2c1d2877269b37ecd5844d127f868 to your computer and use it in GitHub Desktop.
Save fabriciovergara/f5f2c1d2877269b37ecd5844d127f868 to your computer and use it in GitHub Desktop.
InAppNotification
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animate
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.with
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Slider
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.Text
import androidx.compose.material.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.compositionLocalOf
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.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
private typealias InAppNotificationContent = @Composable InAppNotificationScope.() -> Unit
val LocalInAppNotificationController = compositionLocalOf<InAppNotificationController> {
error("LocalInAppNotificationScope must be provided")
}
interface InAppNotificationScope {
/**
* Progress from 0f to 1f, indicate the countdown to hide the notification
*/
val progress: Float
/**
* Cancel the current displaying notification.
* Progress value will automatically be set to 1f
*/
fun cancel()
/**
* Pause the current displaying notification.
* Progress value will be frozen until resume is called again.
*/
fun pause()
/**
* Resume current displaying notification.
*/
fun resume()
}
interface InAppNotificationController {
fun show(duration: Long = TimeUnit.SECONDS.toMillis(5), content: InAppNotificationContent): Job
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun InAppNotificationHost(
content: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val state = remember { InAppNotificationControllerImpl(coroutineScope) }
val current = remember { mutableStateOf<InAppNotificationScopeImpl?>(null) }
LaunchedEffect(state) {
state.flow.collect { next ->
try {
if (next != null) {
val completable = CompletableDeferred<Unit>(coroutineContext.job)
current.value = InAppNotificationScopeImpl(coroutineScope, next, completable)
current.value?.resume()
completable.join()
}
} finally {
current.value = null
}
}
}
CompositionLocalProvider(
LocalInAppNotificationController provides state
) {
Box {
content()
AnimatedContent(
targetState = current.value,
transitionSpec = { slideInVertically { -it } with slideOutVertically { -it } }
) { value ->
val dismissState = rememberDismissState()
val dismissValue = dismissState.currentValue
LaunchedEffect(dismissValue) {
if (dismissValue != DismissValue.Default) {
delay(100)
value?.cancel()
}
}
SwipeToDismiss(
modifier = Modifier.fillMaxWidth(),
state = dismissState,
dismissThresholds = { FractionalThreshold(0.5f) },
background = { }
) {
Box(
modifier = Modifier.animateEnterExit(
enter = slideInVertically { -it },
exit = slideOutVertically { -it }
)
) {
value?.Render()
}
}
}
}
}
}
private class InAppNotificationScopeImpl(
private val scope: CoroutineScope,
private val value: InAppNotificationStateValue,
private val completable: CompletableDeferred<Unit>
) : InAppNotificationScope {
private var job: Job? = null
private val progressState = mutableStateOf(0f)
override val progress: Float get() = progressState.value
override fun cancel() {
job?.cancel()
onUpdateProgress(1f)
}
override fun pause() {
job?.cancel()
}
override fun resume() {
if (job?.isActive == true) {
return
}
if (progressState.value == 1f) {
return
}
job = scope.launch {
val remainDuration = ((1f - progressState.value) * value.duration).toInt()
val spec = tween<Float>(delayMillis = 0, durationMillis = remainDuration, easing = LinearEasing)
animate(initialValue = progressState.value, targetValue = 1f, initialVelocity = 0f, animationSpec = spec) { value, _ ->
onUpdateProgress(value)
}
}
}
private fun onUpdateProgress(value: Float) {
progressState.value = value
if (value == 1f) {
completable.complete(Unit)
}
}
@Composable
fun Render() {
value.content(this)
}
}
private class InAppNotificationControllerImpl(
private val scope: CoroutineScope
) : InAppNotificationController {
val flow = MutableSharedFlow<InAppNotificationStateValue?>(onBufferOverflow = BufferOverflow.SUSPEND)
override fun show(duration: Long, content: InAppNotificationContent): Job = scope.launch {
flow.emit(InAppNotificationStateValue(content, duration))
}
}
private data class InAppNotificationStateValue(
val content: InAppNotificationContent,
val duration: Long
)
@Preview
@Composable
fun Usage() {
MaterialTheme {
InAppNotificationHost {
Scaffold {
Box(
modifier = Modifier
.padding(it)
.fillMaxSize()
) {
val inAppNotificationController = LocalInAppNotificationController.current
Button(
modifier = Modifier.align(Alignment.Center),
onClick = {
inAppNotificationController.show {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.Black)
.padding(8.dp)
) {
Button(
onClick = { cancel() }
) {
Text("Ok")
}
Slider(
modifier = Modifier
.fillMaxWidth(),
value = progress,
onValueChange = {}
)
}
}
}
) {
Text("Show InAppNotification")
}
}
}
}
}
}
@fabriciovergara
Copy link
Author

Screen.Recording.2023-07-11.at.14.20.11.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment