Compare commits

..

18 Commits

Author SHA1 Message Date
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
18 changed files with 334 additions and 170 deletions

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,41 +59,9 @@ 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());
}
}
@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 onDetachedFromWindow() {
super.onDetachedFromWindow();
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
}
@Override
@@ -169,5 +155,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().addOnBackStackChangedListener(mBackStackListener);
getFragmentManager()
.beginTransaction()
.hide(topScreen)
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.commit();
}
}
}

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
@@ -268,6 +270,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mIsShadowHidden = hideShadow;
}
public void setGestureEnabled(boolean gestureEnabled) {
mGestureEnabled = gestureEnabled;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}

View File

@@ -82,6 +82,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

@@ -26,6 +26,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

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]];
}
}
@@ -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,9 +175,15 @@
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];
} else if (top != lastTop) {

View File

@@ -78,48 +78,54 @@
+ (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];
}
}
}
@@ -149,7 +155,9 @@
[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 +191,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.8",
"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,
};