Skip to content

Commit b595811

Browse files
committed
feat(dialogs): add customizable enter/exit animations
Allow users to customize dialog animations via EnterTransition/ExitTransition parameters. Extracted default animations into DialogDefaults and AlertDialogDefaults objects to avoid duplication and provide a clean API. Added two new gallery examples demonstrating custom animations: - SmallDialog with slide-in spring animation - AlertDialog with bouncy scale animation
1 parent 6261be4 commit b595811

4 files changed

Lines changed: 145 additions & 53 deletions

File tree

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/Alert.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.github.kdroidfilter.nucleus.ui.apple.macos.components
22

33
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.EnterTransition
5+
import androidx.compose.animation.ExitTransition
46
import androidx.compose.animation.core.tween
57
import androidx.compose.animation.fadeIn
68
import androidx.compose.animation.fadeOut
@@ -183,6 +185,24 @@ fun AlertBanner(
183185
}
184186
}
185187

188+
// ===========================================================================
189+
// AlertDialogDefaults — default enter/exit transitions for alert dialogs
190+
// ===========================================================================
191+
192+
object AlertDialogDefaults {
193+
val Enter: EnterTransition =
194+
scaleIn(
195+
initialScale = 0.9f,
196+
animationSpec = tween(durationMillis = 200),
197+
) + fadeIn(animationSpec = tween(durationMillis = 200))
198+
199+
val Exit: ExitTransition =
200+
scaleOut(
201+
targetScale = 0.9f,
202+
animationSpec = tween(durationMillis = 150),
203+
) + fadeOut(animationSpec = tween(durationMillis = 150))
204+
}
205+
186206
// ===========================================================================
187207
// AlertDialog (Modal)
188208
// ===========================================================================
@@ -225,6 +245,8 @@ fun AlertDialog(
225245
onDestructive: (() -> Unit)? = null,
226246
onCancel: (() -> Unit)? = null,
227247
buttonLayout: AlertDialogButtonLayout = AlertDialogButtonLayout.Stacked,
248+
enter: EnterTransition = AlertDialogDefaults.Enter,
249+
exit: ExitTransition = AlertDialogDefaults.Exit,
228250
) {
229251
// Keep the popup mounted while the exit animation plays
230252
var showPopup by remember { mutableStateOf(false) }
@@ -276,14 +298,8 @@ fun AlertDialog(
276298
) {
277299
AnimatedVisibility(
278300
visible = animateIn,
279-
enter = scaleIn(
280-
initialScale = 0.9f,
281-
animationSpec = tween(durationMillis = 200),
282-
) + fadeIn(animationSpec = tween(durationMillis = 200)),
283-
exit = scaleOut(
284-
targetScale = 0.9f,
285-
animationSpec = tween(durationMillis = 150),
286-
) + fadeOut(animationSpec = tween(durationMillis = 150)),
301+
enter = enter,
302+
exit = exit,
287303
) {
288304
AlertDialogContent(
289305
title = title,

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/ContentDialog.kt

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.github.kdroidfilter.nucleus.ui.apple.macos.components
22

33
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.EnterTransition
5+
import androidx.compose.animation.ExitTransition
46
import androidx.compose.animation.core.tween
57
import androidx.compose.animation.fadeIn
68
import androidx.compose.animation.fadeOut
@@ -58,6 +60,36 @@ enum class DialogSize(val maxWidth: Dp) {
5860
Full(720.dp),
5961
}
6062

63+
// ===========================================================================
64+
// DialogDefaults — default enter/exit transitions for content dialogs
65+
// ===========================================================================
66+
67+
object DialogDefaults {
68+
val Enter: EnterTransition =
69+
fadeIn(tween(MacosDuration.Slow.millis)) +
70+
scaleIn(
71+
initialScale = 0.95f,
72+
transformOrigin = TransformOrigin.Center,
73+
animationSpec = tween(MacosDuration.Slow.millis),
74+
) +
75+
slideInVertically(
76+
initialOffsetY = { 10 },
77+
animationSpec = tween(MacosDuration.Slow.millis),
78+
)
79+
80+
val Exit: ExitTransition =
81+
fadeOut(tween(MacosDuration.Slow.millis)) +
82+
scaleOut(
83+
targetScale = 0.95f,
84+
transformOrigin = TransformOrigin.Center,
85+
animationSpec = tween(MacosDuration.Slow.millis),
86+
) +
87+
slideOutVertically(
88+
targetOffsetY = { 10 },
89+
animationSpec = tween(MacosDuration.Slow.millis),
90+
)
91+
}
92+
6193
// ===========================================================================
6294
// SmallDialog — macOS-native sheet-style dialog with horizontal button footer
6395
// ===========================================================================
@@ -96,6 +128,8 @@ fun SmallDialog(
96128
destructiveText: String? = null,
97129
onDestructive: (() -> Unit)? = null,
98130
size: DialogSize = DialogSize.Small,
131+
enter: EnterTransition = DialogDefaults.Enter,
132+
exit: ExitTransition = DialogDefaults.Exit,
99133
content: @Composable ColumnScope.() -> Unit,
100134
) {
101135
var showPopup by remember { mutableStateOf(false) }
@@ -149,26 +183,8 @@ fun SmallDialog(
149183
) {
150184
AnimatedVisibility(
151185
visible = animateIn,
152-
enter = fadeIn(tween(MacosDuration.Slow.millis)) +
153-
scaleIn(
154-
initialScale = 0.95f,
155-
transformOrigin = TransformOrigin.Center,
156-
animationSpec = tween(MacosDuration.Slow.millis),
157-
) +
158-
slideInVertically(
159-
initialOffsetY = { 10 },
160-
animationSpec = tween(MacosDuration.Slow.millis),
161-
),
162-
exit = fadeOut(tween(MacosDuration.Slow.millis)) +
163-
scaleOut(
164-
targetScale = 0.95f,
165-
transformOrigin = TransformOrigin.Center,
166-
animationSpec = tween(MacosDuration.Slow.millis),
167-
) +
168-
slideOutVertically(
169-
targetOffsetY = { 10 },
170-
animationSpec = tween(MacosDuration.Slow.millis),
171-
),
186+
enter = enter,
187+
exit = exit,
172188
) {
173189
val shape = RoundedCornerShape(16.dp)
174190
val fallbackBg = if (isDark) Color(0xFF262626).copy(alpha = 0.92f)

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/SaveDialog.kt

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package io.github.kdroidfilter.nucleus.ui.apple.macos.components
22

33
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.animation.EnterTransition
5+
import androidx.compose.animation.ExitTransition
46
import androidx.compose.animation.core.tween
57
import androidx.compose.animation.fadeIn
68
import androidx.compose.animation.fadeOut
7-
import androidx.compose.animation.scaleIn
8-
import androidx.compose.animation.scaleOut
9-
import androidx.compose.animation.slideInVertically
10-
import androidx.compose.animation.slideOutVertically
119
import androidx.compose.foundation.background
1210
import androidx.compose.foundation.border
1311
import androidx.compose.foundation.clickable
@@ -34,7 +32,6 @@ import androidx.compose.ui.Alignment
3432
import androidx.compose.ui.Modifier
3533
import androidx.compose.ui.draw.shadow
3634
import androidx.compose.ui.graphics.Color
37-
import androidx.compose.ui.graphics.TransformOrigin
3835
import androidx.compose.ui.text.style.TextAlign
3936
import androidx.compose.ui.unit.dp
4037
import androidx.compose.ui.window.Popup
@@ -91,6 +88,8 @@ fun SaveDialog(
9188
onLocationChange: ((Int, String) -> Unit)? = null,
9289
onDelete: (() -> Unit)? = null,
9390
size: DialogSize = DialogSize.Large,
91+
enter: EnterTransition = DialogDefaults.Enter,
92+
exit: ExitTransition = DialogDefaults.Exit,
9493
) {
9594
var showPopup by remember { mutableStateOf(false) }
9695
var animateIn by remember { mutableStateOf(false) }
@@ -143,26 +142,8 @@ fun SaveDialog(
143142
) {
144143
AnimatedVisibility(
145144
visible = animateIn,
146-
enter = fadeIn(tween(MacosDuration.Slow.millis)) +
147-
scaleIn(
148-
initialScale = 0.95f,
149-
transformOrigin = TransformOrigin.Center,
150-
animationSpec = tween(MacosDuration.Slow.millis),
151-
) +
152-
slideInVertically(
153-
initialOffsetY = { 10 },
154-
animationSpec = tween(MacosDuration.Slow.millis),
155-
),
156-
exit = fadeOut(tween(MacosDuration.Slow.millis)) +
157-
scaleOut(
158-
targetScale = 0.95f,
159-
transformOrigin = TransformOrigin.Center,
160-
animationSpec = tween(MacosDuration.Slow.millis),
161-
) +
162-
slideOutVertically(
163-
targetOffsetY = { 10 },
164-
animationSpec = tween(MacosDuration.Slow.millis),
165-
),
145+
enter = enter,
146+
exit = exit,
166147
) {
167148
SaveDialogContent(
168149
title = title,

sample/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/sample/pages/DialogPage.kt

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package io.github.kdroidfilter.nucleus.ui.apple.macos.sample.pages
22

3+
import androidx.compose.animation.core.spring
4+
import androidx.compose.animation.core.tween
5+
import androidx.compose.animation.fadeIn
6+
import androidx.compose.animation.fadeOut
7+
import androidx.compose.animation.scaleIn
8+
import androidx.compose.animation.scaleOut
9+
import androidx.compose.animation.slideInVertically
10+
import androidx.compose.animation.slideOutVertically
311
import androidx.compose.foundation.layout.Arrangement
412
import androidx.compose.foundation.layout.Box
513
import androidx.compose.foundation.layout.Column
@@ -220,6 +228,65 @@ fun AlertDialogSideBySideExample() {
220228
}
221229
}
222230

231+
@GalleryExample("Dialog", "Small Dialog — Custom Animation")
232+
@Composable
233+
fun SmallDialogCustomAnimationExample() {
234+
var showDialog by remember { mutableStateOf(false) }
235+
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
236+
PushButton(text = "Open Slide-In Dialog", onClick = { showDialog = true })
237+
}
238+
SmallDialog(
239+
visible = showDialog,
240+
onDismissRequest = { showDialog = false },
241+
title = "Custom Animation",
242+
confirmText = "Done",
243+
onConfirm = { showDialog = false },
244+
cancelText = "Cancel",
245+
onCancel = { showDialog = false },
246+
enter = slideInVertically(
247+
initialOffsetY = { it },
248+
animationSpec = spring(dampingRatio = 0.7f, stiffness = 300f),
249+
) + fadeIn(tween(300)),
250+
exit = slideOutVertically(
251+
targetOffsetY = { it },
252+
animationSpec = tween(250),
253+
) + fadeOut(tween(250)),
254+
) {
255+
Text(
256+
text = "This dialog slides in from the bottom with a spring animation.",
257+
style = MacosTheme.typography.caption1,
258+
color = MacosTheme.colorScheme.textSecondary,
259+
)
260+
}
261+
}
262+
263+
@GalleryExample("Dialog", "Alert Dialog — Custom Animation")
264+
@Composable
265+
fun AlertDialogCustomAnimationExample() {
266+
var showAlertDialog by remember { mutableStateOf(false) }
267+
PushButton(text = "Show Bounce Alert", onClick = { showAlertDialog = true })
268+
if (showAlertDialog) {
269+
AlertDialog(
270+
open = true,
271+
onDismissRequest = { showAlertDialog = false },
272+
title = "Custom Animation",
273+
message = "This alert uses a bouncy spring scale animation.",
274+
confirmText = "OK",
275+
cancelText = "Cancel",
276+
onConfirm = { showAlertDialog = false },
277+
onCancel = { showAlertDialog = false },
278+
enter = scaleIn(
279+
initialScale = 0.5f,
280+
animationSpec = spring(dampingRatio = 0.5f, stiffness = 400f),
281+
) + fadeIn(tween(200)),
282+
exit = scaleOut(
283+
targetScale = 0.8f,
284+
animationSpec = tween(150),
285+
) + fadeOut(tween(150)),
286+
)
287+
}
288+
}
289+
223290
@Composable
224291
internal fun DialogPage() {
225292
GalleryPage("Dialog", "A modal dialog that interrupts the user with important content.") {
@@ -247,6 +314,18 @@ internal fun DialogPage() {
247314
sourceCode = GallerySources.SaveDialogWithDeleteExample,
248315
) { SaveDialogWithDeleteExample() }
249316

317+
SectionHeader("Custom Animations")
318+
ExampleCard(
319+
title = "Slide-In Dialog",
320+
description = "Small dialog with custom slide-in spring animation",
321+
sourceCode = GallerySources.SmallDialogCustomAnimationExample,
322+
) { SmallDialogCustomAnimationExample() }
323+
ExampleCard(
324+
title = "Bounce Alert",
325+
description = "Alert dialog with bouncy spring scale animation",
326+
sourceCode = GallerySources.AlertDialogCustomAnimationExample,
327+
) { AlertDialogCustomAnimationExample() }
328+
250329
SectionHeader("Alert Dialog")
251330
ExampleCard(
252331
title = "Save Alert",

0 commit comments

Comments
 (0)