mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-30 22:50:22 +08:00
Compare commits
36 Commits
2.0.0-beta
...
2.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f8b460549 | ||
|
|
b52aea3fd2 | ||
|
|
4e72ad050b | ||
|
|
0dd03e2c53 | ||
|
|
e38c5bfdc2 | ||
|
|
cdbf4e463f | ||
|
|
4e8d13dc72 | ||
|
|
d3b6bea594 | ||
|
|
b29e634e26 | ||
|
|
f2caf02d8c | ||
|
|
c8845cbb6a | ||
|
|
9bf2edd405 | ||
|
|
748cdc6fba | ||
|
|
89cf0b7e52 | ||
|
|
db4733ad05 | ||
|
|
8952e698d2 | ||
|
|
953b3b30db | ||
|
|
e50b7eae72 | ||
|
|
0e00f49e69 | ||
|
|
26d8dc21d2 | ||
|
|
3193e7da8f | ||
|
|
cce8373a20 | ||
|
|
823d11e691 | ||
|
|
1aeba7faa1 | ||
|
|
e6ed4176cd | ||
|
|
4f792b4281 | ||
|
|
d35c523c37 | ||
|
|
67806cbbb5 | ||
|
|
6212847218 | ||
|
|
a87faf443f | ||
|
|
bdf610bcad | ||
|
|
102880c18b | ||
|
|
24b70abd64 | ||
|
|
f7b6c22591 | ||
|
|
ed997ef4ec | ||
|
|
10a0badee2 |
@@ -67,9 +67,9 @@ yarn add react-native-screens
|
||||
2. Open your App.js file and add the following snippet somewhere near the top of the file (e.g. right after import statements):
|
||||
|
||||
```js
|
||||
import { useScreens } from 'react-native-screens';
|
||||
import { enableScreens } from 'react-native-screens';
|
||||
|
||||
useScreens();
|
||||
enableScreens();
|
||||
```
|
||||
|
||||
3. That's all 🎉 – enjoy faster navigation in your Expo app. Keep in mind screens are in pretty early phase so please report if you discover some issues.
|
||||
|
||||
@@ -49,8 +49,9 @@ repositories {
|
||||
dependencies {
|
||||
implementation 'com.facebook.react:react-native:+'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.fragment:fragment:1.2.1'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
}
|
||||
|
||||
def configureReactNativePom(def pom) {
|
||||
|
||||
@@ -2,18 +2,18 @@ package com.swmansion.rnscreens;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Parcelable;
|
||||
import android.util.SparseArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.facebook.react.bridge.GuardedRunnable;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.uimanager.PointerEvents;
|
||||
import com.facebook.react.uimanager.ReactPointerEventsView;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
|
||||
public class Screen extends ViewGroup {
|
||||
@@ -46,7 +46,7 @@ public class Screen extends ViewGroup {
|
||||
}
|
||||
};
|
||||
|
||||
private @Nullable Fragment mFragment;
|
||||
private @Nullable ScreenFragment mFragment;
|
||||
private @Nullable ScreenContainer mContainer;
|
||||
private boolean mActive;
|
||||
private boolean mTransitioning;
|
||||
@@ -54,8 +54,39 @@ public class Screen extends ViewGroup {
|
||||
private StackAnimation mStackAnimation = StackAnimation.DEFAULT;
|
||||
private boolean mGestureEnabled = true;
|
||||
|
||||
@Override
|
||||
protected void onAnimationEnd() {
|
||||
super.onAnimationEnd();
|
||||
if (mFragment != null) {
|
||||
mFragment.onViewAnimationEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public Screen(ReactContext context) {
|
||||
super(context);
|
||||
// we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
|
||||
// not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
|
||||
// fact that TextView implementation is expected to be attached to window when layout happens.
|
||||
// Then, at the moment of layout it checks whether window type is in a reasonable range to tell
|
||||
// whether it should enable selection controlls (see Editor.java#prepareCursorControllers).
|
||||
// With screens, however, the text input component can be laid out before it is attached, in that
|
||||
// case TextView tries to get window type property from the oldest existing parent, which in this
|
||||
// case is a Screen class, as it is the root of the screen that is about to be attached. Setting
|
||||
// params this way is not the most elegant way to solve this problem but workarounds it for the
|
||||
// time being
|
||||
setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
|
||||
// do nothing, react native will keep the view hierarchy so no need to serialize/deserialize
|
||||
// view's states. The side effect of restoring is that TextInput components would trigger set-text
|
||||
// events which may confuse text input handling.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
|
||||
// ignore restoring instance state too as we are not saving anything anyways.
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,11 +175,11 @@ public class Screen extends ViewGroup {
|
||||
mContainer = container;
|
||||
}
|
||||
|
||||
protected void setFragment(Fragment fragment) {
|
||||
protected void setFragment(ScreenFragment fragment) {
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
protected @Nullable Fragment getFragment() {
|
||||
protected @Nullable ScreenFragment getFragment() {
|
||||
return mFragment;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
@@ -22,15 +23,12 @@ import java.util.Set;
|
||||
public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
|
||||
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
|
||||
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
|
||||
private final ArrayList<Runnable> mAfterTransitionRunnables = new ArrayList<>(1);
|
||||
|
||||
protected @Nullable FragmentManager mFragmentManager;
|
||||
private @Nullable FragmentTransaction mCurrentTransaction;
|
||||
private @Nullable FragmentTransaction mProcessingTransaction;
|
||||
private boolean mNeedUpdate;
|
||||
private boolean mIsAttached;
|
||||
private boolean mIsTransitioning;
|
||||
private boolean mLayoutEnqueued = false;
|
||||
|
||||
|
||||
@@ -118,36 +116,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
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();
|
||||
}
|
||||
@@ -219,7 +187,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
|
||||
private void attachScreen(ScreenFragment screenFragment) {
|
||||
getOrCreateTransaction().add(getId(), screenFragment);
|
||||
mActiveScreenFragments.add(screenFragment);
|
||||
}
|
||||
|
||||
private void moveToFront(ScreenFragment screenFragment) {
|
||||
@@ -230,7 +197,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
|
||||
private void detachScreen(ScreenFragment screenFragment) {
|
||||
getOrCreateTransaction().remove(screenFragment);
|
||||
mActiveScreenFragments.remove(screenFragment);
|
||||
}
|
||||
|
||||
protected boolean isScreenActive(ScreenFragment screenFragment) {
|
||||
@@ -252,16 +218,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
// if there are pending transactions and this view is about to get detached we need to perform
|
||||
// them here as otherwise fragment manager will crash because it won't be able to find container
|
||||
// view.
|
||||
if (mFragmentManager != null && !mFragmentManager.isDestroyed()) {
|
||||
mFragmentManager.executePendingTransactions();
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
mIsAttached = false;
|
||||
|
||||
// fragment manager is destroyed so we can't do anything with it anymore
|
||||
mFragmentManager = null;
|
||||
// so we don't add the same screen twice after re-attach
|
||||
removeAllViews();
|
||||
mActiveScreenFragments.clear();
|
||||
// after re-attach we'll update the screen and add views again
|
||||
markUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -282,11 +246,11 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
|
||||
protected void onUpdate() {
|
||||
// detach screens that are no longer active
|
||||
Set<ScreenFragment> orphaned = new HashSet<>(mActiveScreenFragments);
|
||||
Set<Fragment> orphaned = new HashSet<>(mFragmentManager.getFragments());
|
||||
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
|
||||
ScreenFragment screenFragment = mScreenFragments.get(i);
|
||||
boolean isActive = isScreenActive(screenFragment);
|
||||
if (!isActive && mActiveScreenFragments.contains(screenFragment)) {
|
||||
if (!isActive && screenFragment.isAdded()) {
|
||||
detachScreen(screenFragment);
|
||||
}
|
||||
orphaned.remove(screenFragment);
|
||||
@@ -294,7 +258,9 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
if (!orphaned.isEmpty()) {
|
||||
Object[] orphanedAry = orphaned.toArray();
|
||||
for (int i = 0; i < orphanedAry.length; i++) {
|
||||
detachScreen((ScreenFragment) orphanedAry[i]);
|
||||
if (orphanedAry[i] instanceof ScreenFragment) {
|
||||
detachScreen((ScreenFragment) orphanedAry[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +278,7 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
|
||||
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
|
||||
ScreenFragment screenFragment = mScreenFragments.get(i);
|
||||
boolean isActive = isScreenActive(screenFragment);
|
||||
if (isActive && !mActiveScreenFragments.contains(screenFragment)) {
|
||||
if (isActive && !screenFragment.isAdded()) {
|
||||
addedBefore = true;
|
||||
attachScreen(screenFragment);
|
||||
} else if (isActive && addedBefore) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
@@ -14,6 +15,21 @@ import com.facebook.react.uimanager.UIManagerModule;
|
||||
|
||||
public class ScreenFragment extends Fragment {
|
||||
|
||||
protected static View recycleView(View view) {
|
||||
// screen fragments reuse view instances instead of creating new ones. In order to reuse a given
|
||||
// view it needs to be detached from the view hierarchy to allow the fragment to attach it back.
|
||||
ViewParent parent = view.getParent();
|
||||
if (parent != null) {
|
||||
((ViewGroup) parent).endViewTransition(view);
|
||||
((ViewGroup) parent).removeView(view);
|
||||
}
|
||||
// view detached from fragment manager get their visibility changed to GONE after their state is
|
||||
// dumped. Since we don't restore the state but want to reuse the view we need to change visibility
|
||||
// back to VISIBLE in order for the fragment manager to animate in the view.
|
||||
view.setVisibility(View.VISIBLE);
|
||||
return view;
|
||||
}
|
||||
|
||||
protected Screen mScreenView;
|
||||
|
||||
public ScreenFragment() {
|
||||
@@ -30,7 +46,7 @@ public class ScreenFragment extends Fragment {
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return mScreenView;
|
||||
return recycleView(mScreenView);
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
@@ -45,19 +61,21 @@ public class ScreenFragment extends Fragment {
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
recycleView(getView());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.swmansion.rnscreens;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
@@ -18,6 +22,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
|
||||
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
|
||||
|
||||
private ScreenStackFragment mTopScreen = null;
|
||||
private boolean mRemovalTransitionStarted = false;
|
||||
|
||||
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
|
||||
@Override
|
||||
@@ -50,7 +55,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
|
||||
}
|
||||
|
||||
public Screen getTopScreen() {
|
||||
return mTopScreen.getScreen();
|
||||
return mTopScreen != null ? mTopScreen.getScreen() : null;
|
||||
}
|
||||
|
||||
public Screen getRootScreen() {
|
||||
@@ -90,6 +95,34 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
|
||||
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startViewTransition(View view) {
|
||||
super.startViewTransition(view);
|
||||
mRemovalTransitionStarted = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endViewTransition(View view) {
|
||||
super.endViewTransition(view);
|
||||
if (mRemovalTransitionStarted) {
|
||||
mRemovalTransitionStarted = false;
|
||||
dispatchOnFinishTransitioning();
|
||||
}
|
||||
}
|
||||
|
||||
public void onViewAppearTransitionEnd() {
|
||||
if (!mRemovalTransitionStarted) {
|
||||
dispatchOnFinishTransitioning();
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchOnFinishTransitioning() {
|
||||
((ReactContext) getContext())
|
||||
.getNativeModule(UIManagerModule.class)
|
||||
.getEventDispatcher()
|
||||
.dispatchEvent(new StackFinishTransitioningEvent(getId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeScreenAt(int index) {
|
||||
Screen toBeRemoved = getScreenAt(index);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package com.swmansion.rnscreens;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
@@ -18,6 +22,22 @@ import com.google.android.material.appbar.AppBarLayout;
|
||||
|
||||
public class ScreenStackFragment extends ScreenFragment {
|
||||
|
||||
private static class NotifyingCoordinatorLayout extends CoordinatorLayout {
|
||||
|
||||
private final ScreenFragment mFragment;
|
||||
|
||||
public NotifyingCoordinatorLayout(@NonNull Context context, ScreenFragment fragment) {
|
||||
super(context);
|
||||
mFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAnimationEnd() {
|
||||
super.onAnimationEnd();
|
||||
mFragment.onViewAnimationEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private static final float TOOLBAR_ELEVATION = PixelUtil.toPixelFromDIP(4);
|
||||
|
||||
private AppBarLayout mAppBarLayout;
|
||||
@@ -62,7 +82,7 @@ public class ScreenStackFragment extends ScreenFragment {
|
||||
}
|
||||
|
||||
private CoordinatorLayout configureView() {
|
||||
CoordinatorLayout view = new CoordinatorLayout(getContext());
|
||||
CoordinatorLayout view = new NotifyingCoordinatorLayout(getContext(), this);
|
||||
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
|
||||
params.setBehavior(new AppBarLayout.ScrollingViewBehavior());
|
||||
@@ -86,6 +106,30 @@ public class ScreenStackFragment extends ScreenFragment {
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAnimationEnd() {
|
||||
super.onViewAnimationEnd();
|
||||
notifyViewAppearTransitionEnd();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void notifyViewAppearTransitionEnd() {
|
||||
ViewParent screenStack = getView().getParent();
|
||||
if (screenStack instanceof ScreenStack) {
|
||||
((ScreenStack) screenStack).onViewAppearTransitionEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@@ -94,7 +138,7 @@ public class ScreenStackFragment extends ScreenFragment {
|
||||
mScreenRootView = configureView();
|
||||
}
|
||||
|
||||
return mScreenRootView;
|
||||
return recycleView(mScreenRootView);
|
||||
}
|
||||
|
||||
public boolean isDismissable() {
|
||||
|
||||
@@ -132,6 +132,11 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
return;
|
||||
}
|
||||
|
||||
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mIsHidden) {
|
||||
if (mToolbar.getParent() != null) {
|
||||
getScreenFragment().removeToolbar();
|
||||
@@ -143,7 +148,6 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
getScreenFragment().setToolbar(mToolbar);
|
||||
}
|
||||
|
||||
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
|
||||
activity.setSupportActionBar(mToolbar);
|
||||
ActionBar actionBar = activity.getSupportActionBar();
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
|
||||
public void setStackPresentation(Screen view, String presentation) {
|
||||
if ("push".equals(presentation)) {
|
||||
view.setStackPresentation(Screen.StackPresentation.PUSH);
|
||||
} else if ("modal".equals(presentation) || "containedModal".equals(presentation)) {
|
||||
} else if ("modal".equals(presentation) || "containedModal".equals(presentation) || "fullScreenModal".equals(presentation) || "formSheet".equals(presentation)) {
|
||||
// at the moment Android implementation does not handle contained vs regular modals
|
||||
view.setStackPresentation(Screen.StackPresentation.MODAL);
|
||||
} else if ("transparentModal".equals(presentation) || "containedTransparentModal".equals((presentation))) {
|
||||
@@ -69,6 +69,8 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
|
||||
ScreenDismissedEvent.EVENT_NAME,
|
||||
MapBuilder.of("registrationName", "onDismissed"),
|
||||
ScreenAppearEvent.EVENT_NAME,
|
||||
MapBuilder.of("registrationName", "onAppear"));
|
||||
MapBuilder.of("registrationName", "onAppear"),
|
||||
StackFinishTransitioningEvent.EVENT_NAME,
|
||||
MapBuilder.of("registrationName", "onFinishTransitioning"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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 StackFinishTransitioningEvent extends Event<ScreenAppearEvent> {
|
||||
public static final String EVENT_NAME = "topFinishTransitioning";
|
||||
|
||||
public StackFinishTransitioningEvent(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());
|
||||
}
|
||||
}
|
||||
@@ -30,15 +30,31 @@ class StackView extends React.Component {
|
||||
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
|
||||
};
|
||||
|
||||
_onSceneFocus = (route, descriptor) => {
|
||||
_onAppear = (route, descriptor) => {
|
||||
descriptor.options &&
|
||||
descriptor.options.onAppear &&
|
||||
descriptor.options.onAppear();
|
||||
this.props.navigation.dispatch(
|
||||
StackActions.completeTransition({ toChildKey: route.key })
|
||||
StackActions.completeTransition({
|
||||
toChildKey: route.key,
|
||||
key: this.props.navigation.state.key,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
_onFinishTransitioning = () => {
|
||||
const { routes } = this.props.navigation.state;
|
||||
let lastRoute = routes && routes.length && routes[routes.length - 1];
|
||||
if (lastRoute) {
|
||||
this.props.navigation.dispatch(
|
||||
StackActions.completeTransition({
|
||||
toChildKey: lastRoute.key,
|
||||
key: this.props.navigation.state.key,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_renderHeaderConfig = (index, route, descriptor) => {
|
||||
const { navigationConfig } = this.props;
|
||||
const { options } = descriptor;
|
||||
@@ -188,13 +204,13 @@ class StackView extends React.Component {
|
||||
return (
|
||||
<Screen
|
||||
key={`screen_${route.key}`}
|
||||
style={options.cardStyle}
|
||||
style={[StyleSheet.absoluteFill, options.cardStyle]}
|
||||
stackAnimation={stackAnimation}
|
||||
stackPresentation={stackPresentation}
|
||||
gestureEnabled={
|
||||
options.gestureEnabled === undefined ? true : options.gestureEnabled
|
||||
}
|
||||
onAppear={() => this._onSceneFocus(route, descriptor)}
|
||||
onAppear={() => this._onAppear(route, descriptor)}
|
||||
onDismissed={() => this._removeScene(route)}>
|
||||
{this._renderHeaderConfig(index, route, descriptor)}
|
||||
<SceneView
|
||||
@@ -210,7 +226,9 @@ class StackView extends React.Component {
|
||||
const { navigation, descriptors } = this.props;
|
||||
|
||||
return (
|
||||
<ScreenStack style={styles.scenes}>
|
||||
<ScreenStack
|
||||
style={styles.scenes}
|
||||
onFinishTransitioning={this._onFinishTransitioning}>
|
||||
{navigation.state.routes.map((route, i) =>
|
||||
this._renderScene(i, route, descriptors[route.key])
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,9 @@ typedef NS_ENUM(NSInteger, RNSScreenStackPresentation) {
|
||||
RNSScreenStackPresentationModal,
|
||||
RNSScreenStackPresentationTransparentModal,
|
||||
RNSScreenStackPresentationContainedModal,
|
||||
RNSScreenStackPresentationContainedTransparentModal
|
||||
RNSScreenStackPresentationContainedTransparentModal,
|
||||
RNSScreenStackPresentationFullScreenModal,
|
||||
RNSScreenStackPresentationFormSheet
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#import <React/RCTShadowView.h>
|
||||
#import <React/RCTTouchHandler.h>
|
||||
|
||||
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
|
||||
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate, RCTInvalidating>
|
||||
@end
|
||||
|
||||
@implementation RNSScreenView {
|
||||
@@ -34,11 +34,21 @@
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
if (_active) {
|
||||
if (![self.reactViewController.parentViewController
|
||||
isKindOfClass:[UINavigationController class]]) {
|
||||
[super reactSetFrame:frame];
|
||||
}
|
||||
// ignore setFrame call from react, the frame of this view
|
||||
// is controlled by the UIViewController it is contained in
|
||||
// when screen is mounted under UINavigationController it's size is controller
|
||||
// by the navigation controller itself. That is, it is set to fill space of
|
||||
// the controller. In that case we ignore react layout system from managing
|
||||
// the screen dimentions and we wait for the screen VC to update and then we
|
||||
// pass the dimentions to ui view manager to take into account when laying out
|
||||
// subviews
|
||||
}
|
||||
|
||||
- (UIViewController *)reactViewController
|
||||
{
|
||||
return _controller;
|
||||
}
|
||||
|
||||
- (void)updateBounds
|
||||
@@ -62,7 +72,6 @@
|
||||
|
||||
- (void)setStackPresentation:(RNSScreenStackPresentation)stackPresentation
|
||||
{
|
||||
_stackPresentation = stackPresentation;
|
||||
switch (stackPresentation) {
|
||||
case RNSScreenStackPresentationModal:
|
||||
#ifdef __IPHONE_13_0
|
||||
@@ -75,6 +84,12 @@
|
||||
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
#endif
|
||||
break;
|
||||
case RNSScreenStackPresentationFullScreenModal:
|
||||
_controller.modalPresentationStyle = UIModalPresentationFullScreen;
|
||||
break;
|
||||
case RNSScreenStackPresentationFormSheet:
|
||||
_controller.modalPresentationStyle = UIModalPresentationFormSheet;
|
||||
break;
|
||||
case RNSScreenStackPresentationTransparentModal:
|
||||
_controller.modalPresentationStyle = UIModalPresentationOverFullScreen;
|
||||
break;
|
||||
@@ -84,11 +99,23 @@
|
||||
case RNSScreenStackPresentationContainedTransparentModal:
|
||||
_controller.modalPresentationStyle = UIModalPresentationOverCurrentContext;
|
||||
break;
|
||||
case RNSScreenStackPresentationPush:
|
||||
// ignored, we only need to keep in mind not to set presentation delegate
|
||||
break;
|
||||
}
|
||||
// `modalPresentationStyle` must be set before accessing `presentationController`
|
||||
// otherwise a default controller will be created and cannot be changed after.
|
||||
// Documented here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
|
||||
_controller.presentationController.delegate = self;
|
||||
// There is a bug in UIKit which causes retain loop when presentationController is accessed for a
|
||||
// controller that is not going to be presented modally. We therefore need to avoid setting the
|
||||
// delegate for screens presented using push. This also means that when controller is updated from
|
||||
// modal to push type, this may cause memory leak, we warn about that as well.
|
||||
if (stackPresentation != RNSScreenStackPresentationPush) {
|
||||
// `modalPresentationStyle` must be set before accessing `presentationController`
|
||||
// otherwise a default controller will be created and cannot be changed after.
|
||||
// Documented here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621426-presentationcontroller?language=objc
|
||||
_controller.presentationController.delegate = self;
|
||||
} else if (_stackPresentation != RNSScreenStackPresentationPush) {
|
||||
RCTLogError(@"Screen presentation updated from modal to push, this may likely result in a screen object leakage. If you need to change presentation style create a new screen object instead");
|
||||
}
|
||||
_stackPresentation = stackPresentation;
|
||||
}
|
||||
|
||||
- (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
|
||||
@@ -109,6 +136,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setGestureEnabled:(BOOL)gestureEnabled
|
||||
{
|
||||
#ifdef __IPHONE_13_0
|
||||
if (@available(iOS 13.0, *)) {
|
||||
_controller.modalInPresentation = !gestureEnabled;
|
||||
}
|
||||
#endif
|
||||
|
||||
_gestureEnabled = gestureEnabled;
|
||||
}
|
||||
|
||||
- (UIView *)reactSuperview
|
||||
{
|
||||
return _reactSuperview;
|
||||
@@ -191,10 +229,27 @@
|
||||
[_touchHandler reset];
|
||||
}
|
||||
|
||||
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
return _gestureEnabled;
|
||||
}
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
|
||||
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:)
|
||||
withObject:presentationController];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
_controller = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreen {
|
||||
__weak UIView *_view;
|
||||
__weak id _previousFirstResponder;
|
||||
CGRect _lastViewFrame;
|
||||
}
|
||||
@@ -202,7 +257,7 @@
|
||||
- (instancetype)initWithView:(UIView *)view
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_view = view;
|
||||
self.view = view;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -213,7 +268,7 @@
|
||||
|
||||
if (!CGRectEqualToRect(_lastViewFrame, self.view.frame)) {
|
||||
_lastViewFrame = self.view.frame;
|
||||
[((RNSScreenView *)self.view) updateBounds];
|
||||
[((RNSScreenView *)self.viewIfLoaded) updateBounds];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +288,7 @@
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent
|
||||
{
|
||||
[super willMoveToParentViewController:parent];
|
||||
if (parent == nil) {
|
||||
id responder = [self findFirstResponder:self.view];
|
||||
if (responder != nil) {
|
||||
@@ -262,14 +318,6 @@
|
||||
_previousFirstResponder = nil;
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
if (_view != nil) {
|
||||
self.view = _view;
|
||||
_view = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreenManager
|
||||
@@ -295,6 +343,8 @@ RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
|
||||
RCT_ENUM_CONVERTER(RNSScreenStackPresentation, (@{
|
||||
@"push": @(RNSScreenStackPresentationPush),
|
||||
@"modal": @(RNSScreenStackPresentationModal),
|
||||
@"fullScreenModal": @(RNSScreenStackPresentationFullScreenModal),
|
||||
@"formSheet": @(RNSScreenStackPresentationFormSheet),
|
||||
@"containedModal": @(RNSScreenStackPresentationContainedModal),
|
||||
@"transparentModal": @(RNSScreenStackPresentationTransparentModal),
|
||||
@"containedTransparentModal": @(RNSScreenStackPresentationContainedTransparentModal)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface RNSScreenContainerView ()
|
||||
@interface RNSScreenContainerView () <RCTInvalidating>
|
||||
|
||||
@property (nonatomic, retain) UIViewController *controller;
|
||||
@property (nonatomic, retain) NSMutableSet<RNSScreenView *> *activeScreens;
|
||||
@@ -68,6 +68,11 @@
|
||||
return _reactSubviews;
|
||||
}
|
||||
|
||||
- (UIViewController *)reactViewController
|
||||
{
|
||||
return _controller;
|
||||
}
|
||||
|
||||
- (void)detachScreen:(RNSScreenView *)screen
|
||||
{
|
||||
[screen.controller willMoveToParentViewController:nil];
|
||||
@@ -79,6 +84,9 @@
|
||||
- (void)attachScreen:(RNSScreenView *)screen
|
||||
{
|
||||
[_controller addChildViewController:screen.controller];
|
||||
// the frame is already set at this moment because we adjust it in insertReactSubview. We don't
|
||||
// want to update it here as react-driven animations may already run and hence changing the frame
|
||||
// would result in visual glitches
|
||||
[_controller.view addSubview:screen.controller.view];
|
||||
[screen.controller didMoveToParentViewController:_controller];
|
||||
[_activeScreens addObject:screen];
|
||||
@@ -155,10 +163,22 @@
|
||||
[self markChildUpdated];
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
if (self.window) {
|
||||
[self reactAddControllerToClosestParent:_controller];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
[_controller willMoveToParentViewController:nil];
|
||||
[_controller removeFromParentViewController];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[self reactAddControllerToClosestParent:_controller];
|
||||
_controller.view.frame = self.bounds;
|
||||
for (RNSScreenView *subview in _reactSubviews) {
|
||||
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate, RCTInvalidating>
|
||||
|
||||
@property (nonatomic, copy) RCTDirectEventBlock onFinishTransitioning;
|
||||
|
||||
- (void)markChildUpdated;
|
||||
- (void)didUpdateChildren;
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
#import <React/RCTRootContentView.h>
|
||||
#import <React/RCTTouchHandler.h>
|
||||
|
||||
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
|
||||
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate, UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic) NSMutableArray<UIViewController *> *presentedModals;
|
||||
@property (nonatomic) BOOL updatingModals;
|
||||
@property (nonatomic) BOOL scheduleModalsUpdate;
|
||||
|
||||
@end
|
||||
|
||||
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>
|
||||
@@ -17,12 +22,9 @@
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackView {
|
||||
BOOL _needUpdate;
|
||||
UINavigationController *_controller;
|
||||
NSMutableArray<RNSScreenView *> *_reactSubviews;
|
||||
NSMutableSet<RNSScreenView *> *_dismissedScreens;
|
||||
NSMutableArray<UIViewController *> *_presentedModals;
|
||||
__weak UIViewController* recentPopped;
|
||||
__weak RNSScreenStackManager *_manager;
|
||||
}
|
||||
|
||||
@@ -35,8 +37,6 @@
|
||||
_dismissedScreens = [NSMutableSet new];
|
||||
_controller = [[UINavigationController alloc] init];
|
||||
_controller.delegate = self;
|
||||
_needUpdate = NO;
|
||||
[self addSubview:_controller.view];
|
||||
_controller.interactivePopGestureRecognizer.delegate = self;
|
||||
|
||||
// we have to initialize viewControllers with a non empty array for
|
||||
@@ -48,6 +48,11 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIViewController *)reactViewController
|
||||
{
|
||||
return _controller;
|
||||
}
|
||||
|
||||
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
UIView *view = viewController.view;
|
||||
@@ -64,15 +69,36 @@
|
||||
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
for (NSUInteger i = _reactSubviews.count; i > 0; i--) {
|
||||
if ([viewController isEqual:[_reactSubviews objectAtIndex:i - 1].controller]) {
|
||||
RNSScreenView *screenView = [_reactSubviews objectAtIndex:i - 1];
|
||||
if ([viewController isEqual:screenView.controller]) {
|
||||
break;
|
||||
} else {
|
||||
[_dismissedScreens addObject:[_reactSubviews objectAtIndex:i - 1]];
|
||||
} else if (screenView.stackPresentation == RNSScreenStackPresentationPush) {
|
||||
[_dismissedScreens addObject:screenView];
|
||||
}
|
||||
}
|
||||
if (recentPopped != nil) {
|
||||
recentPopped.view = nil;
|
||||
recentPopped = nil;
|
||||
if (self.onFinishTransitioning) {
|
||||
self.onFinishTransitioning(nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
|
||||
{
|
||||
// We don't directly set presentation delegate but instead rely on the ScreenView's delegate to
|
||||
// forward certain calls to the container (Stack).
|
||||
UIView *screenView = presentationController.presentedViewController.view;
|
||||
if ([screenView isKindOfClass:[RNSScreenView class]]) {
|
||||
[_dismissedScreens addObject:(RNSScreenView *)screenView];
|
||||
[_presentedModals removeObject:presentationController.presentedViewController];
|
||||
if (self.onFinishTransitioning) {
|
||||
// instead of directly triggering onFinishTransitioning this time we enqueue the event on the
|
||||
// main queue. We do that because onDismiss event is also enqueued and we want for the transition
|
||||
// finish event to arrive later than onDismiss (see RNSScreen#notifyDismiss)
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.onFinishTransitioning) {
|
||||
self.onFinishTransitioning(nil);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +109,6 @@
|
||||
screen = (RNSScreenView *) toVC.view;
|
||||
} else if (operation == UINavigationControllerOperationPop) {
|
||||
screen = (RNSScreenView *) fromVC.view;
|
||||
recentPopped = fromVC;
|
||||
}
|
||||
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
|
||||
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
|
||||
@@ -123,11 +148,13 @@
|
||||
RCTLogError(@"ScreenStack only accepts children of type Screen");
|
||||
return;
|
||||
}
|
||||
subview.reactSuperview = self;
|
||||
[_reactSubviews insertObject:subview atIndex:atIndex];
|
||||
}
|
||||
|
||||
- (void)removeReactSubview:(RNSScreenView *)subview
|
||||
{
|
||||
subview.reactSuperview = nil;
|
||||
[_reactSubviews removeObject:subview];
|
||||
[_dismissedScreens removeObject:subview];
|
||||
}
|
||||
@@ -139,8 +166,48 @@
|
||||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
// do nothing
|
||||
[self updateContainer];
|
||||
// we need to wait until children have their layout set. At this point they don't have the layout
|
||||
// set yet, however the layout call is already enqueued on ui thread. Enqueuing update call on the
|
||||
// ui queue will guarantee that the update will run after layout.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateContainer];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didMoveToWindow
|
||||
{
|
||||
[super didMoveToWindow];
|
||||
if (self.window) {
|
||||
// when stack is attached to a window we do two things:
|
||||
// 1) we run updateContainer – we do this because we want push view controllers to be installed
|
||||
// before the VC is mounted. If we do that after it is added to parent the push updates operations
|
||||
// are going to be blocked by UIKit.
|
||||
// 2) we add navigation VS to parent – this is needed for the VC lifecycle events to be dispatched
|
||||
// properly
|
||||
// 3) we again call updateContainer – this time we do this to open modal controllers. Modals
|
||||
// won't open in (1) because they require navigator to be added to parent. We handle that case
|
||||
// gracefully in setModalViewControllers and can retry opening at any point.
|
||||
[self updateContainer];
|
||||
[self reactAddControllerToClosestParent:_controller];
|
||||
[self updateContainer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
|
||||
{
|
||||
if (!controller.parentViewController) {
|
||||
UIView *parentView = (UIView *)self.reactSuperview;
|
||||
while (parentView) {
|
||||
if (parentView.reactViewController) {
|
||||
[parentView.reactViewController addChildViewController:controller];
|
||||
[self addSubview:controller.view];
|
||||
[controller didMoveToParentViewController:parentView.reactViewController];
|
||||
break;
|
||||
}
|
||||
parentView = (UIView *)parentView.reactSuperview;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
|
||||
@@ -176,14 +243,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
// prevent re-entry
|
||||
if (_updatingModals) {
|
||||
_scheduleModalsUpdate = YES;
|
||||
return;
|
||||
}
|
||||
_updatingModals = YES;
|
||||
|
||||
__weak RNSScreenStackView *weakSelf = self;
|
||||
|
||||
void (^afterTransitions)(void) = ^{
|
||||
if (weakSelf.onFinishTransitioning) {
|
||||
weakSelf.onFinishTransitioning(nil);
|
||||
}
|
||||
weakSelf.updatingModals = NO;
|
||||
if (weakSelf.scheduleModalsUpdate) {
|
||||
// if modals update was requested during setModalViewControllers we set scheduleModalsUpdate
|
||||
// flag in order to perform updates at a later point. Here we are done with all modals
|
||||
// transitions and check this flag again. If it was set, we reset the flag and execute updates.
|
||||
weakSelf.scheduleModalsUpdate = NO;
|
||||
[weakSelf updateContainer];
|
||||
}
|
||||
};
|
||||
|
||||
void (^finish)(void) = ^{
|
||||
UIViewController *previous = changeRootController;
|
||||
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
|
||||
UIViewController *next = controllers[i];
|
||||
[previous presentViewController:next
|
||||
animated:(i == controllers.count - 1)
|
||||
completion:nil];
|
||||
previous = next;
|
||||
NSUInteger oldCount = weakSelf.presentedModals.count;
|
||||
if (changeRootIndex < oldCount) {
|
||||
[weakSelf.presentedModals
|
||||
removeObjectsInRange:NSMakeRange(changeRootIndex, oldCount - changeRootIndex)];
|
||||
}
|
||||
BOOL isAttached = changeRootController.parentViewController != nil || changeRootController.presentingViewController != nil;
|
||||
if (!isAttached || changeRootIndex >= controllers.count) {
|
||||
// if change controller view is not attached, presenting modals will silently fail on iOS.
|
||||
// In such a case we trigger controllers update from didMoveToWindow.
|
||||
// We also don't run any present transitions if changeRootIndex is greater or equal to the size
|
||||
// of new controllers array. This means that no new controllers should be presented.
|
||||
afterTransitions();
|
||||
return;
|
||||
} else {
|
||||
UIViewController *previous = changeRootController;
|
||||
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
|
||||
UIViewController *next = controllers[i];
|
||||
BOOL lastModal = (i == controllers.count - 1);
|
||||
[previous presentViewController:next
|
||||
animated:lastModal
|
||||
completion:^{
|
||||
[weakSelf.presentedModals addObject:next];
|
||||
if (lastModal) {
|
||||
afterTransitions();
|
||||
};
|
||||
}];
|
||||
previous = next;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,7 +305,6 @@
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
[_presentedModals setArray:controllers];
|
||||
}
|
||||
|
||||
- (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
|
||||
@@ -204,6 +314,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// if view controller is not yet attached to window we skip updates now and run them when view
|
||||
// is attached
|
||||
if (self.window == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *top = controllers.lastObject;
|
||||
UIViewController *lastTop = _controller.viewControllers.lastObject;
|
||||
|
||||
@@ -213,7 +329,7 @@
|
||||
// controller is still there
|
||||
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
|
||||
|
||||
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone && !_controller.presentedViewController;
|
||||
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
|
||||
|
||||
if (firstTimePush) {
|
||||
// nothing pushed yet
|
||||
@@ -251,7 +367,7 @@
|
||||
NSMutableArray<UIViewController *> *pushControllers = [NSMutableArray new];
|
||||
NSMutableArray<UIViewController *> *modalControllers = [NSMutableArray new];
|
||||
for (RNSScreenView *screen in _reactSubviews) {
|
||||
if (![_dismissedScreens containsObject:screen]) {
|
||||
if (![_dismissedScreens containsObject:screen] && screen.controller != nil) {
|
||||
if (pushControllers.count == 0) {
|
||||
// first screen on the list needs to be places as "push controller"
|
||||
[pushControllers addObject:screen.controller];
|
||||
@@ -272,7 +388,6 @@
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
[self reactAddControllerToClosestParent:_controller];
|
||||
_controller.view.frame = self.bounds;
|
||||
}
|
||||
|
||||
@@ -282,6 +397,8 @@
|
||||
[controller dismissViewControllerAnimated:NO completion:nil];
|
||||
}
|
||||
[_presentedModals removeAllObjects];
|
||||
[_controller willMoveToParentViewController:nil];
|
||||
[_controller removeFromParentViewController];
|
||||
}
|
||||
|
||||
- (void)dismissOnReload
|
||||
@@ -299,8 +416,7 @@
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(transitioning, NSInteger)
|
||||
RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(onFinishTransitioning, RCTDirectEventBlock);
|
||||
|
||||
- (UIView *)view
|
||||
{
|
||||
|
||||
@@ -118,9 +118,12 @@
|
||||
UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar;
|
||||
[navbar setTintColor:config.color];
|
||||
|
||||
#ifdef __IPHONE_13_0
|
||||
if (@available(iOS 13.0, *)) {
|
||||
// font customized on the navigation item level, so nothing to do here
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
BOOL hideShadow = config.hideShadow;
|
||||
|
||||
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
|
||||
@@ -165,15 +168,6 @@
|
||||
[navbar setLargeTitleTextAttributes:largeAttrs];
|
||||
}
|
||||
}
|
||||
|
||||
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
|
||||
if (backButtonImage) {
|
||||
navbar.backIndicatorImage = backButtonImage;
|
||||
navbar.backIndicatorTransitionMaskImage = backButtonImage;
|
||||
} else if (navbar.backIndicatorImage) {
|
||||
navbar.backIndicatorImage = nil;
|
||||
navbar.backIndicatorTransitionMaskImage = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +190,20 @@
|
||||
if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) {
|
||||
hasBackButtonImage = YES;
|
||||
RCTImageView *imageView = subview.subviews[0];
|
||||
if (imageView.image == nil) {
|
||||
// This is yet another workaround for loading custom back icon. It turns out that under
|
||||
// certain circumstances image attribute can be null despite the app running in production
|
||||
// mode (when images are loaded from the filesystem). This can happen because image attribute
|
||||
// is reset when image view is detached from window, and also in some cases initialization
|
||||
// does not populate the frame of the image view before the loading start. The latter result
|
||||
// in the image attribute not being updated. We manually set frame to the size of an image
|
||||
// in order to trigger proper reload that'd update the image attribute.
|
||||
RCTImageSource *source = imageView.imageSources[0];
|
||||
[imageView reactSetFrame:CGRectMake(imageView.frame.origin.x,
|
||||
imageView.frame.origin.y,
|
||||
source.size.width,
|
||||
source.size.height)];
|
||||
}
|
||||
UIImage *image = imageView.image;
|
||||
// IMPORTANT!!!
|
||||
// image can be nil in DEV MODE ONLY
|
||||
@@ -267,11 +275,7 @@
|
||||
}
|
||||
|
||||
[navctr setNavigationBarHidden:shouldHide animated:YES];
|
||||
#ifdef __IPHONE_13_0
|
||||
if (@available(iOS 13.0, *)) {
|
||||
vc.modalInPresentation = !config.screenView.gestureEnabled;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (shouldHide) {
|
||||
return;
|
||||
}
|
||||
@@ -375,8 +379,20 @@
|
||||
navitem.standardAppearance = appearance;
|
||||
navitem.compactAppearance = appearance;
|
||||
navitem.scrollEdgeAppearance = appearance;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// updating backIndicatotImage does not work when called during transition. On iOS pre 13 we need
|
||||
// to update it before the navigation starts.
|
||||
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
|
||||
if (backButtonImage) {
|
||||
navctr.navigationBar.backIndicatorImage = backButtonImage;
|
||||
navctr.navigationBar.backIndicatorTransitionMaskImage = backButtonImage;
|
||||
} else if (navctr.navigationBar.backIndicatorImage) {
|
||||
navctr.navigationBar.backIndicatorImage = nil;
|
||||
navctr.navigationBar.backIndicatorTransitionMaskImage = nil;
|
||||
}
|
||||
}
|
||||
navitem.hidesBackButton = config.hideBackButton;
|
||||
navitem.leftBarButtonItem = nil;
|
||||
navitem.rightBarButtonItem = nil;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-native-screens",
|
||||
"version": "2.0.0-beta.2",
|
||||
"description": "First incomplete navigation solution for your react-native app.",
|
||||
"version": "2.0.0-beta.13",
|
||||
"description": "Native navigation primitives for your React Native app.",
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"test": "npm run format && npm run lint && npm run test:unit",
|
||||
|
||||
2
src/screens.d.ts
vendored
2
src/screens.d.ts
vendored
@@ -15,7 +15,7 @@ declare module 'react-native-screens' {
|
||||
export function enableScreens(shouldEnableScreens?: boolean): void;
|
||||
export function screensEnabled(): boolean;
|
||||
|
||||
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal';
|
||||
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal' | 'fullScreenModal' | 'formSheet';
|
||||
export type StackAnimationTypes = 'default' | 'fade' | 'flip' | 'none';
|
||||
|
||||
export interface ScreenProps extends ViewProps {
|
||||
|
||||
Reference in New Issue
Block a user