diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenAppearEvent.java b/android/src/main/java/com/swmansion/rnscreens/ScreenAppearEvent.java new file mode 100644 index 0000000000..31d77c5b9a --- /dev/null +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenAppearEvent.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 ScreenAppearEvent extends Event { + + public static final String EVENT_NAME = "topAppear"; + + public ScreenAppearEvent(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/ScreenContainer.java b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java index e64922eca3..0b141e5943 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.ContextWrapper; +import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; @@ -22,11 +23,14 @@ public class ScreenContainer extends ViewGroup { protected final ArrayList mScreenFragments = new ArrayList<>(); private final Set mActiveScreenFragments = new HashSet<>(); + private final ArrayList mAfterTransitionRunnables = new ArrayList<>(1); private @Nullable FragmentManager mFragmentManager; private @Nullable FragmentTransaction mCurrentTransaction; + private @Nullable FragmentTransaction mProcessingTransaction; private boolean mNeedUpdate; private boolean mIsAttached; + private boolean mIsTransitioning; private boolean mLayoutEnqueued = false; private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() { @@ -101,6 +105,36 @@ protected void removeScreenAt(int index) { markUpdated(); } + @Override + public void startViewTransition(View view) { + super.startViewTransition(view); + mIsTransitioning = true; + } + + @Override + public void endViewTransition(View view) { + super.endViewTransition(view); + if (mIsTransitioning) { + mIsTransitioning = false; + notifyTransitionFinished(); + } + } + + public boolean isTransitioning() { + return mIsTransitioning || mProcessingTransaction != null; + } + + public void postAfterTransition(Runnable runnable) { + mAfterTransitionRunnables.add(runnable); + } + + protected void notifyTransitionFinished() { + for (int i = 0, size = mAfterTransitionRunnables.size(); i < size; i++) { + mAfterTransitionRunnables.get(i).run(); + } + mAfterTransitionRunnables.clear(); + } + protected int getScreenCount() { return mScreenFragments.size(); } @@ -159,6 +193,19 @@ protected FragmentTransaction getOrCreateTransaction() { protected void tryCommitTransaction() { if (mCurrentTransaction != null) { + final FragmentTransaction transaction = mCurrentTransaction; + mProcessingTransaction = transaction; + mProcessingTransaction.runOnCommit(new Runnable() { + @Override + public void run() { + if (mProcessingTransaction == transaction) { + // we need to take into account that commit is initiated with some other transaction while + // the previous one is still processing. In this case mProcessingTransaction gets overwritten + // and we don't want to set it to null until the second transaction is finished. + mProcessingTransaction = null; + } + } + }); mCurrentTransaction.commitAllowingStateLoss(); mCurrentTransaction = null; } @@ -184,6 +231,10 @@ protected boolean isScreenActive(ScreenFragment screenFragment) { return screenFragment.getScreen().isActive(); } + protected boolean hasScreen(ScreenFragment screenFragment) { + return mScreenFragments.contains(screenFragment); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java index 09fd4582d1..028ca5a851 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.java @@ -7,12 +7,10 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; -import androidx.appcompat.widget.Toolbar; import androidx.fragment.app.Fragment; import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.UIManagerModule; -import com.facebook.react.uimanager.events.EventDispatcher; public class ScreenFragment extends Fragment { @@ -39,12 +37,39 @@ public Screen getScreen() { return mScreenView; } - @Override - public void onDestroy() { - super.onDestroy(); + private void dispatchOnAppear() { ((ReactContext) mScreenView.getContext()) .getNativeModule(UIManagerModule.class) .getEventDispatcher() - .dispatchEvent(new ScreenDismissedEvent(mScreenView.getId())); + .dispatchEvent(new ScreenAppearEvent(mScreenView.getId())); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ScreenContainer container = mScreenView.getContainer(); + if (container.isTransitioning()) { + container.postAfterTransition(new Runnable() { + @Override + public void run() { + dispatchOnAppear(); + } + }); + } else { + dispatchOnAppear(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + ScreenContainer container = mScreenView.getContainer(); + if (container == null || !container.hasScreen(this)) { + // we only send dismissed even when the screen has been removed from its container + ((ReactContext) mScreenView.getContext()) + .getNativeModule(UIManagerModule.class) + .getEventDispatcher() + .dispatchEvent(new ScreenDismissedEvent(mScreenView.getId())); + } } } diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java index 58bb884f76..01878b8205 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStack.java @@ -97,6 +97,11 @@ protected void removeScreenAt(int index) { super.removeScreenAt(index); } + @Override + protected boolean hasScreen(ScreenFragment screenFragment) { + return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment); + } + @Override protected void onUpdate() { // remove all screens previously on stack @@ -128,19 +133,15 @@ protected void onUpdate() { } for (ScreenStackFragment screen : mScreenFragments) { - // add all new views that weren't on stack before - if (!mStack.contains(screen) && !mDismissed.contains(screen)) { - getOrCreateTransaction().add(getId(), screen); - } // detach all screens that should not be visible if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) { - getOrCreateTransaction().hide(screen); + getOrCreateTransaction().remove(screen); } } // attach "below top" screen if set - if (belowTop != null) { + if (belowTop != null && !belowTop.isAdded()) { final ScreenStackFragment top = newTop; - getOrCreateTransaction().show(belowTop).runOnCommit(new Runnable() { + getOrCreateTransaction().add(getId(), belowTop).runOnCommit(new Runnable() { @Override public void run() { top.getScreen().bringToFront(); @@ -148,8 +149,8 @@ public void run() { }); } - if (newTop != null) { - getOrCreateTransaction().show(newTop); + if (newTop != null && !newTop.isAdded()) { + getOrCreateTransaction().add(getId(), newTop); } if (!mStack.contains(newTop)) { diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java index c38906d6e1..32cb92b9cd 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.java @@ -22,6 +22,7 @@ public class ScreenStackFragment extends ScreenFragment { private AppBarLayout mAppBarLayout; private Toolbar mToolbar; private boolean mShadowHidden; + private CoordinatorLayout mScreenRootView; @SuppressLint("ValidFragment") public ScreenStackFragment(Screen screenView) { @@ -59,10 +60,7 @@ public void onStackUpdate() { } } - @Override - public View onCreateView(LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { + private CoordinatorLayout configureView() { CoordinatorLayout view = new CoordinatorLayout(getContext()); CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT); @@ -87,6 +85,17 @@ public View onCreateView(LayoutInflater inflater, return view; } + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (mScreenRootView == null) { + mScreenRootView = configureView(); + } + + return mScreenRootView; + } + public boolean isDismissable() { View child = mScreenView.getChildAt(0); if (child instanceof ScreenStackHeaderConfig) { diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java index 2bd073cb4d..b18e56eaea 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java @@ -62,6 +62,8 @@ public void setStackAnimation(Screen view, String animation) { public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( ScreenDismissedEvent.EVENT_NAME, - MapBuilder.of("registrationName", "onDismissed")); + MapBuilder.of("registrationName", "onDismissed"), + ScreenAppearEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onAppear")); } } diff --git a/createNativeStackNavigator.js b/createNativeStackNavigator.js index d05af4a5c2..4736f5b3c7 100644 --- a/createNativeStackNavigator.js +++ b/createNativeStackNavigator.js @@ -4,7 +4,6 @@ import { StackRouter, SceneView, StackActions, - NavigationActions, createNavigator, } from '@react-navigation/core'; import { createKeyboardAwareNavigator } from '@react-navigation/native'; @@ -27,14 +26,13 @@ function renderComponentOrThunk(componentOrThunk, props) { class StackView extends React.Component { _removeScene = route => { - const { navigation } = this.props; - navigation.dispatch( - NavigationActions.back({ - key: route.key, - immediate: true, - }) + this.props.navigation.dispatch(StackActions.pop({ key: route.key })); + }; + + _onSceneFocus = route => { + this.props.navigation.dispatch( + StackActions.completeTransition({ toChildKey: route.key }) ); - navigation.dispatch(StackActions.completeTransition()); }; _renderHeaderConfig = (index, route, descriptor) => { @@ -165,7 +163,7 @@ class StackView extends React.Component { transparentCard || options.cardTransparent ? 'transparentModal' : mode; } - let stackAnimation = undefined; + let stackAnimation; if (options.animationEnabled === false) { stackAnimation = 'none'; } @@ -177,6 +175,7 @@ class StackView extends React.Component { style={options.cardStyle} stackAnimation={stackAnimation} stackPresentation={stackPresentation} + onAppear={() => this._onSceneFocus(route)} onDismissed={() => this._removeScene(route)}> {this._renderHeaderConfig(index, route, descriptor)} *reactSuperview; @property (nonatomic, retain) UIViewController *controller; diff --git a/ios/RNSScreen.m b/ios/RNSScreen.m index 22bba3cfe4..90cf4dd6ad 100644 --- a/ios/RNSScreen.m +++ b/ios/RNSScreen.m @@ -135,6 +135,17 @@ - (void)notifyDismissed } } +- (void)notifyAppear +{ + if (self.onAppear) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.onAppear) { + self.onAppear(nil); + } + }); + } +} + - (BOOL)isMountedUnderScreenOrReactRoot { for (UIView *parent = self.superview; parent != nil; parent = parent.superview) { @@ -235,6 +246,12 @@ - (void)viewDidDisappear:(BOOL)animated } } +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + [((RNSScreenView *)self.view) notifyAppear]; +} + - (void)notifyFinishTransitioning { [_previousFirstResponder becomeFirstResponder]; @@ -258,6 +275,7 @@ @implementation RNSScreenManager RCT_EXPORT_VIEW_PROPERTY(active, BOOL) RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation) RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation) +RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock); - (UIView *)view