Skip to content

Commit 158b0e0

Browse files
Port layout-aware enter/exit animations to Dialog for Fragment parity
The Dialog path was doing a plain alpha fade (300ms) regardless of in-app layout, while the Fragment path uses layout-specific animations at 500ms: slide_down_custom / fade_in_custom / slide_up_custom for entry and top_exit / fade_out_custom / bottom_exit for exit. This commit closes that gap for Compose hosts without touching the shipping Fragment class. Changes: - InAppAnimationService now exposes getEnterAnimationResource(layout) and getExitAnimationResource(layout), plus a hideAndAnimateWebView method symmetric with showAndAnimateWebView. - showAndAnimateWebView now takes the layout and loads the correct animation with ITERABLE_IN_APP_ANIMATION_DURATION (500ms). - The hardcoded ANIMATION_DURATION_MS=300 was conflating two distinct timings: background transitions (300ms) and view animations (500ms). It's now replaced by the matching IterableConstants values. - IterableInAppDialogNotification.hideWebView plays the exit animation, hides the background, and dismisses after 400ms — mirroring the Fragment's hideWebView timing. When shouldAnimate=false it still dismisses synchronously so existing Dialog tests remain valid. - runResizeScript carries a TODO(future PR) pointing at the native window resize logic that still needs porting from the Fragment's resize(float). Until then, Dialog hosts rely on the HTML's window.resize() self-sizing hook, which covers fixed-height in-apps but not dynamically-resizing content. Made-with: Cursor
1 parent f5ea996 commit 158b0e0

2 files changed

Lines changed: 106 additions & 14 deletions

File tree

iterableapi/src/main/java/com/iterable/iterableapi/InAppAnimationService.kt

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import android.graphics.drawable.Drawable
77
import android.graphics.drawable.TransitionDrawable
88
import android.view.View
99
import android.view.Window
10+
import android.view.animation.AnimationUtils
11+
import androidx.annotation.AnimRes
1012
import androidx.core.graphics.ColorUtils
1113

1214
internal class InAppAnimationService {
@@ -36,7 +38,7 @@ internal class InAppAnimationService {
3638
val layers = arrayOf(from, to)
3739
val transition = TransitionDrawable(layers)
3840
window.setBackgroundDrawable(transition)
39-
transition.startTransition(ANIMATION_DURATION_MS)
41+
transition.startTransition(IterableConstants.ITERABLE_IN_APP_BACKGROUND_ANIMATION_DURATION)
4042
} else {
4143
window.setBackgroundDrawable(to)
4244
}
@@ -58,17 +60,78 @@ internal class InAppAnimationService {
5860
}
5961
}
6062

61-
fun showAndAnimateWebView(webView: View, shouldAnimate: Boolean, context: Context?) {
63+
/**
64+
* Returns the enter animation resource for the given in-app layout, mirroring the
65+
* behavior of [IterableInAppFragmentHTMLNotification] so Compose/Dialog hosts get the
66+
* same slide/fade animations as Fragment hosts.
67+
*/
68+
@AnimRes
69+
fun getEnterAnimationResource(layout: InAppLayoutService.InAppLayout): Int {
70+
return when (layout) {
71+
InAppLayoutService.InAppLayout.TOP -> R.anim.slide_down_custom
72+
InAppLayoutService.InAppLayout.BOTTOM -> R.anim.slide_up_custom
73+
InAppLayoutService.InAppLayout.CENTER,
74+
InAppLayoutService.InAppLayout.FULLSCREEN -> R.anim.fade_in_custom
75+
}
76+
}
77+
78+
/**
79+
* Returns the exit animation resource for the given in-app layout, mirroring the
80+
* behavior of [IterableInAppFragmentHTMLNotification].
81+
*/
82+
@AnimRes
83+
fun getExitAnimationResource(layout: InAppLayoutService.InAppLayout): Int {
84+
return when (layout) {
85+
InAppLayoutService.InAppLayout.TOP -> R.anim.top_exit
86+
InAppLayoutService.InAppLayout.BOTTOM -> R.anim.bottom_exit
87+
InAppLayoutService.InAppLayout.CENTER,
88+
InAppLayoutService.InAppLayout.FULLSCREEN -> R.anim.fade_out_custom
89+
}
90+
}
91+
92+
fun showAndAnimateWebView(
93+
webView: View,
94+
shouldAnimate: Boolean,
95+
context: Context?,
96+
layout: InAppLayoutService.InAppLayout
97+
) {
98+
webView.alpha = 1.0f
99+
webView.visibility = View.VISIBLE
100+
62101
if (shouldAnimate && context != null) {
63-
webView.alpha = 0f
64-
webView.visibility = View.VISIBLE
65-
webView.animate()
66-
.alpha(1.0f)
67-
.setDuration(ANIMATION_DURATION_MS.toLong())
68-
.start()
69-
} else {
70-
webView.alpha = 1.0f
71-
webView.visibility = View.VISIBLE
102+
try {
103+
val anim = AnimationUtils.loadAnimation(context, getEnterAnimationResource(layout))
104+
anim.duration = IterableConstants.ITERABLE_IN_APP_ANIMATION_DURATION.toLong()
105+
webView.startAnimation(anim)
106+
} catch (e: Exception) {
107+
IterableLogger.w(TAG, "Failed to start enter animation", e)
108+
}
109+
}
110+
}
111+
112+
/**
113+
* Plays the layout-appropriate exit animation on the given view. Returns `true` when
114+
* an animation was started, `false` otherwise (either because [shouldAnimate] was
115+
* false or loading the animation failed). Callers should schedule dismissal
116+
* accordingly.
117+
*/
118+
fun hideAndAnimateWebView(
119+
webView: View,
120+
shouldAnimate: Boolean,
121+
context: Context?,
122+
layout: InAppLayoutService.InAppLayout
123+
): Boolean {
124+
if (!shouldAnimate || context == null) {
125+
return false
126+
}
127+
return try {
128+
val anim = AnimationUtils.loadAnimation(context, getExitAnimationResource(layout))
129+
anim.duration = IterableConstants.ITERABLE_IN_APP_ANIMATION_DURATION.toLong()
130+
webView.startAnimation(anim)
131+
true
132+
} catch (e: Exception) {
133+
IterableLogger.w(TAG, "Failed to start exit animation", e)
134+
false
72135
}
73136
}
74137

@@ -91,7 +154,6 @@ internal class InAppAnimationService {
91154
}
92155

93156
companion object {
94-
private const val ANIMATION_DURATION_MS = 300
95157
private const val TAG = "InAppAnimService"
96158
}
97159
}

