From b1078b27720ef5485a835ddddc18435b28019363 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Sat, 1 Aug 2020 06:26:45 -0400 Subject: [PATCH] [native-stack] Add transitionStart and transitionEnd events (#499) Implement transitionStart and transitionEnd events in native stack, similar to the events available in js stack. On Android there isn't really a concept of nested stacks and it uses only one fragment manager. This means a single "transition" so we need a way to trigger this transition on all nested stacks of a screen. To do that we added a way for nested stacks to register on their parent screen so when a screen is dismissed it can also call the dismiss method on all its child stacks. --- .../java/com/swmansion/rnscreens/Screen.java | 8 ++ .../swmansion/rnscreens/ScreenContainer.java | 15 +++- .../rnscreens/ScreenDisappearEvent.java | 30 +++++++ .../swmansion/rnscreens/ScreenFragment.java | 87 +++++++++++++++++-- .../rnscreens/ScreenStackFragment.java | 64 +++++++++++--- .../rnscreens/ScreenViewManager.java | 6 ++ .../rnscreens/ScreenWillAppearEvent.java | 30 +++++++ .../rnscreens/ScreenWillDisappearEvent.java | 30 +++++++ ios/RNSScreen.h | 3 + ios/RNSScreen.m | 52 +++++++++-- native-stack/README.md | 43 ++++++++- src/index.d.ts | 19 +++- src/native-stack/types.tsx | 8 ++ src/native-stack/views/NativeStackView.tsx | 26 ++++++ 14 files changed, 391 insertions(+), 30 deletions(-) create mode 100644 android/src/main/java/com/swmansion/rnscreens/ScreenDisappearEvent.java create mode 100644 android/src/main/java/com/swmansion/rnscreens/ScreenWillAppearEvent.java create mode 100644 android/src/main/java/com/swmansion/rnscreens/ScreenWillDisappearEvent.java diff --git a/android/src/main/java/com/swmansion/rnscreens/Screen.java b/android/src/main/java/com/swmansion/rnscreens/Screen.java index b0986c102f..37d14d82f6 100644 --- a/android/src/main/java/com/swmansion/rnscreens/Screen.java +++ b/android/src/main/java/com/swmansion/rnscreens/Screen.java @@ -54,6 +54,14 @@ public void onViewDetachedFromWindow(View view) { private StackAnimation mStackAnimation = StackAnimation.DEFAULT; private boolean mGestureEnabled = true; + @Override + protected void onAnimationStart() { + super.onAnimationStart(); + if (mFragment != null) { + mFragment.onViewAnimationStart(); + } + } + @Override protected void onAnimationEnd() { super.onAnimationEnd(); diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java index 7c54a734fa..b0bbb5b9e2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java @@ -29,6 +29,7 @@ public class ScreenContainer extends ViewGroup { private boolean mNeedUpdate; private boolean mIsAttached; private boolean mLayoutEnqueued = false; + private @Nullable ScreenFragment mParentScreenFragment = null; private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() { @@ -74,6 +75,10 @@ public void requestLayout() { } } + public boolean isNested() { + return mParentScreenFragment != null; + } + protected void markUpdated() { if (!mNeedUpdate) { mNeedUpdate = true; @@ -137,8 +142,10 @@ private void setupFragmentManager() { // If parent is of type Screen it means we are inside a nested fragment structure. // Otherwise we expect to connect directly with root view and get root fragment manager if (parent instanceof Screen) { - Fragment screenFragment = ((Screen) parent).getFragment(); + ScreenFragment screenFragment = ((Screen) parent).getFragment(); setFragmentManager(screenFragment.getChildFragmentManager()); + mParentScreenFragment = screenFragment; + mParentScreenFragment.registerChildScreenContainer(this); return; } @@ -250,6 +257,12 @@ protected void onDetachedFromWindow() { removeMyFragments(); mFragmentManager.executePendingTransactions(); } + + if (mParentScreenFragment != null) { + mParentScreenFragment.unregisterChildScreenContainer(this); + mParentScreenFragment = null; + } + super.onDetachedFromWindow(); mIsAttached = false; // When fragment container view is detached we force all its children to be removed. diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenDisappearEvent.java b/android/src/main/java/com/swmansion/rnscreens/ScreenDisappearEvent.java new file mode 100644 index 0000000000..5be3411651 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenDisappearEvent.java @@ -0,0 +1,30 @@ +package com.swmansion.rnscreens; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class ScreenDisappearEvent extends Event { + + public static final String EVENT_NAME = "topDisappear"; + + public ScreenDisappearEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java index 09a502782d..902e601ea2 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java @@ -8,12 +8,15 @@ import android.view.ViewParent; import android.widget.FrameLayout; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.UIManagerModule; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + public class ScreenFragment extends Fragment { protected static View recycleView(View view) { @@ -33,6 +36,7 @@ protected static View recycleView(View view) { } protected Screen mScreenView; + private List mChildScreenContainers = new ArrayList<>(); public ScreenFragment() { throw new IllegalStateException("Screen fragments should never be restored"); @@ -60,18 +64,90 @@ public Screen getScreen() { return mScreenView; } - private void dispatchOnAppear() { + protected void dispatchOnWillAppear() { + ((ReactContext) mScreenView.getContext()) + .getNativeModule(UIManagerModule.class) + .getEventDispatcher() + .dispatchEvent(new ScreenWillAppearEvent(mScreenView.getId())); + + for (ScreenContainer sc : mChildScreenContainers) { + if (sc.getScreenCount() > 0) { + Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1); + topScreen.getFragment().dispatchOnWillAppear(); + } + } + } + + protected void dispatchOnAppear() { ((ReactContext) mScreenView.getContext()) .getNativeModule(UIManagerModule.class) .getEventDispatcher() .dispatchEvent(new ScreenAppearEvent(mScreenView.getId())); + + for (ScreenContainer sc : mChildScreenContainers) { + if (sc.getScreenCount() > 0) { + Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1); + topScreen.getFragment().dispatchOnAppear(); + } + } + } + + protected void dispatchOnWillDisappear() { + ((ReactContext) mScreenView.getContext()) + .getNativeModule(UIManagerModule.class) + .getEventDispatcher() + .dispatchEvent(new ScreenWillDisappearEvent(mScreenView.getId())); + + for (ScreenContainer sc : mChildScreenContainers) { + if (sc.getScreenCount() > 0) { + Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1); + topScreen.getFragment().dispatchOnWillDisappear(); + } + } + } + + protected void dispatchOnDisappear() { + ((ReactContext) mScreenView.getContext()) + .getNativeModule(UIManagerModule.class) + .getEventDispatcher() + .dispatchEvent(new ScreenDisappearEvent(mScreenView.getId())); + + for (ScreenContainer sc : mChildScreenContainers) { + if (sc.getScreenCount() > 0) { + Screen topScreen = sc.getScreenAt(sc.getScreenCount() - 1); + topScreen.getFragment().dispatchOnDisappear(); + } + } + } + + public void registerChildScreenContainer(ScreenContainer screenContainer) { + mChildScreenContainers.add(screenContainer); + } + + public void unregisterChildScreenContainer(ScreenContainer screenContainer) { + mChildScreenContainers.remove(screenContainer); + } + + public void onViewAnimationStart() { + // onViewAnimationStart is triggered from View#onAnimationStart method of the fragment's root view. + // We override Screen#onAnimationStart and an appropriate method of the StackFragment's root view + // in order to achieve this. + if (isResumed()) { + dispatchOnWillAppear(); + } else { + dispatchOnWillDisappear(); + } } public void onViewAnimationEnd() { // onViewAnimationEnd is triggered from View#onAnimationEnd method of the fragment's root view. // We override Screen#onAnimationEnd and an appropriate method of the StackFragment's root view // in order to achieve this. - dispatchOnAppear(); + if (isResumed()) { + dispatchOnAppear(); + } else { + dispatchOnDisappear(); + } } @Override @@ -85,5 +161,6 @@ public void onDestroy() { .getEventDispatcher() .dispatchEvent(new ScreenDismissedEvent(mScreenView.getId())); } + mChildScreenContainers.clear(); } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java index dbb8315a72..f609d806b6 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java @@ -9,17 +9,18 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.animation.Animation; +import android.view.animation.AnimationSet; import android.widget.LinearLayout; +import com.facebook.react.uimanager.PixelUtil; +import com.google.android.material.appbar.AppBarLayout; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.fragment.app.Fragment; -import com.facebook.react.uimanager.PixelUtil; -import com.google.android.material.appbar.AppBarLayout; - public class ScreenStackFragment extends ScreenFragment { private static class NotifyingCoordinatorLayout extends CoordinatorLayout { @@ -31,10 +32,31 @@ public NotifyingCoordinatorLayout(@NonNull Context context, ScreenFragment fragm mFragment = fragment; } + private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + mFragment.onViewAnimationStart(); + } + + @Override + public void onAnimationEnd(Animation animation) { + mFragment.onViewAnimationEnd(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }; + @Override - protected void onAnimationEnd() { - super.onAnimationEnd(); - mFragment.onViewAnimationEnd(); + public void startAnimation(Animation animation) { + // For some reason View##onAnimationEnd doesn't get called for + // exit transitions so we use this hack. + AnimationSet set = new AnimationSet(true); + set.addAnimation(animation); + set.setAnimationListener(mAnimationListener); + super.startAnimation(set); } } @@ -89,12 +111,32 @@ public void onViewAnimationEnd() { @Nullable @Override - public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { - if (enter && transit == 0) { - // this means that the fragment will appear without transition, in this case onViewAnimationEnd - // won't be called and we need to notify stack directly from here. - notifyViewAppearTransitionEnd(); + public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) { + // this means that the fragment will appear without transition, in this case + // onViewAnimationStart and onViewAnimationEnd won't be called and we need to notify + // stack directly from here. + // When using the Toolbar back button this is called an extra time with transit = 0 but in + // this case we don't want to notify. The way I found to detect is case is check isHidden. + if (transit == 0 && !isHidden()) { + // If the container is nested then appear events will be dispatched by their parent screen so + // they must not be triggered here. + ScreenContainer container = getScreen().getContainer(); + boolean isNested = container != null && container.isNested(); + if (enter) { + if (!isNested) { + dispatchOnWillAppear(); + dispatchOnAppear(); + } + } else { + if (!isNested) { + dispatchOnWillDisappear(); + dispatchOnDisappear(); + } + notifyViewAppearTransitionEnd(); + } + } + return null; } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java index 83bff04dec..0466e718ad 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java @@ -68,8 +68,14 @@ public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( ScreenDismissedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDismissed"), + ScreenWillAppearEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onWillAppear"), ScreenAppearEvent.EVENT_NAME, MapBuilder.of("registrationName", "onAppear"), + ScreenWillDisappearEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onWillDisappear"), + ScreenDisappearEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onDisappear"), StackFinishTransitioningEvent.EVENT_NAME, MapBuilder.of("registrationName", "onFinishTransitioning")); } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenWillAppearEvent.java b/android/src/main/java/com/swmansion/rnscreens/ScreenWillAppearEvent.java new file mode 100644 index 0000000000..a38b6ce164 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenWillAppearEvent.java @@ -0,0 +1,30 @@ +package com.swmansion.rnscreens; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class ScreenWillAppearEvent extends Event { + + public static final String EVENT_NAME = "topWillAppear"; + + public ScreenWillAppearEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } +} diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenWillDisappearEvent.java b/android/src/main/java/com/swmansion/rnscreens/ScreenWillDisappearEvent.java new file mode 100644 index 0000000000..0b81e187c9 --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenWillDisappearEvent.java @@ -0,0 +1,30 @@ +package com.swmansion.rnscreens; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +public class ScreenWillDisappearEvent extends Event { + + public static final String EVENT_NAME = "topWillDisappear"; + + public ScreenWillDisappearEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } +} diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index 1a3bc66f13..6aa02006cf 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -42,7 +42,10 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) { @interface RNSScreenView : RCTView @property (nonatomic, copy) RCTDirectEventBlock onAppear; +@property (nonatomic, copy) RCTDirectEventBlock onDisappear; @property (nonatomic, copy) RCTDirectEventBlock onDismissed; +@property (nonatomic, copy) RCTDirectEventBlock onWillAppear; +@property (nonatomic, copy) RCTDirectEventBlock onWillDisappear; @property (weak, nonatomic) UIView *reactSuperview; @property (nonatomic, retain) UIViewController *controller; @property (nonatomic, readonly) BOOL dismissed; diff --git a/ios/RNSScreen.m b/ios/RNSScreen.m index 479f7db7fb..b29d7d1a5d 100644 --- a/ios/RNSScreen.m +++ b/ios/RNSScreen.m @@ -179,6 +179,20 @@ - (void)notifyDismissed } } +- (void)notifyWillAppear +{ + if (self.onWillAppear) { + self.onWillAppear(nil); + } +} + +- (void)notifyWillDisappear +{ + if (self.onWillDisappear) { + self.onWillDisappear(nil); + } +} + - (void)notifyAppear { if (self.onAppear) { @@ -190,6 +204,13 @@ - (void)notifyAppear } } +- (void)notifyDisappear +{ + if (self.onDisappear) { + self.onDisappear(nil); + } +} + - (BOOL)isMountedUnderScreenOrReactRoot { for (UIView *parent = self.superview; parent != nil; parent = parent.superview) { @@ -299,13 +320,18 @@ - (void)willMoveToParentViewController:(UIViewController *)parent } } -- (void)viewDidDisappear:(BOOL)animated +- (void)viewWillAppear:(BOOL)animated { - [super viewDidDisappear:animated]; - if (self.parentViewController == nil && self.presentingViewController == nil) { - // screen dismissed, send event - [((RNSScreenView *)self.view) notifyDismissed]; - } + [super viewWillAppear:animated]; + + [((RNSScreenView *)self.view) notifyWillAppear]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; + + [((RNSScreenView *)self.view) notifyWillDisappear]; } - (void)viewDidAppear:(BOOL)animated @@ -314,6 +340,17 @@ - (void)viewDidAppear:(BOOL)animated [((RNSScreenView *)self.view) notifyAppear]; } +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; + + [((RNSScreenView *)self.view) notifyDisappear]; + if (self.parentViewController == nil && self.presentingViewController == nil) { + // screen dismissed, send event + [((RNSScreenView *)self.view) notifyDismissed]; + } +} + - (void)notifyFinishTransitioning { [_previousFirstResponder becomeFirstResponder]; @@ -330,7 +367,10 @@ @implementation RNSScreenManager RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation) RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation) +RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onWillDisappear, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onDisappear, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock); - (UIView *)view diff --git a/native-stack/README.md b/native-stack/README.md index f5b137b4f9..56f9b51ed9 100644 --- a/native-stack/README.md +++ b/native-stack/README.md @@ -217,17 +217,52 @@ React.useEffect( ); ``` -#### `finishTransitioning` +#### `transitionStart` -Event which fires when the current screen finishes its transition. +Event which fires when a transition animation starts. + +Event data: + +- `closing` - Whether the screen will be dismissed or will appear. Example: ```js React.useEffect( () => { - const unsubscribe = navigation.addListener('finishTransitioning', e => { - // Do something + const unsubscribe = navigation.addListener('transitionStart', e => { + if (e.data.closing) { + // Will be dismissed + } else { + // Will appear + } + }); + + return unsubscribe; + }, + [navigation] +); +``` + +#### `transitionEnd` + +Event which fires when a transition animation ends. + +Event data: + +- `closing` - Whether the screen was dismissed or did appear. + +Example: + +```js +React.useEffect( + () => { + const unsubscribe = navigation.addListener('transitionEnd', e => { + if (e.data.closing) { + // Was dismissed + } else { + // Did appear + } }); return unsubscribe; diff --git a/src/index.d.ts b/src/index.d.ts index fa6fa2d1d2..0f2985c0f1 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -50,12 +50,25 @@ declare module 'react-native-screens' { active?: 0 | 1 | Animated.AnimatedInterpolation; onComponentRef?: (view: any) => void; children?: React.ReactNode; + + /** + * @description A callback that gets called when the current screen will appear. This is called as soon as the transition begins. + */ + onWillAppear?: (e: NativeSyntheticEvent) => void; /** - *@description A callback that gets called when the current screen appears. + * @description A callback that gets called when the current screen will disappear. This is called as soon as the transition begins. + */ + onWillDisappear?: (e: NativeSyntheticEvent) => void; + /** + * @description A callback that gets called when the current screen appears. */ onAppear?: (e: NativeSyntheticEvent) => void; /** - *@description A callback that gets called when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down). The callback takes no arguments. + * @description A callback that gets called when the current screen disappears. + */ + onDisappear?: (e: NativeSyntheticEvent) => void; + /** + * @description A callback that gets called when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down). The callback takes no arguments. */ onDismissed?: (e: NativeSyntheticEvent) => void; /** @@ -69,7 +82,7 @@ declare module 'react-native-screens' { */ stackPresentation: StackPresentationTypes; /** - *@description Allows for the customization of how the given screen should appear/dissapear when pushed or popped at the top of the stack. The followin values are currently supported: + * @description Allows for the customization of how the given screen should appear/dissapear when pushed or popped at the top of the stack. The followin values are currently supported: * @type "default" – uses a platform default animation * @type "fade" – fades screen in or out * @type "flip" – flips the screen, requires stackPresentation: "modal" (iOS only) diff --git a/src/native-stack/types.tsx b/src/native-stack/types.tsx index 6f9444a9ac..b463ad3bf7 100644 --- a/src/native-stack/types.tsx +++ b/src/native-stack/types.tsx @@ -23,6 +23,14 @@ export type NativeStackNavigationEventMap = { * Event which fires when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down). */ dismiss: { data: undefined }; + /** + * Event which fires when a transition animation starts. + */ + transitionStart: { data: { closing: boolean } }; + /** + * Event which fires when a transition animation ends. + */ + transitionEnd: { data: { closing: boolean } }; }; export type NativeStackNavigationProp< diff --git a/src/native-stack/views/NativeStackView.tsx b/src/native-stack/views/NativeStackView.tsx index 4b27b7e26f..817178a2aa 100644 --- a/src/native-stack/views/NativeStackView.tsx +++ b/src/native-stack/views/NativeStackView.tsx @@ -49,11 +49,37 @@ export default function NativeStackView({ gestureEnabled={Platform.OS === 'android' ? false : gestureEnabled} stackPresentation={stackPresentation} stackAnimation={stackAnimation} + onWillAppear={() => { + navigation.emit({ + type: 'transitionStart', + data: { closing: false }, + target: route.key, + }); + }} + onWillDisappear={() => { + navigation.emit({ + type: 'transitionStart', + data: { closing: true }, + target: route.key, + }); + }} onAppear={() => { navigation.emit({ type: 'appear', target: route.key, }); + navigation.emit({ + type: 'transitionEnd', + data: { closing: false }, + target: route.key, + }); + }} + onDisappear={() => { + navigation.emit({ + type: 'transitionEnd', + data: { closing: true }, + target: route.key, + }); }} onDismissed={() => { navigation.emit({