Skip to content

Commit 2784cb8

Browse files
feat(android): implement onTransitionEnd event callback
1 parent 74e9e0e commit 2784cb8

2 files changed

Lines changed: 90 additions & 7 deletions

File tree

android/src/main/java/com/ease/EaseView.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class EaseView(context: Context) : ReactViewGroup(context) {
3737

3838
// --- Hardware layer ---
3939
var useHardwareLayer: Boolean = true
40+
41+
// --- Event callback ---
42+
var onTransitionEnd: ((property: String, finished: Boolean) -> Unit)? = null
4043
private var activeAnimationCount: Int = 0
4144
private var savedLayerType: Int = View.LAYER_TYPE_NONE
4245

@@ -66,6 +69,16 @@ class EaseView(context: Context) : ReactViewGroup(context) {
6669
private val INTERPOLATOR_LINEAR by lazy { LinearInterpolator() }
6770
}
6871

72+
private fun toJsPropertyName(propertyName: String): String? = when (propertyName) {
73+
"alpha" -> "opacity"
74+
"translationX" -> "translateX"
75+
"translationY" -> "translateY"
76+
"scaleX" -> "scale"
77+
"scaleY" -> null // Skip — scaleX already fires for scale
78+
"rotation" -> "rotate"
79+
else -> null
80+
}
81+
6982
private fun getInterpolator(easing: String): TimeInterpolator = when (easing) {
7083
"easeIn" -> INTERPOLATOR_EASE_IN
7184
"easeOut" -> INTERPOLATOR_EASE_OUT
@@ -238,11 +251,18 @@ class EaseView(context: Context) : ReactViewGroup(context) {
238251
}
239252
}
240253
addListener(object : AnimatorListenerAdapter() {
254+
private var cancelled = false
241255
override fun onAnimationStart(animation: Animator) {
242256
this@EaseView.onEaseAnimationStart()
243257
}
258+
override fun onAnimationCancel(animation: Animator) {
259+
cancelled = true
260+
}
244261
override fun onAnimationEnd(animation: Animator) {
245262
this@EaseView.onEaseAnimationEnd()
263+
toJsPropertyName(propertyName)?.let { jsProp ->
264+
onTransitionEnd?.invoke(jsProp, !cancelled)
265+
}
246266
}
247267
})
248268
}
@@ -274,8 +294,22 @@ class EaseView(context: Context) : ReactViewGroup(context) {
274294
this@EaseView.onEaseAnimationStart()
275295
}
276296
}
277-
addEndListener { _, _, _, _ ->
297+
addEndListener { _, canceled, _, _ ->
278298
this@EaseView.onEaseAnimationEnd()
299+
val viewPropertyName = when (viewProperty) {
300+
DynamicAnimation.ALPHA -> "alpha"
301+
DynamicAnimation.TRANSLATION_X -> "translationX"
302+
DynamicAnimation.TRANSLATION_Y -> "translationY"
303+
DynamicAnimation.SCALE_X -> "scaleX"
304+
DynamicAnimation.SCALE_Y -> "scaleY"
305+
DynamicAnimation.ROTATION -> "rotation"
306+
else -> null
307+
}
308+
viewPropertyName?.let { name ->
309+
toJsPropertyName(name)?.let { jsProp ->
310+
onTransitionEnd?.invoke(jsProp, !canceled)
311+
}
312+
}
279313
}
280314
}
281315

android/src/main/java/com/ease/EaseViewManager.kt

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package com.ease
22

3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.ReadableArray
5+
import com.facebook.react.bridge.WritableMap
6+
import com.facebook.react.common.MapBuilder
37
import com.facebook.react.module.annotations.ReactModule
8+
import com.facebook.react.uimanager.PixelUtil
49
import com.facebook.react.uimanager.ThemedReactContext
10+
import com.facebook.react.uimanager.UIManagerHelper
511
import com.facebook.react.uimanager.annotations.ReactProp
12+
import com.facebook.react.uimanager.events.Event
613
import com.facebook.react.views.view.ReactViewGroup
714
import com.facebook.react.views.view.ReactViewManager
815

@@ -11,8 +18,17 @@ class EaseViewManager : ReactViewManager() {
1118

1219
override fun getName(): String = NAME
1320

14-
override fun createViewInstance(context: ThemedReactContext): EaseView =
15-
EaseView(context)
21+
override fun createViewInstance(context: ThemedReactContext): EaseView {
22+
val view = EaseView(context)
23+
view.onTransitionEnd = { property, finished ->
24+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
25+
val surfaceId = UIManagerHelper.getSurfaceId(context)
26+
eventDispatcher?.dispatchEvent(
27+
TransitionEndEvent(surfaceId, view.id, property, finished)
28+
)
29+
}
30+
return view
31+
}
1632

1733
// --- Animate value setters ---
1834

@@ -23,12 +39,12 @@ class EaseViewManager : ReactViewManager() {
2339

2440
@ReactProp(name = "animateTranslateX", defaultFloat = 0f)
2541
fun setAnimateTranslateX(view: EaseView, value: Float) {
26-
view.pendingTranslateX = value
42+
view.pendingTranslateX = PixelUtil.toPixelFromDIP(value)
2743
}
2844

2945
@ReactProp(name = "animateTranslateY", defaultFloat = 0f)
3046
fun setAnimateTranslateY(view: EaseView, value: Float) {
31-
view.pendingTranslateY = value
47+
view.pendingTranslateY = PixelUtil.toPixelFromDIP(value)
3248
}
3349

3450
@ReactProp(name = "animateScale", defaultFloat = 1f)
@@ -50,12 +66,12 @@ class EaseViewManager : ReactViewManager() {
5066

5167
@ReactProp(name = "initialAnimateTranslateX", defaultFloat = 0f)
5268
fun setInitialAnimateTranslateX(view: EaseView, value: Float) {
53-
view.initialAnimateTranslateX = value
69+
view.initialAnimateTranslateX = PixelUtil.toPixelFromDIP(value)
5470
}
5571

5672
@ReactProp(name = "initialAnimateTranslateY", defaultFloat = 0f)
5773
fun setInitialAnimateTranslateY(view: EaseView, value: Float) {
58-
view.initialAnimateTranslateY = value
74+
view.initialAnimateTranslateY = PixelUtil.toPixelFromDIP(value)
5975
}
6076

6177
@ReactProp(name = "initialAnimateScale", defaultFloat = 1f)
@@ -112,6 +128,20 @@ class EaseViewManager : ReactViewManager() {
112128
view.useHardwareLayer = value
113129
}
114130

131+
// --- Prevent BaseViewManager from resetting animated properties ---
132+
133+
override fun setTransformProperty(
134+
view: ReactViewGroup,
135+
transforms: ReadableArray?,
136+
transformOrigin: ReadableArray?
137+
) {
138+
// No-op: we manage translationX/Y, scaleX/Y, rotation ourselves via animations.
139+
}
140+
141+
override fun setOpacity(view: ReactViewGroup, opacity: Float) {
142+
// No-op: we manage opacity ourselves via animations.
143+
}
144+
115145
// --- Lifecycle ---
116146

117147
override fun onAfterUpdateTransaction(view: ReactViewGroup) {
@@ -124,6 +154,25 @@ class EaseViewManager : ReactViewManager() {
124154
(view as? EaseView)?.cleanup()
125155
}
126156

157+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
158+
return MapBuilder.builder<String, Any>()
159+
.put("onTransitionEnd", MapBuilder.of("registrationName", "onTransitionEnd"))
160+
.build()
161+
}
162+
163+
private class TransitionEndEvent(
164+
surfaceId: Int,
165+
viewId: Int,
166+
private val property: String,
167+
private val finished: Boolean
168+
) : Event<TransitionEndEvent>(surfaceId, viewId) {
169+
override fun getEventName() = "onTransitionEnd"
170+
override fun getEventData(): WritableMap = Arguments.createMap().apply {
171+
putString("property", property)
172+
putBoolean("finished", finished)
173+
}
174+
}
175+
127176
companion object {
128177
const val NAME = "EaseView"
129178
}

0 commit comments

Comments
 (0)