iterableapi/src/main/java/com/iterable/iterableapi/IterableInAppDialogNotification.kt

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class IterableInAppDialogNotification internal constructor(
4848
private const val TAG = "IterableInAppDialog"
4949
private const val BACK_BUTTON = "itbl://backButton"
5050
private const val DELAY_THRESHOLD_MS = 500L
51+
private const val DISMISS_DELAY_MS = 400L
5152

5253
@Volatile
5354
@JvmStatic
@@ -274,7 +275,8 @@ class IterableInAppDialogNotification internal constructor(
274275

275276
private fun showAndAnimateWebView() {
276277
webView?.let { wv ->
277-
animationService.showAndAnimateWebView(wv, shouldAnimate, context)
278+
val layout = layoutService.getInAppLayout(insetPadding)
279+
animationService.showAndAnimateWebView(wv, shouldAnimate, context, layout)
278280
}
279281
}
280282

@@ -283,6 +285,12 @@ class IterableInAppDialogNotification internal constructor(
283285
}
284286

285287
override fun runResizeScript() {
288+
// TODO(future PR): port IterableInAppFragmentHTMLNotification.resize(float) so the
289+
// dialog window resizes natively to fit WebView content height (incl. debounced
290+
// resize, gravity-aware RelativeLayout params, and full-screen fallback).
291+
// Until then, Dialog hosts only invoke the JS `window.resize()` hook and rely on
292+
// the HTML to self-size; content-sized in-apps that dynamically grow/shrink will
293+
// not have the Dialog window follow.
286294
webViewService.runResizeScript(webView)
287295
}
288296

@@ -305,7 +313,29 @@ class IterableInAppDialogNotification internal constructor(
305313
}
306314

307315
private fun hideWebView() {
308-
dismiss()
316+
val wv = webView
317+
val win = window
318+
val layout = layoutService.getInAppLayout(insetPadding)
319+
320+
if (shouldAnimate && wv != null) {
321+
animationService.hideAndAnimateWebView(wv, true, context, layout)
322+
323+
if (win != null) {
324+
animationService.hideInAppBackground(
325+
win,
326+
inAppBackgroundColor,
327+
inAppBackgroundAlpha,
328+
true
329+
)
330+
}
331+
332+
// Mirrors the 400ms post-animation dismiss delay used by
333+
// IterableInAppFragmentHTMLNotification.hideWebView() so the exit animation
334+
// has time to play before the dialog window is torn down.
335+
wv.postDelayed({ dismiss() }, DISMISS_DELAY_MS)
336+
} else {
337+
dismiss()
338+
}
309339
}
310340

311341
private fun processMessageRemoval() {

0 commit comments

Comments
 (0)