Compare commits

..

27 Commits

Author SHA1 Message Date
Krzysztof Magiera
124e8acb2d Bump version -> 2.0.0-alpha.12 2019-11-22 13:03:35 +01:00
Krzysztof Magiera
2c5f95cea6 Fix updating stack config props. (#231)
When some of config props change or new header items are added we need to perform a header update. This diff adds logic to trigger updating header props when that happens.
2019-11-22 13:03:11 +01:00
Janic Duplessis
0a2336d005 [native-stack] Add flip transition on iOS (#225) 2019-11-20 11:12:50 +01:00
Krzysztof Magiera
58d1791d4a Bump version -> 2.0.0-alpha.11 2019-11-14 09:16:31 +01:00
Krzysztof Magiera
3d56c5d4e2 Bump version -> 2.0.0-alpha.10 2019-11-14 09:16:05 +01:00
Krzysztof Magiera
47658d4d7d Fix issue with stack triggering back on reload on Android. (#223)
There was an issue with back stack listener being triggered after reload caused by the fact we weren't cleaning up stack manager's back stack on reload. As a result after reload listener would get triggered with and empty stack first and only then with a back stack with 1 item. Consequence of the first trigger was that we'd call dismiss and move back from the top screen which wasn't a desirable behavior.
2019-11-13 22:45:08 +01:00
Krzysztof Magiera
258ae419de Bump version -> 2.0.0-alpha.9 2019-11-13 19:47:57 +01:00
Krzysztof Magiera
62123f16f9 Fix screen layout with non-translucent header on iOS. (#222)
After #184 we no longer were acconting for the size of navigation bar when laying out screens on the stack. This was causing elements to be drawn under a non-translucent bar unless SafeAreaView's been used. As this seem not to be desirable in most of the cases (there is no way of seeing items rendered underneath non-translucent header) this change brings back the previous behavior. Instead of using manual method of calculating top inset as it's been done before we rely on edgesForExtendedLayout property of the view controller.
2019-11-13 19:47:37 +01:00
Krzysztof Magiera
a94424192b Fix iOS stack navigator restoring state. (#221)
When more than one screen is initially mounted we were only installing the top navigator with UINavController. This was cauing the back-handling logic to not work properly.
2019-11-13 18:30:30 +01:00
Krzysztof Magiera
752d6c0f04 Bump version -> 2.0.0-alpha.8 2019-11-06 22:04:14 +01:00
Krzysztof Magiera
a017713efc Fix screen container layout on Android. (#217)
This is a similar fix to the one already merged in #215 but for Android this time. We change default screen container to layout its direct children without using flexbox. This is to follow the same pattern as with native stack container.
2019-11-06 21:57:31 +01:00
Krzysztof Magiera
26384b625e Use fragment manager back stack to handle hw back button presses on Android. (#216)
Before this change we'd rely on new androidx OnBackPressedCallback mechanism. It turns out not to work well in a few cases. The biggest problem with it was that when registered it'd never allow for the hw back button press to fallback to a system default behaviour and instead would always "still" that event. After several attempts of trying to make it work I decided to revert back to a FragmentManager's default solution.

This change adds an ability to use FragmentManager's back stack API. There are also a few problems with it we need to workaround though. One of the problem is the fact that we can not modify back stack history. What we do because of that is that we try to keep at most one item on the back stack and reset it each time our native stack updates. In order for that to work we create a fake transaction that hides and shows the same screen that displays on top. Thanks to that when hw back is pressed the built in transaction rollback logic does nothing to the UI and allows us to handle back navigation using back stack change listener.
2019-11-06 21:54:19 +01:00
Krzysztof Magiera
5d5e8bfca6 Fix screen container after rnsscreen layout change. (#215)
For native stack we introduced a new way of layouting screen controllers. We no longer rely on flexbox to layout them so that we can use native stack to provide a correct dimensions and safe area paddings. Unfortunately this change broke the old screen container. With this commit we are adapting layout change to old screen container that will now layout its direct children on the native side by making them take up the whole space of the container.
2019-11-06 21:54:05 +01:00
Sunny Luo
2536837795 Fix compile error on xcode 10 (#212) 2019-11-05 14:05:23 +01:00
osdnk
20650a8ede Bump -> 2.0.0-alpha.7 2019-10-31 12:22:48 +01:00
Michał Osadnik
ee0dbfe8ae Revert "Fix deprecated items (#156)" (#209)
This reverts commit 5a9b3d1408.
2019-10-31 12:15:40 +01:00
Krzysztof Magiera
23cbc009d4 Bump version -> 2.0.0-alpha6 2019-10-25 14:19:24 +02:00
Krzysztof Magiera
f21a093918 Make it possible for iOS largeTitle to collapse while scrolling. (#202)
iOS navbar largeTitle setting allows the navbar to be automatically collapsed when scrolling. For that to work iOS expects scrollable component to be rendered in the screen view hierarchy as a first children (or just to be looked up on the ancestor path using first children when navigating down). On top of that in RN we should use contentInsetAdjustmentBehavior="automatic" for SV's contentInsets to adjust as expected when the header scrolls and to allow for the SV background to be present below the navigation bar.

This works pretty well w/o any changes. However when testing I disovered one issue with the navigation bar rendering in a collapsed state on the first render. After that you could've scroll down to reveal large title bar but the initial state would be collapsed. As on iOS it is expected for large title enabled screens to initially render in the revealed state a workaround was required. It turned out that the header rendered in collapsed state because at the moment when navigation controller is created is has an empty list of view controllers. The list is empty because react first creates nnavigation controller and only then adds children to it. It turned out the fix was to populate viewControllers with a single item at first, then when controllers list is replaced with the children provided from react the header renders properly in the revealed state.
2019-10-25 14:17:59 +02:00
Krzysztof Magiera
1d4712acbd iOS13 header customization updates. (#201)
This change makes it possible for header customization options to use appearence API introduces in iOS 13. Thanks to this the default header setting will respect header translucency. Thanks to the appearence API we no longer need to set so many properties in setAnimatedConfig.
2019-10-25 14:06:55 +02:00
Janic Duplessis
09c71a45a2 Remove es exports from screens.native.js (#193)
Follow up of 4749405d64, let's remove the es exports from this file to avoid confusion. It uses `module.export`.
2019-10-23 23:01:50 +02:00
Janic Duplessis
79e664f11d Fix iOS version runtime checks in RNSScreenStackHeaderConfig (#195)
- `@available` should not be used with other conditions (not sure if it breaks anything but it does trigger warnings in xcode)
- missing runtime check before using `modalInPresentation`, kept the precompiler instruction so it still builds on older xcodes.
2019-10-23 23:01:29 +02:00
Ferran Negre
b622abc935 fix: typo pops = props (#197) 2019-10-23 23:00:23 +02:00
Krzysztof Magiera
7d4bbb8f88 Some compatibility adjustments for react-navigation bindings (#196) 2019-10-23 16:04:50 +02:00
Krzysztof Magiera
7c304a007f Expose 'hideShadow' option in react naviation bindings (#192) 2019-10-22 23:57:39 +02:00
Krzysztof Magiera
adf3333462 Avoid calling deprecated cancel method on a root view (#191) 2019-10-22 23:41:35 +02:00
Krzysztof Magiera
d4636d3130 Android native stack bugfixes. (#190)
A few bugs fixed with android native stack in this commit:
 - fixed a problem with views not resizing when keyboard appears, the bug was due to onMeasure forwardin getHeight which was the old height of the container instead of the changed one
 - fixed a problem with back button behavior not being consistent. Now back button is controlled by 'gestureEnabled' to emulate iOS behavior where there is no hw back button but you can swipe back instead.
 - added compatibility for "contained" modal styles - for now we always render "containted" fragments on Android, plan to add a way to render in dialogs in the future
2019-10-22 23:22:48 +02:00
Krzysztof Magiera
4749405d64 Fix useScreens -> enableScreens renaming (#189)
There was a bug in PR that introduces enableScreens method to replace useScreens. The bug was that the method did not end up being exported (we use module.exports and not export syntax). On top of that I'm adding a deprecation warning to useScreens method as it interferes with some react hooks tooling.
2019-10-22 23:17:24 +02:00
20 changed files with 411 additions and 174 deletions

View File

@@ -140,6 +140,7 @@ A callback that gets called when the current screen is dismissed by hardware bac
Allows for the customization of how the given screen should appear/dissapear when pushed or popped at the top of the stack. The followin values are currently supported:
- `"default"` uses a platform default animation
- `"fade"` fades screen in or out
- `"flip"` flips the screen, requires `stackPresentation: "modal"` (iOS only)
- `"none"` the screen appears/dissapears without an animation
#### `stackPresentation`

View File

@@ -91,12 +91,12 @@ afterEvaluate { project ->
}
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
archiveClassifier.set('javadoc')
classifier = 'javadoc'
from androidJavadoc.destinationDir
}
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
include '**/*.java'
}

View File

@@ -17,5 +17,6 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -2,7 +2,6 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.Paint;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
@@ -13,7 +12,6 @@ import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.MeasureSpecAssertions;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;

View File

@@ -7,6 +7,7 @@ import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.facebook.react.ReactRootView;
@@ -25,21 +26,45 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private @Nullable FragmentTransaction mCurrentTransaction;
private boolean mNeedUpdate;
private boolean mIsAttached;
private boolean mLayoutEnqueued = false;
private ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
updateIfNeeded();
}
};
private final Runnable mLayoutRunnable = new Runnable() {
@Override
public void run() {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
public ScreenContainer(Context context) {
super(context);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
// no-op
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).layout(0, 0, getWidth(), getHeight());
}
}
@Override
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
}
}
protected void markUpdated() {
@@ -109,9 +134,13 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return (FragmentActivity) context;
}
protected FragmentManager getFragmentManager() {
return findRootFragmentActivity().getSupportFragmentManager();
}
protected FragmentTransaction getOrCreateTransaction() {
if (mCurrentTransaction == null) {
mCurrentTransaction = findRootFragmentActivity().getSupportFragmentManager().beginTransaction();
mCurrentTransaction = getFragmentManager().beginTransaction();
mCurrentTransaction.setReorderingAllowed(true);
}
return mCurrentTransaction;
@@ -157,6 +186,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
mIsAttached = false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}
private void updateIfNeeded() {
if (!mNeedUpdate || !mIsAttached) {
return;

View File

@@ -43,4 +43,9 @@ public class ScreenContainerViewManager extends ViewGroupManager<ScreenContainer
public View getChildAt(ScreenContainer parent, int index) {
return parent.getScreenAt(index);
}
@Override
public boolean needsCustomLayoutForChildren() {
return true;
}
}

View File

@@ -2,6 +2,7 @@ package com.swmansion.rnscreens;
import android.content.Context;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import java.util.ArrayList;
@@ -10,11 +11,24 @@ import java.util.Set;
public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
private static final String BACK_STACK_TAG = "RN_SCREEN_LAST";
private final ArrayList<ScreenStackFragment> mStack = new ArrayList<>();
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
private ScreenStackFragment mTopScreen = null;
private boolean mLayoutEnqueued = false;
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (getFragmentManager().getBackStackEntryCount() == 0) {
// when back stack entry count hits 0 it means the user's navigated back using hw back
// button. As the "fake" transaction we installed on the back stack does nothing we need
// to handle back navigation on our own.
dismiss(mTopScreen);
}
}
};
public ScreenStack(Context context) {
super(context);
@@ -25,6 +39,10 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
onUpdate();
}
public Screen getTopScreen() {
return mTopScreen.getScreen();
}
public Screen getRootScreen() {
for (int i = 0, size = getScreenCount(); i < size; i++) {
Screen screen = getScreenAt(i);
@@ -41,40 +59,17 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).layout(0, 0, getWidth(), getHeight());
}
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
}
}
private final Runnable mLayoutRunnable = new Runnable() {
@Override
public void run() {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
@Override
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
}
@@ -169,5 +164,52 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
mStack.addAll(mScreenFragments);
tryCommitTransaction();
setupBackHandlerIfNeeded(mTopScreen);
for (ScreenStackFragment screen : mStack) {
screen.onStackUpdate();
}
}
/**
* The below method sets up fragment manager's back stack in a way that it'd trigger our back
* stack change listener when hw back button is clicked.
*
* Because back stack by default rolls back the transaction the stack entry is associated with we
* generate a "fake" transaction that hides and shows the top fragment. As a result when back
* stack entry is rolled back nothing happens and we are free to handle back navigation on our
* own in `mBackStackListener`.
*
* We pop that "fake" transaction each time we update stack and we add a new one in case the top
* screen is allowed to be dismised using hw back button. This way in the listener we can tell
* if back button was pressed based on the count of the items on back stack. We expect 0 items
* in case hw back is pressed becakse we try to keep the number of items at 1 by always resetting
* and adding new items. In case we don't add a new item to back stack we remove listener so that
* it does not get triggered.
*
* It is important that we don't install back handler when stack contains a single screen as in
* that case we want the parent navigator or activity handler to take over.
*/
private void setupBackHandlerIfNeeded(ScreenStackFragment topScreen) {
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
ScreenStackFragment firstScreen = null;
for (int i = 0, size = mStack.size(); i < size; i++) {
ScreenStackFragment screen = mStack.get(i);
if (!mDismissed.contains(screen)) {
firstScreen = screen;
break;
}
}
if (topScreen != firstScreen && topScreen.isDismissable()) {
getFragmentManager()
.beginTransaction()
.hide(topScreen)
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.commitAllowingStateLoss();
getFragmentManager().addOnBackStackChangedListener(mBackStackListener);
}
}
}

View File

@@ -52,6 +52,13 @@ public class ScreenStackFragment extends ScreenFragment {
}
}
public void onStackUpdate() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {
((ScreenStackHeaderConfig) child).onUpdate();
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@@ -79,4 +86,12 @@ public class ScreenStackFragment extends ScreenFragment {
return view;
}
public boolean isDismissable() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {
return ((ScreenStackHeaderConfig) child).isDismissable();
}
return true;
}
}

View File

@@ -1,7 +1,6 @@
package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.util.TypedValue;
@@ -11,7 +10,6 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@@ -29,17 +27,14 @@ public class ScreenStackHeaderConfig extends ViewGroup {
private int mTitleFontSize;
private int mBackgroundColor;
private boolean mIsHidden;
private boolean mGestureEnabled = true;
private boolean mIsBackButtonHidden;
private boolean mIsShadowHidden;
private int mTintColor;
private final Toolbar mToolbar;
private OnBackPressedCallback mBackCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
getScreenStack().dismiss(getScreenFragment());
}
};
private boolean mIsAttachedToWindow = false;
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
@@ -68,7 +63,14 @@ public class ScreenStackHeaderConfig extends ViewGroup {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
update();
mIsAttachedToWindow = true;
onUpdate();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttachedToWindow = false;
}
private Screen getScreen() {
@@ -101,14 +103,20 @@ public class ScreenStackHeaderConfig extends ViewGroup {
return null;
}
private void installBackCallback() {
mBackCallback.remove();
Fragment fragment = getScreenFragment();
fragment.requireActivity().getOnBackPressedDispatcher().addCallback(fragment, mBackCallback);
public boolean isDismissable() {
return mGestureEnabled;
}
private void update() {
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) {
return;
}
if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
@@ -125,13 +133,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
ActionBar actionBar = activity.getSupportActionBar();
// hide back button
final ScreenStack stack = getScreenStack();
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
actionBar.setDisplayHomeAsUpEnabled(isRoot ? false : !mIsBackButtonHidden);
if (!isRoot) {
installBackCallback();
}
mBackCallback.setEnabled(!isRoot);
// 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
@@ -205,6 +207,12 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
}
private void maybeUpdate() {
if (getParent() != null) {
onUpdate();
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews[index];
}
@@ -218,6 +226,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mSubviewsCount--;
}
mConfigSubviews[index] = null;
maybeUpdate();
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
@@ -225,6 +234,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mSubviewsCount++;
}
mConfigSubviews[index] = child;
maybeUpdate();
}
private TextView getTitleTextView() {
@@ -268,6 +278,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mIsShadowHidden = hideShadow;
}
public void setGestureEnabled(boolean gestureEnabled) {
mGestureEnabled = gestureEnabled;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}

View File

@@ -52,6 +52,12 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
return true;
}
@Override
protected void onAfterUpdateTransaction(ScreenStackHeaderConfig parent) {
super.onAfterUpdateTransaction(parent);
parent.onUpdate();
}
@ReactProp(name = "title")
public void setTitle(ScreenStackHeaderConfig config, String title) {
config.setTitle(title);
@@ -82,6 +88,11 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
config.setHideShadow(hideShadow);
}
@ReactProp(name = "gestureEnabled", defaultBoolean = true)
public void setGestureEnabled(ScreenStackHeaderConfig config, boolean gestureEnabled) {
config.setGestureEnabled(gestureEnabled);
}
@ReactProp(name = "hideBackButton")
public void setHideBackButton(ScreenStackHeaderConfig config, boolean hideBackButton) {
config.setHideBackButton(hideBackButton);

View File

@@ -35,9 +35,11 @@ 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)) {
} else if ("modal".equals(presentation) || "containedModal".equals(presentation)) {
// at the moment Android implementation does not handle contained vs regular modals
view.setStackPresentation(Screen.StackPresentation.MODAL);
} else if ("transparentModal".equals(presentation)) {
} else if ("transparentModal".equals(presentation) || "containedTransparentModal".equals((presentation))) {
// at the moment Android implementation does not handle contained vs regular modals
view.setStackPresentation(Screen.StackPresentation.TRANSPARENT_MODAL);
} else {
throw new JSApplicationIllegalArgumentException("Unknown presentation type " + presentation);

View File

@@ -18,6 +18,13 @@ import {
ScreenStackHeaderTitleView,
} from 'react-native-screens';
function renderComponentOrThunk(componentOrThunk, props) {
if (typeof componentOrThunk === 'function') {
return componentOrThunk(props);
}
return componentOrThunk;
}
class StackView extends React.Component {
_removeScene = route => {
const { navigation } = this.props;
@@ -46,6 +53,7 @@ class StackView extends React.Component {
largeTitle,
headerLargeTitleStyle,
translucent,
hideShadow,
} = options;
const scene = {
@@ -72,6 +80,7 @@ class StackView extends React.Component {
headerLargeTitleStyle && headerLargeTitleStyle.fontFamily,
largeTitleFontSize:
headerLargeTitleStyle && headerLargeTitleStyle.fontSize,
hideShadow,
};
const hasHeader = headerMode !== 'none' && options.header !== null;
@@ -88,7 +97,7 @@ class StackView extends React.Component {
if (options.headerLeft !== undefined) {
children.push(
<ScreenStackHeaderLeftView key="left">
{options.headerLeft({ scene })}
{renderComponentOrThunk(options.headerLeft, { scene })}
</ScreenStackHeaderLeftView>
);
} else if (options.headerBackImage !== undefined) {
@@ -118,17 +127,21 @@ class StackView extends React.Component {
}
if (options.headerTitle) {
children.push(
<ScreenStackHeaderTitleView key="title">
{options.headerTitle({ scene })}
</ScreenStackHeaderTitleView>
);
if (title === undefined && typeof options.headerTitle === 'string') {
headerOptions.title = options.headerTitle;
} else {
children.push(
<ScreenStackHeaderTitleView key="title">
{renderComponentOrThunk(options.headerTitle, { scene })}
</ScreenStackHeaderTitleView>
);
}
}
if (options.headerRight) {
children.push(
<ScreenStackHeaderRightView key="right">
{options.headerRight({ scene })}
{renderComponentOrThunk(options.headerRight, { scene })}
</ScreenStackHeaderRightView>
);
}

View File

@@ -17,6 +17,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
RNSScreenStackAnimationDefault,
RNSScreenStackAnimationNone,
RNSScreenStackAnimationFade,
RNSScreenStackAnimationFlip,
};
@interface RCTConvert (RNSScreen)
@@ -26,6 +27,13 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
@end
@interface RNSScreen : UIViewController
- (instancetype)initWithView:(UIView *)view;
- (void)notifyFinishTransitioning;
@end
@interface RNSScreenManager : RCTViewManager
@end

View File

@@ -8,40 +8,6 @@
#import <React/RCTShadowView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenFrameData : NSObject
@property (nonatomic, readonly) CGFloat rightInset;
@property (nonatomic, readonly) CGFloat topInset;
@property (nonatomic, readonly) CGFloat bottomInset;
@property (nonatomic, readonly) CGFloat leftInset;
@property (nonatomic, readonly) CGFloat navbarOffset;
- (instancetype)initWithInsets:(UIEdgeInsets)insets;
@end
@implementation RNSScreenFrameData
- (instancetype)initWithInsets:(UIEdgeInsets)insets andNavbarOffset:(CGFloat)navbarOffset
{
if (self = [super init]) {
_topInset = insets.top;
_bottomInset = insets.bottom;
_leftInset = insets.left;
_rightInset = insets.right;
_navbarOffset = navbarOffset;
}
return self;
}
@end
@interface RNSScreen : UIViewController
- (instancetype)initWithView:(UIView *)view;
- (void)notifyFinishTransitioning;
@end
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
@end
@@ -129,6 +95,9 @@
case RNSScreenStackAnimationFade:
_controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
break;
case RNSScreenStackAnimationFlip:
_controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
break;
case RNSScreenStackAnimationNone:
case RNSScreenStackAnimationDefault:
// Default
@@ -145,6 +114,8 @@
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
} else {
((RNSScreenStackHeaderConfig*) view).screenView = self;
}
}
@@ -309,8 +280,10 @@ RCT_ENUM_CONVERTER(RNSScreenStackPresentation, (@{
RCT_ENUM_CONVERTER(RNSScreenStackAnimation, (@{
@"default": @(RNSScreenStackAnimationDefault),
@"none": @(RNSScreenStackAnimationNone),
@"fade": @(RNSScreenStackAnimationFade)
@"fade": @(RNSScreenStackAnimationFade),
@"flip": @(RNSScreenStackAnimationFlip),
}), RNSScreenStackAnimationDefault, integerValue)
@end

View File

@@ -54,6 +54,7 @@
{
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
}
- (void)removeReactSubview:(RNSScreenView *)subview
@@ -159,6 +160,10 @@
[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);
[subview setNeedsLayout];
}
}
@end

View File

@@ -6,6 +6,8 @@
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTShadowView.h>
#import <React/RCTRootContentView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@end
@@ -35,6 +37,12 @@
_needUpdate = NO;
[self addSubview:_controller.view];
_controller.interactivePopGestureRecognizer.delegate = self;
// we have to initialize viewControllers with a non empty array for
// largeTitle header to render in the opened state. If it is empty
// the header will render in collapsed state which is perhaps a bug
// in UIKit but ¯\_()_/¯
[_controller setViewControllers:@[[UIViewController new]]];
}
return self;
}
@@ -58,7 +66,6 @@
if ([viewController isEqual:[_reactSubviews objectAtIndex:i - 1].controller]) {
break;
} else {
// TODO: send dismiss event
[_dismissedScreens addObject:[_reactSubviews objectAtIndex:i - 1]];
}
}
@@ -72,7 +79,7 @@
} else if (operation == UINavigationControllerOperationPop) {
screen = (RNSScreenView *) fromVC.view;
}
if (screen != nil && screen.stackAnimation != RNSScreenStackAnimationDefault) {
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
}
return nil;
@@ -85,30 +92,13 @@
// Without the below code the Touchable will remain active (highlighted) for the duration of back
// gesture and onPress may fire when we release the finger.
UIView *parent = _controller.view;
while (parent != nil && ![parent isKindOfClass:[RCTRootView class]]) parent = parent.superview;
RCTRootView *rootView = (RCTRootView *)parent;
[rootView cancelTouches];
while (parent != nil && ![parent isKindOfClass:[RCTRootContentView class]]) parent = parent.superview;
RCTRootContentView *rootView = (RCTRootContentView *)parent;
[rootView.touchHandler cancel];
return _controller.viewControllers.count > 1;
}
- (void)markUpdated
{
// We want 'updateContainer' to be executed on main thread after all enqueued operations in
// uimanager are complete. In order to achieve that we enqueue call on UIManagerQueue from which
// we enqueue call on the main queue. This seems to be working ok in all the cases I've tried but
// there is a chance it is not the correct way to do that.
if (!_needUpdate) {
_needUpdate = YES;
RCTExecuteOnUIManagerQueue(^{
RCTExecuteOnMainQueue(^{
_needUpdate = NO;
[self updateContainer];
});
});
}
}
- (void)markChildUpdated
{
// do nothing
@@ -126,14 +116,12 @@
return;
}
[_reactSubviews insertObject:subview atIndex:atIndex];
[self markUpdated];
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
[_reactSubviews removeObject:subview];
[_dismissedScreens removeObject:subview];
[self markUpdated];
}
- (NSArray<UIView *> *)reactSubviews
@@ -144,6 +132,7 @@
- (void)didUpdateReactSubviews
{
// do nothing
[self updateContainer];
}
- (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
@@ -186,11 +175,17 @@
UIViewController *top = controllers.lastObject;
UIViewController *lastTop = _controller.viewControllers.lastObject;
BOOL shouldAnimate = ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
// at the start we set viewControllers to contain a single UIVIewController
// instance. This is a workaround for header height adjustment bug (see comment
// in the init function). Here, we need to detect if the initial empty
// controller is still there
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
if (_controller.viewControllers.count == 0) {
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
if (firstTimePush) {
// nothing pushed yet
[_controller setViewControllers:@[top] animated:NO];
[_controller setViewControllers:controllers animated:NO];
} else if (top != lastTop) {
if (![controllers containsObject:lastTop]) {
// last top controller is no longer on stack

View File

@@ -1,8 +1,12 @@
#import <React/RCTViewManager.h>
#import <React/RCTConvert.h>
#import "RNSScreen.h"
@interface RNSScreenStackHeaderConfig : UIView
@property (nonatomic, weak) RNSScreenView *screenView;
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *titleFontFamily;
@property (nonatomic, retain) NSNumber *titleFontSize;

View File

@@ -66,60 +66,89 @@
return _reactSubviews;
}
- (UIViewController*)screen
- (UIView *)reactSuperview
{
UIView *superview = self.superview;
if ([superview isKindOfClass:[RNSScreenView class]]) {
return ((RNSScreenView *)superview).controller;
return _screenView;
}
- (void)removeFromSuperview
{
[super removeFromSuperview];
_screenView = nil;
}
- (void)updateViewControllerIfNeeded
{
UIViewController *vc = _screenView.controller;
UINavigationController *nav = (UINavigationController*) vc.parentViewController;
if (vc != nil && nav.visibleViewController == vc) {
[RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self];
}
return nil;
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
[self updateViewControllerIfNeeded];
}
- (void)didUpdateReactSubviews
{
[super didUpdateReactSubviews];
[self updateViewControllerIfNeeded];
}
+ (void)setAnimatedConfig:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar;
BOOL hideShadow = config.hideShadow;
[navbar setTintColor:config.color];
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
[navbar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
[navbar setBarTintColor:[UIColor clearColor]];
hideShadow = YES;
if (@available(iOS 13.0, *)) {
// font customized on the navigation item level, so nothing to do here
} else {
[navbar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
[navbar setBarTintColor:config.backgroundColor];
}
[navbar setTranslucent:config.translucent];
[navbar setValue:@(hideShadow ? YES : NO) forKey:@"hidesShadow"];
BOOL hideShadow = config.hideShadow;
if (config.titleFontFamily || config.titleFontSize || config.titleColor) {
NSMutableDictionary *attrs = [NSMutableDictionary new];
if (config.titleColor) {
attrs[NSForegroundColorAttributeName] = config.titleColor;
}
CGFloat size = config.titleFontSize ? [config.titleFontSize floatValue] : 17;
if (config.titleFontFamily) {
attrs[NSFontAttributeName] = [UIFont fontWithName:config.titleFontFamily size:size];
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
[navbar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
[navbar setBarTintColor:[UIColor clearColor]];
hideShadow = YES;
} else {
attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:size];
[navbar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
[navbar setBarTintColor:config.backgroundColor];
}
[navbar setTitleTextAttributes:attrs];
}
if (@available(iOS 11.0, *) && config.largeTitle) {
if (config.largeTitleFontFamily || config.largeTitleFontSize || config.titleColor) {
NSMutableDictionary *largeAttrs = [NSMutableDictionary new];
[navbar setTranslucent:config.translucent];
[navbar setValue:@(hideShadow ? YES : NO) forKey:@"hidesShadow"];
if (config.titleFontFamily || config.titleFontSize || config.titleColor) {
NSMutableDictionary *attrs = [NSMutableDictionary new];
if (config.titleColor) {
largeAttrs[NSForegroundColorAttributeName] = config.titleColor;
attrs[NSForegroundColorAttributeName] = config.titleColor;
}
CGFloat largeSize = config.largeTitleFontSize ? [config.largeTitleFontSize floatValue] : 34;
if (config.largeTitleFontFamily) {
largeAttrs[NSFontAttributeName] = [UIFont fontWithName:config.largeTitleFontFamily size:largeSize];
CGFloat size = config.titleFontSize ? [config.titleFontSize floatValue] : 17;
if (config.titleFontFamily) {
attrs[NSFontAttributeName] = [UIFont fontWithName:config.titleFontFamily size:size];
} else {
largeAttrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:largeSize];
attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:size];
}
[navbar setTitleTextAttributes:attrs];
}
if (@available(iOS 11.0, *)) {
if (config.largeTitle && (config.largeTitleFontFamily || config.largeTitleFontSize || config.titleColor)) {
NSMutableDictionary *largeAttrs = [NSMutableDictionary new];
if (config.titleColor) {
largeAttrs[NSForegroundColorAttributeName] = config.titleColor;
}
CGFloat largeSize = config.largeTitleFontSize ? [config.largeTitleFontSize floatValue] : 34;
if (config.largeTitleFontFamily) {
largeAttrs[NSFontAttributeName] = [UIFont fontWithName:config.largeTitleFontFamily size:largeSize];
} else {
largeAttrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:largeSize];
}
[navbar setLargeTitleTextAttributes:largeAttrs];
}
[navbar setLargeTitleTextAttributes:largeAttrs];
}
}
}
@@ -136,6 +165,11 @@
}
+ (void)willShowViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
[self updateViewController:vc withConfig:config];
}
+ (void)updateViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
UINavigationItem *navitem = vc.navigationItem;
UINavigationController *navctr = (UINavigationController *)vc.parentViewController;
@@ -146,10 +180,21 @@
BOOL wasHidden = navctr.navigationBarHidden;
BOOL shouldHide = config == nil || config.hide;
if (!shouldHide && !config.translucent) {
// when nav bar is not translucent we chage edgesForExtendedLayout to avoid system laying out
// the screen underneath navigation controllers
vc.edgesForExtendedLayout = UIRectEdgeNone;
} else {
// system default is UIRectEdgeAll
vc.edgesForExtendedLayout = UIRectEdgeAll;
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
navctr.interactivePopGestureRecognizer.enabled = config.gestureEnabled;
#ifdef __IPHONE_13_0
vc.modalInPresentation = !config.gestureEnabled;
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.gestureEnabled;
}
#endif
if (shouldHide) {
return;
@@ -183,7 +228,73 @@
}
navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever;
}
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [UINavigationBarAppearance new];
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
// transparent background color
[appearance configureWithTransparentBackground];
} else {
// non-transparent background or default background
if (config.translucent) {
[appearance configureWithDefaultBackground];
} else {
[appearance configureWithOpaqueBackground];
}
// set background color if specified
if (config.backgroundColor) {
appearance.backgroundColor = config.backgroundColor;
}
}
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
appearance.backgroundColor = config.backgroundColor;
}
if (config.hideShadow) {
appearance.shadowColor = nil;
}
if (config.titleFontFamily || config.titleFontSize || config.titleColor) {
NSMutableDictionary *attrs = [NSMutableDictionary new];
if (config.titleColor) {
attrs[NSForegroundColorAttributeName] = config.titleColor;
}
CGFloat size = config.titleFontSize ? [config.titleFontSize floatValue] : 17;
if (config.titleFontFamily) {
attrs[NSFontAttributeName] = [UIFont fontWithName:config.titleFontFamily size:size];
} else {
attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:size];
}
appearance.titleTextAttributes = attrs;
}
if (config.largeTitleFontFamily || config.largeTitleFontSize || config.titleColor) {
NSMutableDictionary *largeAttrs = [NSMutableDictionary new];
if (config.titleColor) {
largeAttrs[NSForegroundColorAttributeName] = config.titleColor;
}
CGFloat largeSize = config.largeTitleFontSize ? [config.largeTitleFontSize floatValue] : 34;
if (config.largeTitleFontFamily) {
largeAttrs[NSFontAttributeName] = [UIFont fontWithName:config.largeTitleFontFamily size:largeSize];
} else {
largeAttrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:largeSize];
}
appearance.largeTitleTextAttributes = largeAttrs;
}
navitem.standardAppearance = appearance;
navitem.compactAppearance = appearance;
navitem.scrollEdgeAppearance = appearance;
}
#endif
for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
switch (subview.type) {
case RNSScreenStackHeaderSubviewTypeLeft: {

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-screens",
"version": "2.0.0-alpha.5",
"version": "2.0.0-alpha.12",
"description": "First incomplete navigation solution for your react-native app.",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",

View File

@@ -17,7 +17,7 @@ const getViewManagerConfigCompat = name =>
? UIManager.getViewManagerConfig(name)
: UIManager[name];
export function enableScreens(shouldEnableScreens = true) {
function enableScreens(shouldEnableScreens = true) {
ENABLE_SCREENS = shouldEnableScreens;
if (ENABLE_SCREENS && !getViewManagerConfigCompat('RNSScreen')) {
console.error(
@@ -27,11 +27,12 @@ export function enableScreens(shouldEnableScreens = true) {
}
// we should remove this at some point
export function useScreens(shouldUseScreens = true) {
function useScreens(shouldUseScreens = true) {
console.warn('Method `useScreens` is deprecated, please use `enableScreens`');
enableScreens(shouldUseScreens);
}
export function screensEnabled() {
function screensEnabled() {
return ENABLE_SCREENS;
}
@@ -202,6 +203,7 @@ module.exports = {
ScreenStackHeaderTitleView,
ScreenStackHeaderCenterView,
enableScreens,
useScreens,
screensEnabled,
};