Skip to content

Instantly share code, notes, and snippets.

@hiteshchopra11
Created May 10, 2023 21:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hiteshchopra11/dc9aa026be206c0e5d907d0f33f46a54 to your computer and use it in GitHub Desktop.
Save hiteshchopra11/dc9aa026be206c0e5d907d0f33f46a54 to your computer and use it in GitHub Desktop.
package com.example.samplecompose
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.material.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.AccessibilityManager
import androidx.compose.ui.platform.LocalAccessibilityManager
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.resume
@Stable
class GenericSnackbarHostState<T> {
private val mutex = Mutex()
var currentSnackbarData by mutableStateOf<GenericSnackbarData<T>?>(null)
private set
suspend fun showSnackbar(
message: T,
actionLabel: String? = null,
duration: GenericSnackbarDuration = GenericSnackbarDuration.Short
): SnackbarResult = mutex.withLock {
try {
return suspendCancellableCoroutine { continuation ->
currentSnackbarData = GenericSnackbarDataImpl(message, actionLabel, duration, continuation)
}
} finally {
currentSnackbarData = null
}
}
@Stable
private class GenericSnackbarDataImpl<T>(
override val message: T,
override val actionLabel: String?,
override val duration: GenericSnackbarDuration,
private val continuation: CancellableContinuation<SnackbarResult>
) : GenericSnackbarData<T> {
override fun performAction() {
if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
}
override fun dismiss() {
if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
}
}
}
@Composable
fun <T> GenericSnackBarHost(
hostState: GenericSnackbarHostState<T>,
modifier: Modifier = Modifier,
snackbar: @Composable (GenericSnackbarData<T>) -> Unit
) {
val currentSnackbarData = hostState.currentSnackbarData
val accessibilityManager = LocalAccessibilityManager.current
LaunchedEffect(currentSnackbarData) {
if (currentSnackbarData != null) {
val duration = currentSnackbarData.duration.toMillis(
currentSnackbarData.actionLabel != null,
accessibilityManager
)
delay(duration)
currentSnackbarData.dismiss()
}
}
FadeInFadeOutWithScale(
current = hostState.currentSnackbarData,
modifier = modifier,
content = snackbar
)
}
@Composable
private fun <T> FadeInFadeOutWithScale(
current: GenericSnackbarData<T>?,
modifier: Modifier = Modifier,
content: @Composable (GenericSnackbarData<T>) -> Unit
) {
val state = remember { FadeInFadeOutState<GenericSnackbarData<T>?>() }
if (current != state.current) {
state.current = current
val keys = state.items.map { it.key }.toMutableList()
if (!keys.contains(current)) {
keys.add(current)
}
state.items.clear()
keys.filterNotNull().mapTo(state.items) { key ->
FadeInFadeOutAnimationItem(key) { children ->
val isVisible = key == current
val duration = if (isVisible) SnackbarFadeInMillis else SnackbarFadeOutMillis
val delay = SnackbarFadeOutMillis + SnackbarInBetweenDelayMillis
val animationDelay = if (isVisible && keys.filterNotNull().size != 1) delay else 0
val opacity = animatedOpacity(
animation = tween(
easing = LinearEasing,
delayMillis = animationDelay,
durationMillis = duration
),
visible = isVisible,
onAnimationFinish = {
if (key != state.current) {
// leave only the current in the list
state.items.removeAll { it.key == key }
state.scope?.invalidate()
}
}
)
val scale = animatedScale(
animation = tween(
easing = FastOutSlowInEasing,
delayMillis = animationDelay,
durationMillis = duration
),
visible = isVisible
)
Box(
Modifier
.graphicsLayer(
scaleX = scale.value,
scaleY = scale.value,
alpha = opacity.value
)
.semantics {
liveRegion = LiveRegionMode.Polite
dismiss { key.dismiss(); true }
}
) {
children()
}
}
}
}
Box(modifier) {
state.scope = currentRecomposeScope
state.items.fastForEach { (item, opacity) ->
key(item) {
opacity {
content(item!!)
}
}
}
}
}
private class FadeInFadeOutState<T> {
// we use Any here as something which will not be equals to the real initial value
var current: Any? = Any()
var items = mutableListOf<FadeInFadeOutAnimationItem<T>>()
var scope: RecomposeScope? = null
}
private data class FadeInFadeOutAnimationItem<T>(
val key: T,
val transition: FadeInFadeOutTransition
)
private typealias FadeInFadeOutTransition = @Composable (content: @Composable () -> Unit) -> Unit
@Composable
private fun animatedOpacity(
animation: AnimationSpec<Float>,
visible: Boolean,
onAnimationFinish: () -> Unit = {}
): State<Float> {
val alpha = remember { Animatable(if (!visible) 1f else 0f) }
LaunchedEffect(visible) {
alpha.animateTo(
if (visible) 1f else 0f,
animationSpec = animation
)
onAnimationFinish()
}
return alpha.asState()
}
@Composable
private fun animatedScale(animation: AnimationSpec<Float>, visible: Boolean): State<Float> {
val scale = remember { Animatable(if (!visible) 1f else 0.8f) }
LaunchedEffect(visible) {
scale.animateTo(
if (visible) 1f else 0.8f,
animationSpec = animation
)
}
return scale.asState()
}
private const val SnackbarFadeInMillis = 150
private const val SnackbarFadeOutMillis = 75
private const val SnackbarInBetweenDelayMillis = 0
internal fun GenericSnackbarDuration.toMillis(
hasAction: Boolean,
accessibilityManager: AccessibilityManager?
): Long {
val original = when (this) {
GenericSnackbarDuration.Indefinite -> Long.MAX_VALUE
GenericSnackbarDuration.Long -> 10000L
GenericSnackbarDuration.Short -> 4000L
}
if (accessibilityManager == null) {
return original
}
return accessibilityManager.calculateRecommendedTimeoutMillis(
original,
containsIcons = true,
containsText = true,
containsControls = hasAction
)
}
interface GenericSnackbarData<Param> {
val message: Param
val actionLabel: String?
val duration: GenericSnackbarDuration
fun performAction()
fun dismiss()
}
enum class GenericSnackbarDuration {
Short,
Long,
Indefinite
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment