Skip to content

Commit

Permalink
fix(Android): missing transition events with formSheet presentation (#…
Browse files Browse the repository at this point in the history
…2682)

## Description

This PR aligns* events received in element tree between Android and iOS
for screens with `presentation: 'formSheet'`.

\* **almost aligns** - on iOS when natively dismissed `onDismissed` and
`onDisappear/transitionEnd` will be received, whereas on Android it will
be only `onDismissed` for now.
Adding support for `onDisappear/transitionEnd` requires wider changes in
logic which I don't want in incoming 4.7.0 release.

## Changes

`formSheet` animations based on custom animators now have listener set,
which emmits appropriate events **bypassing** "regular" view emitting
infrastructure.
I've found that `ScreenFragment.onViewAnimation{Start,End}` methods do
not work for some reason (`isResumed` is false for form sheet) with the
sheets, therefore I bypassed it for now.


## Test code and steps to reproduce

`TestAndroidTransition` + add logs in `NativeStackView` in
`react-navigation` to see when events are received (or attach
appropriate listeners).

## Checklist

- [ ] Ensured that CI passes
  • Loading branch information
kkafar authored Feb 11, 2025
1 parent 9f01d1b commit 2ca38af
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ import com.swmansion.rnscreens.bottomsheet.useSingleDetent
import com.swmansion.rnscreens.bottomsheet.useThreeDetents
import com.swmansion.rnscreens.bottomsheet.useTwoDetents
import com.swmansion.rnscreens.bottomsheet.usesFormSheetPresentation
import com.swmansion.rnscreens.events.ScreenAnimationDelegate
import com.swmansion.rnscreens.events.ScreenDismissedEvent
import com.swmansion.rnscreens.events.ScreenEventDelegate
import com.swmansion.rnscreens.events.ScreenEventEmitter
import com.swmansion.rnscreens.ext.recycle
import com.swmansion.rnscreens.transition.ExternalBoundaryValuesEvaluator
import com.swmansion.rnscreens.utils.DeviceUtils
Expand Down Expand Up @@ -366,7 +367,14 @@ class ScreenStackFragment :
}
animatorSet.play(alphaAnimator).with(slideAnimator)
}
animatorSet.addListener(ScreenEventDelegate(this))
animatorSet.addListener(
ScreenAnimationDelegate(
this,
ScreenEventEmitter(this.screen),
if (enter) ScreenAnimationDelegate.AnimationType.ENTER
else ScreenAnimationDelegate.AnimationType.EXIT
)
)
return animatorSet
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.swmansion.rnscreens.events

import android.animation.Animator
import android.util.Log
import com.swmansion.rnscreens.ScreenStackFragmentWrapper

// The goal is to make this universal delegate for handling animation progress related logic.
// At this moment this class works only with form sheet presentation.
class ScreenAnimationDelegate(
private val wrapper: ScreenStackFragmentWrapper,
private val eventEmitter: ScreenEventEmitter?,
private val animationType: AnimationType,
) : Animator.AnimatorListener {
enum class AnimationType {
ENTER,
EXIT
}

private var currentState: LifecycleState = LifecycleState.INITIALIZED

private fun progressState() {
currentState =
when (currentState) {
LifecycleState.INITIALIZED -> LifecycleState.START_DISPATCHED
LifecycleState.START_DISPATCHED -> LifecycleState.END_DISPATCHED
LifecycleState.END_DISPATCHED -> LifecycleState.END_DISPATCHED
}
}

override fun onAnimationStart(animation: Animator) {
if (currentState === LifecycleState.INITIALIZED) {
progressState()

// These callbacks do not work as expected from this call site, TODO: investigate it.
// To fix it quickly we emit required events manually
// wrapper.onViewAnimationStart()

when (animationType) {
AnimationType.ENTER -> eventEmitter?.dispatchOnWillAppear()
AnimationType.EXIT -> eventEmitter?.dispatchOnWillDisappear()
}

val isExitAnimation = animationType === AnimationType.EXIT
eventEmitter?.dispatchTransitionProgress(
0.0f,
isExitAnimation,
isExitAnimation,
)
}
}

override fun onAnimationEnd(animation: Animator) {
if (currentState === LifecycleState.START_DISPATCHED) {
progressState()
animation.removeListener(this)

// wrapper.onViewAnimationEnd()

when (animationType) {
AnimationType.ENTER -> eventEmitter?.dispatchOnAppear()
AnimationType.EXIT -> eventEmitter?.dispatchOnDisappear()
}

val isExitAnimation = animationType === AnimationType.EXIT
eventEmitter?.dispatchTransitionProgress(
1.0f,
isExitAnimation,
isExitAnimation,
)

wrapper.screen.endRemovalTransition()
}
}

override fun onAnimationCancel(animation: Animator) = Unit

override fun onAnimationRepeat(animation: Animator) = Unit

private enum class LifecycleState {
INITIALIZED,
START_DISPATCHED,
END_DISPATCHED,
}

companion object {
const val TAG = "ScreenEventDelegate"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.swmansion.rnscreens.events

import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.rnscreens.Screen
import com.swmansion.rnscreens.ScreenFragment

// TODO: Consider taking weak ref here or accepting screen as argument in every method
// to avoid reference cycle.
class ScreenEventEmitter(val screen: Screen) {
val reactEventDispatcher
get() = screen.reactEventDispatcher

val reactSurfaceId
get() = UIManagerHelper.getSurfaceId(screen)

fun dispatchOnWillAppear() =
reactEventDispatcher?.dispatchEvent(ScreenWillAppearEvent(reactSurfaceId, screen.id))

fun dispatchOnAppear() =
reactEventDispatcher?.dispatchEvent(ScreenAppearEvent(reactSurfaceId, screen.id))

fun dispatchOnWillDisappear() =
reactEventDispatcher?.dispatchEvent(ScreenWillDisappearEvent(reactSurfaceId, screen.id))

fun dispatchOnDisappear() =
reactEventDispatcher?.dispatchEvent(ScreenDisappearEvent(reactSurfaceId, screen.id))

fun dispatchOnDismissed() =
reactEventDispatcher?.dispatchEvent(ScreenDismissedEvent(reactSurfaceId, screen.id))

fun dispatchTransitionProgress(progress: Float, isExitAnimation: Boolean, isGoingForward: Boolean) {
val sanitizedProgress = progress.coerceIn(0.0f, 1.0f)
val coalescingKey = ScreenFragment.getCoalescingKey(sanitizedProgress)
reactEventDispatcher?.dispatchEvent(ScreenTransitionProgressEvent(reactSurfaceId, screen.id, sanitizedProgress, isExitAnimation, isGoingForward, coalescingKey))
}
}

0 comments on commit 2ca38af

Please sign in to comment.