Compare commits

..

20 Commits

Author SHA1 Message Date
Krzysztof Magiera
6f9c504627 Bump version -> 2.0.0-alpha.15 2019-11-29 13:16:50 +01:00
Krzysztof Magiera
953763f7d9 Support containedModal mode in RNN stack (#241) 2019-11-29 13:12:39 +01:00
Krzysztof Magiera
7c351df14d Reset header navbar items when not set. (#240)
This fixes the problem when navbar settings update from ones that have header items (e.g. custom title object) to a config without such items. In such a case we'd set navbar items in the first go and never reset those items.
2019-11-29 13:11:08 +01:00
Krzysztof Magiera
da9426b4b9 Bump version -> 2.0.0-alpha.14 2019-11-29 11:20:15 +01:00
Krzysztof Magiera
4169fad8b3 Support headerBackTitleVisible and cadrStyle options. (#239) 2019-11-29 11:19:23 +01:00
Krzysztof Magiera
4f8efd2873 Revert "Fix navbar window fitting on Android. (#235)"
This reverts commit 0927e03687.
2019-11-27 22:17:52 +01:00
Krzysztof Magiera
cbc86bb6d8 Avoid changing back stack when fragment manager is not resumed. (#237)
This change fixes the problem when there are changes being made to the stack while the hosting activity is in paused state (e.g. an activity-based dialog from google play services). In such a case it is not allowed to do any changes which can affect fragment manager's state (e.g. updating backstack). For other changes we use `commitAllowingStatLoss` to indicate we are not interested in fragment manager handling their restoration and hence we can perform most operations. Unfortunately installing back handler is not allowed without state change and we need to wait until the fragment host is resumed before we install it.
2019-11-27 22:00:16 +01:00
Krzysztof Magiera
0927e03687 Fix navbar window fitting on Android. (#235)
This change fixes the issue of handling transparent status bar. In such a case the navbar should became slightly bigger in order to fill the status bar space that now belongs to the window. In order for that to happen we use `setFitsSystemWindows` on main screen layout, on app bar layout and on toolbar (the toolbar is the view that gets bigger in the end).

Note that this currently only work if we use Android's native mechanism for changingthe status bar behavior and not using `StatusBar` module from RN. It is because `StausBar` module actually strips top insets from the event that gets delivered to the views. For the time being you can try the following to change status bar:
```
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
```

Note that we are also disabling app bar's scroll behavior. We wasn't utilising this yet, and it cause some weird errors on hot reload because scrolling behavior actually sets `firsSystemWindows` flag on the screen view which ended up consuming insets before appbar would get them.
2019-11-27 12:35:26 +01:00
Janic Duplessis
efaf0cd125 Fix titleFontSize on Android (#227) 2019-11-26 19:08:34 +01:00
Krzysztof Magiera
21e6a9732a Bump version -> 2.0.0-alpha.13 2019-11-26 15:33:58 +01:00
Krzysztof Magiera
31192250e1 Fix stack nesting on Android. (#234)
This change fixes two issues related to stack nesting on Android.

First one being the fact that root and nested fragments were relying on the same fragment manager as opposed to using nested fragment manager structure (via getChildFragmentManager). This resulted in an unprodictable behavior when displaying the top screen. This commit changes this behavior by correctly returning child fragment manager or root fragment manager depending on the containers hierarchy.

Second issue was related to back stack handling and resulted in always the root fragment manager interacting. Instead we want the bottommost active stack to handle back presses. This has been addressed by adding `setPrimaryNavigationFragment` when creating back stack entries.
2019-11-26 15:32:41 +01:00
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
12 changed files with 151 additions and 30 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

@@ -23,6 +23,7 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
private @Nullable FragmentManager mFragmentManager;
private @Nullable FragmentTransaction mCurrentTransaction;
private boolean mNeedUpdate;
private boolean mIsAttached;
@@ -108,11 +109,18 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return mScreenFragments.get(index).getScreen();
}
protected final FragmentActivity findRootFragmentActivity() {
private FragmentManager findFragmentManager() {
ViewParent parent = this;
while (!(parent instanceof ReactRootView) && parent.getParent() != null) {
// We traverse view hierarchy up until we find screen parent or a root view
while (!(parent instanceof ReactRootView || parent instanceof Screen) && parent.getParent() != null) {
parent = parent.getParent();
}
// If parent is of type Screen it means we are inside a nested fragment structure.
// Otherwise we expect to connect directly with root view and get root fragment manager
if (parent instanceof Screen) {
return ((Screen) parent).getFragment().getChildFragmentManager();
}
// we expect top level view to be of type ReactRootView, this isn't really necessary but in order
// to find root view we test if parent is null. This could potentially happen also when the view
// is detached from the hierarchy and that test would not correctly indicate the root view. So
@@ -131,11 +139,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
throw new IllegalStateException(
"In order to use RNScreens components your app's activity need to extend ReactFragmentActivity or ReactCompatActivity");
}
return (FragmentActivity) context;
return ((FragmentActivity) context).getSupportFragmentManager();
}
protected FragmentManager getFragmentManager() {
return findRootFragmentActivity().getSupportFragmentManager();
protected final FragmentManager getFragmentManager() {
if (mFragmentManager == null) {
mFragmentManager = findFragmentManager();
}
return mFragmentManager;
}
protected FragmentTransaction getOrCreateTransaction() {

View File

@@ -2,6 +2,7 @@ package com.swmansion.rnscreens;
import android.content.Context;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -30,6 +31,15 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
};
private final FragmentManager.FragmentLifecycleCallbacks mLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentResumed(FragmentManager fm, Fragment f) {
if (mTopScreen == f) {
setupBackHandlerIfNeeded(mTopScreen);
}
}
};
public ScreenStack(Context context) {
super(context);
}
@@ -61,7 +71,23 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
FragmentManager fm = getFragmentManager();
fm.removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
if (!fm.isStateSaved()) {
// state save means that the container where fragment manager was installed has been unmounted.
// This could happen as a result of dismissing nested stack. In such a case we don't need to
// reset back stack as it'd result in a crash caused by the fact the fragment manager is no
// longer attached.
fm.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
}
@Override
@@ -79,7 +105,11 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
getOrCreateTransaction().remove(screen);
}
}
ScreenStackFragment newTop = null;
// When going back from a nested stack with a single screen on it, we may hit an edge case
// when all screens are dismissed and no screen is to be displayed on top. We need to gracefully
// handle the case of newTop being NULL, which happens in several places below
ScreenStackFragment newTop = null; // newTop is nullable, see the above comment ^
ScreenStackFragment belowTop = null; // this is only set if newTop has TRANSPARENT_MODAL presentation mode
for (int i = mScreenFragments.size() - 1; i >= 0; i--) {
@@ -97,7 +127,6 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
}
for (ScreenStackFragment screen : mScreenFragments) {
// add all new views that weren't on stack before
if (!mStack.contains(screen) && !mDismissed.contains(screen)) {
@@ -118,7 +147,10 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
});
}
getOrCreateTransaction().show(newTop);
if (newTop != null) {
getOrCreateTransaction().show(newTop);
}
if (!mStack.contains(newTop)) {
// if new top screen wasn't on stack we do "open animation" so long it is not the very first screen on stack
@@ -156,7 +188,9 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
tryCommitTransaction();
setupBackHandlerIfNeeded(mTopScreen);
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
for (ScreenStackFragment screen : mStack) {
screen.onStackUpdate();
@@ -183,6 +217,12 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
* that case we want the parent navigator or activity handler to take over.
*/
private void setupBackHandlerIfNeeded(ScreenStackFragment topScreen) {
if (!mTopScreen.isResumed()) {
// if the top fragment is not in a resumed state, adding back stack transaction would throw.
// In such a case we skip installing back handler and use FragmentLifecycleCallbacks to get
// notified when it gets resumed so that we can install the handler.
return;
}
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
ScreenStackFragment firstScreen = null;
@@ -194,13 +234,14 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
}
if (topScreen != firstScreen && topScreen.isDismissable()) {
getFragmentManager().addOnBackStackChangedListener(mBackStackListener);
getFragmentManager()
.beginTransaction()
.hide(topScreen)
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.commit();
.setPrimaryNavigationFragment(topScreen)
.commitAllowingStateLoss();
getFragmentManager().addOnBackStackChangedListener(mBackStackListener);
}
}
}

View File

@@ -24,7 +24,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
private String mTitle;
private int mTitleColor;
private String mTitleFontFamily;
private int mTitleFontSize;
private float mTitleFontSize;
private int mBackgroundColor;
private boolean mIsHidden;
private boolean mGestureEnabled = true;
@@ -207,6 +207,12 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
}
private void maybeUpdate() {
if (getParent() != null) {
onUpdate();
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews[index];
}
@@ -220,6 +226,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mSubviewsCount--;
}
mConfigSubviews[index] = null;
maybeUpdate();
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
@@ -227,6 +234,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mSubviewsCount++;
}
mConfigSubviews[index] = child;
maybeUpdate();
}
private TextView getTitleTextView() {
@@ -250,7 +258,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mTitleFontFamily = titleFontFamily;
}
public void setTitleFontSize(int titleFontSize) {
public void setTitleFontSize(float titleFontSize) {
mTitleFontSize = titleFontSize;
}

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);
@@ -63,8 +69,8 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
}
@ReactProp(name = "titleFontSize")
public void setTitleFontSize(ScreenStackHeaderConfig config, double titleFontSizeSP) {
config.setTitleFontSize((int) PixelUtil.toPixelFromSP(titleFontSizeSP));
public void setTitleFontSize(ScreenStackHeaderConfig config, float titleFontSize) {
config.setTitleFontSize(titleFontSize);
}
@ReactProp(name = "titleColor", customType = "Color")

View File

@@ -48,6 +48,7 @@ class StackView extends React.Component {
headerTitleStyle,
headerBackTitleStyle,
headerBackTitle,
headerBackTitleVisible,
headerTintColor,
gestureEnabled,
largeTitle,
@@ -69,7 +70,7 @@ class StackView extends React.Component {
titleFontFamily: headerTitleStyle && headerTitleStyle.fontFamily,
titleColor: headerTintColor,
titleFontSize: headerTitleStyle && headerTitleStyle.fontSize,
backTitle: headerBackTitle,
backTitle: headerBackTitleVisible === false ? '' : headerBackTitle,
backTitleFontFamily:
headerBackTitleStyle && headerBackTitleStyle.fontFamily,
backTitleFontSize: headerBackTitleStyle && headerBackTitleStyle.fontSize,
@@ -154,20 +155,21 @@ class StackView extends React.Component {
};
_renderScene = (index, route, descriptor) => {
const { navigation, getComponent } = descriptor;
const { navigation, getComponent, options } = descriptor;
const { mode, transparentCard } = this.props.navigationConfig;
const SceneComponent = getComponent();
let stackPresentation = 'push';
if (mode === 'modal') {
stackPresentation = transparentCard ? 'transparentModal' : 'modal';
if (mode === 'modal' || mode === 'containedModal') {
stackPresentation =
transparentCard || options.cardTransparent ? 'transparentModal' : mode;
}
const { screenProps } = this.props;
return (
<Screen
key={`screen_${route.key}`}
style={StyleSheet.absoluteFill}
style={options.cardStyle}
stackPresentation={stackPresentation}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}

View File

@@ -17,6 +17,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
RNSScreenStackAnimationDefault,
RNSScreenStackAnimationNone,
RNSScreenStackAnimationFade,
RNSScreenStackAnimationFlip,
};
@interface RCTConvert (RNSScreen)

View File

@@ -95,6 +95,9 @@
case RNSScreenStackAnimationFade:
_controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
break;
case RNSScreenStackAnimationFlip:
_controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
break;
case RNSScreenStackAnimationNone:
case RNSScreenStackAnimationDefault:
// Default
@@ -111,6 +114,8 @@
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
} else {
((RNSScreenStackHeaderConfig*) view).screenView = self;
}
}
@@ -275,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

@@ -79,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;
@@ -185,7 +185,7 @@
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,13 +66,36 @@
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
@@ -142,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;
@@ -152,6 +180,15 @@
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
@@ -258,6 +295,9 @@
navitem.scrollEdgeAppearance = appearance;
}
#endif
navitem.leftBarButtonItem = nil;
navitem.rightBarButtonItem = nil;
navitem.titleView = nil;
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.8",
"version": "2.0.0-alpha.15",
"description": "First incomplete navigation solution for your react-native app.",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",