Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Breaking:** ToastKit's public exceptions now extend the package-owned `ToastKitException` base, which extends `RuntimeException`. Replace ToastKit-specific `InvalidArgumentException` catches with `ToastKitException`.
- Composer requirement raised to PHP `^8.4` and NativePHP Mobile `^4.1`.
- Pest upgraded to `^4.0`.

### Fixed

- Android toasts now draw above Material3 bottom sheets, dialogs and any other window the app opens, including ones opened while a toast is showing. Each position is hosted in its own non-focusable window instead of the activity's decor view, so a text field keeps focus and the keyboard. Bottom toasts sit above the keyboard. On Android 14+ only the toast cards take touches; on Android 10 to 13 the padding around them does too.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ internal object ToastKitManager {
private val activeIds = mutableSetOf<String>()
private val uniqueToId = mutableMapOf<String, String>()

/** Called on the main thread whenever [rendered] changes, so the overlay windows can follow it. */
var onRenderedChanged: (() -> Unit)? = null

fun show(configuration: ToastKitConfiguration): ToastKitShowResult {
val result = synchronized(identityLock) {
configuration.uniqueKey?.let { key ->
Expand Down Expand Up @@ -98,6 +101,7 @@ internal object ToastKitManager {
states[id] = state.copy(visible = true)
visible.add(state.configuration)
rendered.add(state.configuration)
onRenderedChanged?.invoke()
schedule(state.configuration)
ToastKitEventDispatcher.shown(id)
}
Expand Down Expand Up @@ -126,6 +130,7 @@ internal object ToastKitManager {
main.postDelayed({
rendered.removeAll { it.id == id }
exiting.remove(id)
onRenderedChanged?.invoke()
releaseIdentity(state.configuration)
if (promote) promote()
}, exitDuration)
Expand Down Expand Up @@ -165,7 +170,10 @@ internal object ToastKitManager {
val index = visible.indexOfFirst { it.id == configuration.id }
if (index >= 0) visible[index] = configuration
val renderedIndex = rendered.indexOfFirst { it.id == configuration.id }
if (renderedIndex >= 0) rendered[renderedIndex] = configuration
if (renderedIndex >= 0) {
rendered[renderedIndex] = configuration
onRenderedChanged?.invoke()
}
}

private inline fun onMain(crossinline operation: () -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.input.pointer.pointerInput
Expand All @@ -53,12 +56,17 @@ import kotlin.math.abs
import kotlinx.coroutines.delay

@Composable
internal fun ToastKitHost(position: ToastKitPosition) {
internal fun ToastKitHost(
position: ToastKitPosition,
keyboardHeight: Int = 0,
onCardBounds: (String, Rect?) -> Unit = { _, _ -> },
) {
val items = ToastKitManager.rendered.filter { effectivePosition(it) == position }
val vertical = when (position) { ToastKitPosition.TOP -> Arrangement.Top; ToastKitPosition.CENTER -> Arrangement.Center; ToastKitPosition.BOTTOM -> Arrangement.Bottom }
val safeArea = when (position) {
ToastKitPosition.TOP -> Modifier.statusBarsPadding()
ToastKitPosition.BOTTOM -> Modifier.navigationBarsPadding()
// The overlay window sits above the keyboard, so keep bottom toasts clear of it.
ToastKitPosition.BOTTOM -> Modifier.windowInsetsPadding(WindowInsets.navigationBars.union(WindowInsets(bottom = keyboardHeight)))
ToastKitPosition.CENTER -> Modifier
}
Column(
Expand All @@ -67,13 +75,16 @@ internal fun ToastKitHost(position: ToastKitPosition) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
items.forEach { toast ->
key(toast.id) { ToastKitCard(toast) }
key(toast.id) {
DisposableEffect(toast.id) { onDispose { onCardBounds(toast.id, null) } }
ToastKitCard(toast, Modifier.onGloballyPositioned { onCardBounds(toast.id, it.boundsInWindow()) })
}
}
}
}

@Composable
private fun ToastKitCard(toast: ToastKitConfiguration) {
private fun ToastKitCard(toast: ToastKitConfiguration, modifier: Modifier = Modifier) {
val position = effectivePosition(toast)
var x by remember(toast.id) { mutableFloatStateOf(0f) }
var y by remember(toast.id) { mutableFloatStateOf(0f) }
Expand Down Expand Up @@ -134,7 +145,7 @@ private fun ToastKitCard(toast: ToastKitConfiguration) {
val visibility = remember(toast.id) { MutableTransitionState(false) }
val shouldBeVisible = toast.id !in ToastKitManager.exiting
LaunchedEffect(shouldBeVisible) { visibility.targetState = shouldBeVisible }
AnimatedVisibility(visibleState = visibility, enter = enter, exit = exit) {
AnimatedVisibility(visibleState = visibility, modifier = modifier, enter = enter, exit = exit) {
val interaction = Modifier.padding(vertical = 4.dp).widthIn(max = 560.dp)
.then(if (toast.presentation == ToastKitPresentation.STANDARD) Modifier.fillMaxWidth() else Modifier)
.semantics { liveRegion = LiveRegionMode.Polite }
Expand Down Expand Up @@ -430,7 +441,7 @@ private fun resolvedDirection(toast: ToastKitConfiguration): ToastKitDirection =
else -> toast.direction
}

private fun effectivePosition(toast: ToastKitConfiguration): ToastKitPosition =
internal fun effectivePosition(toast: ToastKitConfiguration): ToastKitPosition =
if (toast.presentation == ToastKitPresentation.DYNAMIC) ToastKitPosition.TOP else toast.position

private fun directionalEnter(toast: ToastKitConfiguration, spec: androidx.compose.animation.core.FiniteAnimationSpec<androidx.compose.ui.unit.IntOffset>): EnterTransition =
Expand Down
Loading