mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-26 09:14:22 +08:00
Compare commits
48 Commits
2.0.0-alph
...
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 | ||
|
|
fd7fcfbc5b | ||
|
|
06b928f5c1 | ||
|
|
acb038e213 | ||
|
|
22d4400b93 | ||
|
|
f78c264419 | ||
|
|
65bd436cbe | ||
|
|
a0745762ba | ||
|
|
89658d9361 | ||
|
|
074aabc33e | ||
|
|
eb2a1ae8fd | ||
|
|
9721353e7b | ||
|
|
523b3d8f3a |
@@ -219,7 +219,7 @@ PODS:
|
||||
- ReactCommon/jscallinvoker (= 0.61.2)
|
||||
- RNGestureHandler (1.3.0):
|
||||
- React
|
||||
- RNScreens (2.0.0-alpha.3):
|
||||
- RNScreens (2.0.0-alpha.23):
|
||||
- React
|
||||
- Yoga (1.14.0)
|
||||
|
||||
@@ -256,7 +256,7 @@ DEPENDENCIES:
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
https://github.com/CocoaPods/Specs.git:
|
||||
- boost-for-react-native
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -342,9 +342,9 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: fb54c732fd20405a76598e431aa2f8c2bf527de9
|
||||
ReactCommon: 5848032ed2f274fcb40f6b9ec24067787c42d479
|
||||
RNGestureHandler: 5329a942fce3d41c68b84c2c2276ce06a696d8b0
|
||||
RNScreens: a32a406ec4884f0ba383ef835332e8379f75e3da
|
||||
RNScreens: 55c735f525774e894be67848c250c95a9c3194c0
|
||||
Yoga: 14927e37bd25376d216b150ab2a561773d57911f
|
||||
|
||||
PODFILE CHECKSUM: 1a141b811c7076eb11c48f2e22336181f52531b5
|
||||
|
||||
COCOAPODS: 1.7.3
|
||||
COCOAPODS: 1.8.4
|
||||
|
||||
@@ -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.
|
||||
@@ -155,9 +155,8 @@ Defines how the method that should be used to present the given screen. It is a
|
||||
The config component is expected to be rendered as a direct children of `<Screen>`. It provides an ability to configure native navigation header that gets rendered as a part of native screen stack. The component acts as a "virtual" element that is not directly rendered under `Screen`. You can use its properties to customize platform native header for the parent screen and also render react-native components that you'd like to be displayed inside the header (e.g. in the title are or on the side).
|
||||
|
||||
Along with this component properties that can be used to customize header behavior one can also use one or the below component containers to render custom react-native content in different areas of the native header:
|
||||
- `ScreenStackHeaderTitleView` – react native views rendered as children will appear on the navigation bar in the place of title. Note that title is aligned next to back button on Android while it is centered on iOS.
|
||||
- `ScreenStackHeaderCenterView` – the childern will render in the center of the native navigation bar (on iOS this has the same behavior as `ScreenStackHeaderTitleView` container)
|
||||
- `ScreenStackHeaderRightView` – the children will render on the right hand side of the navigation bar (or on the left hand side in case LTR locales are set on the user's device)
|
||||
- `ScreenStackHeaderCenterView` – the childern will render in the center of the native navigation bar.
|
||||
- `ScreenStackHeaderRightView` – the children will render on the right hand side of the navigation bar (or on the left hand side in case LTR locales are set on the user's device).
|
||||
- `ScreenStackHeaderLeftView` – the children will render on the left hand side of the navigation bar (or on the right hand side in case LTR locales are set on the user's device).
|
||||
|
||||
Below is a list of properties that can be set with `ScreenStackHeaderConfig` component:
|
||||
|
||||
@@ -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,22 +1,43 @@
|
||||
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;
|
||||
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 {
|
||||
|
||||
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;
|
||||
@@ -61,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());
|
||||
@@ -85,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,
|
||||
@@ -93,10 +138,39 @@ public class ScreenStackFragment extends ScreenFragment {
|
||||
mScreenRootView = configureView();
|
||||
}
|
||||
|
||||
return mScreenRootView;
|
||||
return recycleView(mScreenRootView);
|
||||
}
|
||||
|
||||
public boolean isDismissable() {
|
||||
return mScreenView.isGestureEnabled();
|
||||
}
|
||||
|
||||
public boolean canNavigateBack() {
|
||||
ScreenContainer container = mScreenView.getContainer();
|
||||
if (container instanceof ScreenStack) {
|
||||
if (((ScreenStack) container).getRootScreen() == getScreen()) {
|
||||
// this screen is the root of the container, if it is nested we can check parent container
|
||||
// if it is also a root or not
|
||||
Fragment parentFragment = getParentFragment();
|
||||
if (parentFragment instanceof ScreenStackFragment) {
|
||||
return ((ScreenStackFragment) parentFragment).canNavigateBack();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("ScreenStackFragment added into a non-stack container");
|
||||
}
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
ScreenContainer container = mScreenView.getContainer();
|
||||
if (container instanceof ScreenStack) {
|
||||
((ScreenStack) container).dismiss(this);
|
||||
} else {
|
||||
throw new IllegalStateException("ScreenStackFragment added into a non-stack container");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,16 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
private OnClickListener mBackClickListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
getScreenStack().dismiss(getScreenFragment());
|
||||
ScreenStack stack = getScreenStack();
|
||||
ScreenStackFragment fragment = getScreenFragment();
|
||||
if (stack.getRootScreen() == fragment.getScreen()) {
|
||||
Fragment parentFragment = fragment.getParentFragment();
|
||||
if (parentFragment instanceof ScreenStackFragment) {
|
||||
((ScreenStackFragment) parentFragment).dismiss();
|
||||
}
|
||||
} else {
|
||||
fragment.dismiss();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,13 +126,17 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
public void onUpdate() {
|
||||
Screen parent = (Screen) getParent();
|
||||
final ScreenStack stack = getScreenStack();
|
||||
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
|
||||
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
|
||||
|
||||
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mIsHidden) {
|
||||
if (mToolbar.getParent() != null) {
|
||||
getScreenFragment().removeToolbar();
|
||||
@@ -135,12 +148,11 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
getScreenFragment().setToolbar(mToolbar);
|
||||
}
|
||||
|
||||
AppCompatActivity activity = (AppCompatActivity) getScreenFragment().getActivity();
|
||||
activity.setSupportActionBar(mToolbar);
|
||||
ActionBar actionBar = activity.getSupportActionBar();
|
||||
|
||||
// hide back button
|
||||
actionBar.setDisplayHomeAsUpEnabled(isRoot ? false : !mIsBackButtonHidden);
|
||||
actionBar.setDisplayHomeAsUpEnabled(getScreenFragment().canNavigateBack() ? !mIsBackButtonHidden : false);
|
||||
|
||||
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
|
||||
// navigation click listener. The default behavior set in the wrapper is to call into
|
||||
@@ -215,11 +227,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
|
||||
case RIGHT:
|
||||
params.gravity = Gravity.RIGHT;
|
||||
break;
|
||||
case TITLE:
|
||||
params.width = LayoutParams.MATCH_PARENT;
|
||||
mToolbar.setTitle(null);
|
||||
case CENTER:
|
||||
params.width = LayoutParams.MATCH_PARENT;
|
||||
params.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
mToolbar.setTitle(null);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,14 @@ import com.facebook.react.views.view.ReactViewGroup;
|
||||
|
||||
public class ScreenStackHeaderSubview extends ReactViewGroup {
|
||||
|
||||
public class Measurements {
|
||||
public int width;
|
||||
public int height;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
LEFT,
|
||||
CENTER,
|
||||
TITLE,
|
||||
RIGHT,
|
||||
BACK
|
||||
}
|
||||
|
||||
private int mReactWidth, mReactHeight;
|
||||
private final UIManagerModule mUIManager;
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
@@ -43,26 +36,13 @@ public class ScreenStackHeaderSubview extends ReactViewGroup {
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
if (changed && (mType == Type.CENTER)) {
|
||||
// if we want the view to be centered we need to account for the fact that right and left
|
||||
// paddings may not be equal.
|
||||
Measurements measurements = new Measurements();
|
||||
View parent = (View) getParent();
|
||||
int parentWidth = parent.getWidth();
|
||||
int rightPadding = parentWidth - right;
|
||||
int leftPadding = left;
|
||||
measurements.width = Math.max(0, parentWidth - 2 * Math.max(rightPadding, leftPadding));
|
||||
measurements.height = bottom - top;
|
||||
mUIManager.setViewLocalData(getId(), measurements);
|
||||
}
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
// no-op
|
||||
}
|
||||
|
||||
private Type mType = Type.RIGHT;
|
||||
|
||||
public ScreenStackHeaderSubview(ReactContext context) {
|
||||
super(context);
|
||||
mUIManager = context.getNativeModule(UIManagerModule.class);
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.swmansion.rnscreens;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.facebook.react.uimanager.LayoutShadowNode;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.views.view.ReactViewGroup;
|
||||
@@ -11,15 +9,6 @@ import com.facebook.react.views.view.ReactViewManager;
|
||||
@ReactModule(name = ScreenStackHeaderSubviewManager.REACT_CLASS)
|
||||
public class ScreenStackHeaderSubviewManager extends ReactViewManager {
|
||||
|
||||
private static class SubviewShadowNode extends LayoutShadowNode {
|
||||
@Override
|
||||
public void setLocalData(Object data) {
|
||||
ScreenStackHeaderSubview.Measurements measurements = (ScreenStackHeaderSubview.Measurements) data;
|
||||
setStyleWidth(measurements.width);
|
||||
setStyleHeight(measurements.height);
|
||||
}
|
||||
}
|
||||
|
||||
protected static final String REACT_CLASS = "RNSScreenStackHeaderSubview";
|
||||
|
||||
@Override
|
||||
@@ -32,19 +21,12 @@ public class ScreenStackHeaderSubviewManager extends ReactViewManager {
|
||||
return new ScreenStackHeaderSubview(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LayoutShadowNode createShadowNodeInstance(ReactApplicationContext context) {
|
||||
return new SubviewShadowNode();
|
||||
}
|
||||
|
||||
@ReactProp(name = "type")
|
||||
public void setType(ScreenStackHeaderSubview view, String type) {
|
||||
if ("left".equals(type)) {
|
||||
view.setType(ScreenStackHeaderSubview.Type.LEFT);
|
||||
} else if ("center".equals(type)) {
|
||||
view.setType(ScreenStackHeaderSubview.Type.CENTER);
|
||||
} else if ("title".equals(type)) {
|
||||
view.setType(ScreenStackHeaderSubview.Type.TITLE);
|
||||
} else if ("right".equals(type)) {
|
||||
view.setType(ScreenStackHeaderSubview.Type.RIGHT);
|
||||
} else if ("back".equals(type)) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ScreenStackHeaderBackButtonImage,
|
||||
ScreenStackHeaderLeftView,
|
||||
ScreenStackHeaderRightView,
|
||||
ScreenStackHeaderTitleView,
|
||||
ScreenStackHeaderCenterView,
|
||||
} from 'react-native-screens';
|
||||
|
||||
function renderComponentOrThunk(componentOrThunk, props) {
|
||||
@@ -30,13 +30,31 @@ class StackView extends React.Component {
|
||||
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
|
||||
};
|
||||
|
||||
_onSceneFocus = (route, descriptor) => {
|
||||
descriptor.options && descriptor.options.onAppear && descriptor.options.onAppear()
|
||||
_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;
|
||||
@@ -50,7 +68,6 @@ class StackView extends React.Component {
|
||||
headerBackTitle,
|
||||
headerBackTitleVisible,
|
||||
headerTintColor,
|
||||
gestureEnabled,
|
||||
largeTitle,
|
||||
headerLargeTitleStyle,
|
||||
translucent,
|
||||
@@ -140,9 +157,9 @@ class StackView extends React.Component {
|
||||
headerOptions.title = options.headerTitle;
|
||||
} else {
|
||||
children.push(
|
||||
<ScreenStackHeaderTitleView key="title">
|
||||
<ScreenStackHeaderCenterView key="center">
|
||||
{renderComponentOrThunk(options.headerTitle, { scene })}
|
||||
</ScreenStackHeaderTitleView>
|
||||
</ScreenStackHeaderCenterView>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -187,11 +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={gestureEnabled === undefined ? true : gestureEnabled}
|
||||
onAppear={() => this._onSceneFocus(route, descriptor)}
|
||||
gestureEnabled={
|
||||
options.gestureEnabled === undefined ? true : options.gestureEnabled
|
||||
}
|
||||
onAppear={() => this._onAppear(route, descriptor)}
|
||||
onDismissed={() => this._removeScene(route)}>
|
||||
{this._renderHeaderConfig(index, route, descriptor)}
|
||||
<SceneView
|
||||
@@ -207,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
|
||||
{
|
||||
|
||||
@@ -20,34 +20,26 @@
|
||||
@end
|
||||
|
||||
|
||||
@interface RNSScreenHeaderItemMeasurements : NSObject
|
||||
@property (nonatomic, readonly) CGSize headerSize;
|
||||
@property (nonatomic, readonly) CGFloat leftPadding;
|
||||
@property (nonatomic, readonly) CGFloat rightPadding;
|
||||
|
||||
- (instancetype)initWithHeaderSize:(CGSize)headerSize leftPadding:(CGFloat)leftPadding rightPadding:(CGFloat)rightPadding;
|
||||
@end
|
||||
|
||||
@implementation RNSScreenHeaderItemMeasurements
|
||||
|
||||
- (instancetype)initWithHeaderSize:(CGSize)headerSize leftPadding:(CGFloat)leftPadding rightPadding:(CGFloat)rightPadding
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_headerSize = headerSize;
|
||||
_leftPadding = leftPadding;
|
||||
_rightPadding = rightPadding;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface RNSScreenStackHeaderSubview : UIView
|
||||
|
||||
@property (nonatomic, weak) RCTBridge *bridge;
|
||||
@property (nonatomic, weak) UIView *reactSuperview;
|
||||
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge*)bridge;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackHeaderSubview
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_bridge = bridge;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackHeaderConfig {
|
||||
@@ -126,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.) {
|
||||
@@ -173,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,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
|
||||
@@ -275,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;
|
||||
}
|
||||
@@ -383,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;
|
||||
@@ -403,17 +411,22 @@
|
||||
}
|
||||
case RNSScreenStackHeaderSubviewTypeCenter:
|
||||
case RNSScreenStackHeaderSubviewTypeTitle: {
|
||||
subview.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
navitem.titleView = subview;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vc.transitionCoordinator != nil && !wasHidden) {
|
||||
[vc.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||
|
||||
} completion:nil];
|
||||
if (vc.transitionCoordinator != nil
|
||||
&& vc.transitionCoordinator.presentationStyle == UIModalPresentationNone
|
||||
&& !wasHidden) {
|
||||
// when there is an ongoing transition we may need to update navbar setting in animation block
|
||||
// using animateAlongsideTransition. However, we only do that given the transition is not a modal
|
||||
// transition (presentationStyle == UIModalPresentationNone) and that the bar was not previously
|
||||
// hidden. This is because both for modal transitions and transitions from screen with hidden bar
|
||||
// the transition animation block does not get triggered. This is ok, because with both of those
|
||||
// types of transitions there is no "shared" navigation bar that needs to be updated in an animated
|
||||
// way.
|
||||
[vc.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||
[self setAnimatedConfig:vc withConfig:config];
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
|
||||
@@ -477,67 +490,6 @@ RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackHeaderSubview
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_bridge = bridge;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
if (!self.translatesAutoresizingMaskIntoConstraints) {
|
||||
CGSize size = self.superview.frame.size;
|
||||
CGFloat right = size.width - self.frame.size.width - self.frame.origin.x;
|
||||
CGFloat left = self.frame.origin.x;
|
||||
[_bridge.uiManager
|
||||
setLocalData:[[RNSScreenHeaderItemMeasurements alloc]
|
||||
initWithHeaderSize:size
|
||||
leftPadding:left rightPadding:right]
|
||||
forView:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
if (self.translatesAutoresizingMaskIntoConstraints) {
|
||||
[super reactSetFrame:frame];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize
|
||||
{
|
||||
return UILayoutFittingExpandedSize;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface RNSScreenStackHeaderSubviewShadow : RCTShadowView
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackHeaderSubviewShadow
|
||||
|
||||
- (void)setLocalData:(RNSScreenHeaderItemMeasurements *)data
|
||||
{
|
||||
self.width = (YGValue){data.headerSize.width - data.leftPadding - data.rightPadding, YGUnitPoint};
|
||||
self.height = (YGValue){data.headerSize.height, YGUnitPoint};
|
||||
|
||||
if (data.leftPadding > data.rightPadding) {
|
||||
self.paddingLeft = (YGValue){0, YGUnitPoint};
|
||||
self.paddingRight = (YGValue){data.leftPadding - data.rightPadding, YGUnitPoint};
|
||||
} else {
|
||||
self.paddingLeft = (YGValue){data.rightPadding - data.leftPadding, YGUnitPoint};
|
||||
self.paddingRight = (YGValue){0, YGUnitPoint};
|
||||
}
|
||||
[self didSetProps:@[@"width", @"height", @"paddingLeft", @"paddingRight"]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RNSScreenStackHeaderSubviewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
@@ -549,9 +501,4 @@ RCT_EXPORT_VIEW_PROPERTY(type, RNSScreenStackHeaderSubviewType)
|
||||
return [[RNSScreenStackHeaderSubview alloc] initWithBridge:self.bridge];
|
||||
}
|
||||
|
||||
- (RCTShadowView *)shadowView
|
||||
{
|
||||
return [RNSScreenStackHeaderSubviewShadow new];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "react-native-screens",
|
||||
"version": "2.0.0-alpha.31",
|
||||
"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",
|
||||
|
||||
22
src/screens.d.ts
vendored
22
src/screens.d.ts
vendored
@@ -3,13 +3,19 @@
|
||||
|
||||
declare module 'react-native-screens' {
|
||||
import { ComponentClass } from 'react';
|
||||
import { ViewProps, Animated, NativeSyntheticEvent, NativeTouchEvent, ImageProps } from 'react-native';
|
||||
import {
|
||||
ViewProps,
|
||||
Animated,
|
||||
NativeSyntheticEvent,
|
||||
NativeTouchEvent,
|
||||
ImageProps,
|
||||
} from 'react-native';
|
||||
|
||||
export function useScreens(shouldUseScreens?: boolean): void;
|
||||
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 {
|
||||
@@ -35,11 +41,14 @@ declare module 'react-native-screens' {
|
||||
* @type "none" – the screen appears/dissapears without an animation
|
||||
*/
|
||||
stackAnimation?: StackAnimationTypes;
|
||||
/**
|
||||
* @description When set to false the back swipe gesture will be disabled when the parent Screen is on top of the stack. The default value is true.
|
||||
*/
|
||||
gestureEnabled?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export type ScreenContainerProps = ViewProps;
|
||||
|
||||
|
||||
export interface ScreenStackProps extends ViewProps {
|
||||
transitioning?: number;
|
||||
progress?: number;
|
||||
@@ -82,10 +91,6 @@ declare module 'react-native-screens' {
|
||||
* @description If set to true the back button will not be rendered as a part of navigation header.
|
||||
*/
|
||||
hideBackButton?: boolean;
|
||||
/**
|
||||
* @description When set to false the back swipe gesture will be disabled when the parent Screen is on top of the stack. The default value is true.
|
||||
*/
|
||||
gestureEnabled?: boolean;
|
||||
/**
|
||||
* @host (iOS only)
|
||||
* @description When set to true, it makes native navigation bar on iOS semi transparent with blur effect. It is a common way of presenting navigation bar introduced in iOS 11. The default value is false
|
||||
@@ -135,7 +140,6 @@ declare module 'react-native-screens' {
|
||||
export const ScreenStackHeaderBackButtonImage: ComponentClass<ImageProps>;
|
||||
export const ScreenStackHeaderLeftView: ComponentClass<ViewProps>;
|
||||
export const ScreenStackHeaderRightView: ComponentClass<ViewProps>;
|
||||
export const ScreenStackHeaderTitleView: ComponentClass<ViewProps>;
|
||||
export const ScreenStackHeaderConfig: ComponentClass<
|
||||
ScreenStackHeaderConfigProps
|
||||
>;
|
||||
|
||||
@@ -171,14 +171,6 @@ const ScreenStackHeaderLeftView = props => (
|
||||
/>
|
||||
);
|
||||
|
||||
const ScreenStackHeaderTitleView = props => (
|
||||
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
||||
{...props}
|
||||
type="title"
|
||||
style={styles.headerSubview}
|
||||
/>
|
||||
);
|
||||
|
||||
const ScreenStackHeaderCenterView = props => (
|
||||
<ScreensNativeModules.NativeScreenStackHeaderSubview
|
||||
{...props}
|
||||
@@ -210,7 +202,6 @@ module.exports = {
|
||||
ScreenStackHeaderBackButtonImage,
|
||||
ScreenStackHeaderRightView,
|
||||
ScreenStackHeaderLeftView,
|
||||
ScreenStackHeaderTitleView,
|
||||
ScreenStackHeaderCenterView,
|
||||
|
||||
enableScreens,
|
||||
|
||||
Reference in New Issue
Block a user