Compare commits

...

9 Commits

Author SHA1 Message Date
osdnk
e61b8b3bd6 Bump version -> 2.0.0-alpha.20 2019-12-17 17:56:11 +01:00
Michał Osadnik
77d877f0c1 Fix not displaying view in navigation (#252)
fixes #208

For stack overriding reactSetFrame for active does nothing because active is always NO, so there's no regression.

But it fixes in other cases (like stack navigation - not native one).
2019-12-17 17:55:03 +01:00
Kevin Hermawan
15c27e3fe7 Fix README typo (#226) 2019-12-15 15:12:09 +01:00
Krzysztof Magiera
ed478a829d Bump version -> 2.0.0-alpha.19 2019-12-12 23:01:59 +01:00
Krzysztof Magiera
a3335b1384 Use containedTransparentModal in RNN wrapper when requested. (#249)
Previously when transparent card was requested we'd default to transparentModal stack presentation mode. However if user requests contained modal mode we should be using containedTransparentModal instead. This change fixes that behavior.
2019-12-12 23:01:36 +01:00
Krzysztof Magiera
e00a08a3dc Bump version -> 2.0.0-alpha.18 2019-12-11 22:28:45 +01:00
Krzysztof Magiera
656e82de9f Dispatch appear event for screens. (#248)
Appear event is used by react-navigation to properly dispatch focus. It is important that appear is dispatched after dismissed event. The reverse order of actions would result in getting react-navigation stack in a weird state.

It is relatively streightforward to implement onAppear event on iOS where we hook into didAppear callback from UIViewController. It gets dispatched in the right moment, that is when the transition is fully over.

On Android however it is much more tricky. There is no standard way to be notified from the fragment level that fragment transition finished. One way that is frequently recommended is to override Fragment.onCreateAnimation. However, this only works when custom transitions are provided (e.g. if we set the transition to use fade animation). As we want the platform native transition to be run by default we had to look for other ways. The current approach relies on fragment container's callbacks startViewTransition and endViewTransition, with the latter being triggered once the animation is over. We also need to take into account that a good starting point for the transition is when we call commit on fragment transaction. We use these two methods to determine if the fragment is instantiated (onCreate) within a running transaction and if so we schedule event dispatch at the moment when endViewTransition is called.

Another change this commit introduces on the Android side is that we no longer rely on show/hide for replacing fragments on stack and we now use add/remove transaction methods. Due to this change we had to make our fragments reusable and make onCreateView indempotent.
2019-12-11 22:28:19 +01:00
Krzysztof Magiera
1958cf37ea Bump version -> 2.0.0-alpha.17 2019-12-04 14:44:09 +01:00
Krzysztof Magiera
75fb558cd3 Fix modal controllers update algorithm. (#245)
The previous algorithm was buggy and did not handle the case when multiple VCs are being dismissed. Unfortunately I couldn't find a reliable way that'd allow for reshuffling modally presented VCs (inserting or deleting VCs not from the top) and so I added a special check that'd throw in the case someone attemted to do this.
2019-12-04 14:41:52 +01:00
13 changed files with 223 additions and 56 deletions

View File

@@ -127,7 +127,7 @@ Otherwise the views will be attached as long as the parent container is attached
### `<ScreenStack>`
Screen stack component expects one or more `Screen` components as direct children and renders them in a platform native stack container (for iOS it is `UINavigationController` and for Android inside `Fragment` container). For `Screen` components placed as children of `ScteenStack` the `active` property is ignored and instead the screen that corresponds to the last child is rendered as active. All type of updates done to the list of children are acceptable, when the top element is exchanged the container will use platform default (unless customized) animation to transition between screens.
Screen stack component expects one or more `Screen` components as direct children and renders them in a platform native stack container (for iOS it is `UINavigationController` and for Android inside `Fragment` container). For `Screen` components placed as children of `ScreenStack` the `active` property is ignored and instead the screen that corresponds to the last child is rendered as active. All type of updates done to the list of children are acceptable, when the top element is exchanged the container will use platform default (unless customized) animation to transition between screens.
`StackScreen` extends the capabilities of `Screen` component to allow additional customizations and to make it possible to handle events such as using hardware back or back gesture to dismiss the top screen. Below is the list of additional properties that can be used for `Screen` component:

View File

@@ -0,0 +1,30 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class ScreenAppearEvent extends Event<ScreenAppearEvent> {
public static final String EVENT_NAME = "topAppear";
public ScreenAppearEvent(int viewId) {
super(viewId);
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public short getCoalescingKey() {
// All events for a given view can be coalesced.
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap());
}
}

View File

@@ -2,6 +2,7 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -22,11 +23,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
private final ArrayList<Runnable> mAfterTransitionRunnables = new ArrayList<>(1);
private @Nullable FragmentManager mFragmentManager;
private @Nullable FragmentTransaction mCurrentTransaction;
private @Nullable FragmentTransaction mProcessingTransaction;
private boolean mNeedUpdate;
private boolean mIsAttached;
private boolean mIsTransitioning;
private boolean mLayoutEnqueued = false;
private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
@@ -101,6 +105,36 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
markUpdated();
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mIsTransitioning = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mIsTransitioning) {
mIsTransitioning = false;
notifyTransitionFinished();
}
}
public boolean isTransitioning() {
return mIsTransitioning || mProcessingTransaction != null;
}
public void postAfterTransition(Runnable runnable) {
mAfterTransitionRunnables.add(runnable);
}
protected void notifyTransitionFinished() {
for (int i = 0, size = mAfterTransitionRunnables.size(); i < size; i++) {
mAfterTransitionRunnables.get(i).run();
}
mAfterTransitionRunnables.clear();
}
protected int getScreenCount() {
return mScreenFragments.size();
}
@@ -159,6 +193,19 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void tryCommitTransaction() {
if (mCurrentTransaction != null) {
final FragmentTransaction transaction = mCurrentTransaction;
mProcessingTransaction = transaction;
mProcessingTransaction.runOnCommit(new Runnable() {
@Override
public void run() {
if (mProcessingTransaction == transaction) {
// we need to take into account that commit is initiated with some other transaction while
// the previous one is still processing. In this case mProcessingTransaction gets overwritten
// and we don't want to set it to null until the second transaction is finished.
mProcessingTransaction = null;
}
}
});
mCurrentTransaction.commitAllowingStateLoss();
mCurrentTransaction = null;
}
@@ -184,6 +231,10 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return screenFragment.getScreen().isActive();
}
protected boolean hasScreen(ScreenFragment screenFragment) {
return mScreenFragments.contains(screenFragment);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();

View File

@@ -7,12 +7,10 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
public class ScreenFragment extends Fragment {
@@ -39,12 +37,39 @@ public class ScreenFragment extends Fragment {
return mScreenView;
}
@Override
public void onDestroy() {
super.onDestroy();
private void dispatchOnAppear() {
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
.dispatchEvent(new ScreenAppearEvent(mScreenView.getId()));
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ScreenContainer container = mScreenView.getContainer();
if (container.isTransitioning()) {
container.postAfterTransition(new Runnable() {
@Override
public void run() {
dispatchOnAppear();
}
});
} else {
dispatchOnAppear();
}
}
@Override
public void onDestroy() {
super.onDestroy();
ScreenContainer container = mScreenView.getContainer();
if (container == null || !container.hasScreen(this)) {
// we only send dismissed even when the screen has been removed from its container
((ReactContext) mScreenView.getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new ScreenDismissedEvent(mScreenView.getId()));
}
}
}

View File

@@ -97,6 +97,11 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
super.removeScreenAt(index);
}
@Override
protected boolean hasScreen(ScreenFragment screenFragment) {
return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment);
}
@Override
protected void onUpdate() {
// remove all screens previously on stack
@@ -128,19 +133,15 @@ 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)) {
getOrCreateTransaction().add(getId(), screen);
}
// detach all screens that should not be visible
if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) {
getOrCreateTransaction().hide(screen);
getOrCreateTransaction().remove(screen);
}
}
// attach "below top" screen if set
if (belowTop != null) {
if (belowTop != null && !belowTop.isAdded()) {
final ScreenStackFragment top = newTop;
getOrCreateTransaction().show(belowTop).runOnCommit(new Runnable() {
getOrCreateTransaction().add(getId(), belowTop).runOnCommit(new Runnable() {
@Override
public void run() {
top.getScreen().bringToFront();
@@ -148,8 +149,8 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
});
}
if (newTop != null) {
getOrCreateTransaction().show(newTop);
if (newTop != null && !newTop.isAdded()) {
getOrCreateTransaction().add(getId(), newTop);
}
if (!mStack.contains(newTop)) {

View File

@@ -22,6 +22,7 @@ public class ScreenStackFragment extends ScreenFragment {
private AppBarLayout mAppBarLayout;
private Toolbar mToolbar;
private boolean mShadowHidden;
private CoordinatorLayout mScreenRootView;
@SuppressLint("ValidFragment")
public ScreenStackFragment(Screen screenView) {
@@ -59,10 +60,7 @@ public class ScreenStackFragment extends ScreenFragment {
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
private CoordinatorLayout configureView() {
CoordinatorLayout view = new CoordinatorLayout(getContext());
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
@@ -87,6 +85,17 @@ public class ScreenStackFragment extends ScreenFragment {
return view;
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
if (mScreenRootView == null) {
mScreenRootView = configureView();
}
return mScreenRootView;
}
public boolean isDismissable() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {

View File

@@ -62,6 +62,8 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"));
MapBuilder.of("registrationName", "onDismissed"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"));
}
}

View File

@@ -4,7 +4,6 @@ import {
StackRouter,
SceneView,
StackActions,
NavigationActions,
createNavigator,
} from '@react-navigation/core';
import { createKeyboardAwareNavigator } from '@react-navigation/native';
@@ -27,14 +26,13 @@ function renderComponentOrThunk(componentOrThunk, props) {
class StackView extends React.Component {
_removeScene = route => {
const { navigation } = this.props;
navigation.dispatch(
NavigationActions.back({
key: route.key,
immediate: true,
})
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
};
_onSceneFocus = route => {
this.props.navigation.dispatch(
StackActions.completeTransition({ toChildKey: route.key })
);
navigation.dispatch(StackActions.completeTransition());
};
_renderHeaderConfig = (index, route, descriptor) => {
@@ -161,11 +159,16 @@ class StackView extends React.Component {
let stackPresentation = 'push';
if (mode === 'modal' || mode === 'containedModal') {
stackPresentation =
transparentCard || options.cardTransparent ? 'transparentModal' : mode;
stackPresentation = mode;
if (transparentCard || options.cardTransparent) {
stackPresentation =
mode === 'containedModal'
? 'containedTransparentModal'
: 'transparentModal';
}
}
let stackAnimation = undefined;
let stackAnimation;
if (options.animationEnabled === false) {
stackAnimation = 'none';
}
@@ -177,6 +180,7 @@ class StackView extends React.Component {
style={options.cardStyle}
stackAnimation={stackAnimation}
stackPresentation={stackPresentation}
onAppear={() => this._onSceneFocus(route)}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}
<SceneView

View File

@@ -39,6 +39,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
@interface RNSScreenView : RCTView
@property (nonatomic, copy) RCTDirectEventBlock onAppear;
@property (nonatomic, copy) RCTDirectEventBlock onDismissed;
@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
@property (nonatomic, retain) UIViewController *controller;

View File

@@ -33,6 +33,9 @@
- (void)reactSetFrame:(CGRect)frame
{
if (_active) {
[super reactSetFrame:frame];
}
// ignore setFrame call from react, the frame of this view
// is controlled by the UIViewController it is contained in
}
@@ -135,6 +138,17 @@
}
}
- (void)notifyAppear
{
if (self.onAppear) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onAppear) {
self.onAppear(nil);
}
});
}
}
- (BOOL)isMountedUnderScreenOrReactRoot
{
for (UIView *parent = self.superview; parent != nil; parent = parent.superview) {
@@ -235,6 +249,12 @@
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[((RNSScreenView *)self.view) notifyAppear];
}
- (void)notifyFinishTransitioning
{
[_previousFirstResponder becomeFirstResponder];
@@ -258,6 +278,7 @@ RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDismissed, RCTDirectEventBlock);
- (UIView *)view

View File

@@ -2,7 +2,7 @@
#import <React/RCTUIManagerObserverCoordinator.h>
#import "RNSScreenContainer.h"
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate>
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate, RCTInvalidating>
- (void)markChildUpdated;
- (void)didUpdateChildren;

View File

@@ -143,31 +143,48 @@
NSMutableArray<UIViewController *> *controllersToRemove = [NSMutableArray arrayWithArray:_presentedModals];
[controllersToRemove removeObjectsInArray:controllers];
// presenting new controllers
for (UIViewController *newController in newControllers) {
[_presentedModals addObject:newController];
if (_controller.presentedViewController != nil) {
[_controller.presentedViewController presentViewController:newController animated:YES completion:nil];
// find bottom-most controller that should stay on the stack for the duration of transition
NSUInteger changeRootIndex = 0;
UIViewController *changeRootController = _controller;
for (NSUInteger i = 0; i < MIN(_presentedModals.count, controllers.count); i++) {
if (_presentedModals[i] == controllers[i]) {
changeRootController = controllers[i];
changeRootIndex = i + 1;
} else {
[_controller presentViewController:newController animated:YES completion:nil];
break;
}
}
// hiding old controllers
for (UIViewController *controller in [controllersToRemove reverseObjectEnumerator]) {
[_presentedModals removeObject:controller];
if (controller.presentedViewController != nil) {
UIViewController *restore = controller.presentedViewController;
UIViewController *parent = controller.presentingViewController;
[controller dismissViewControllerAnimated:NO completion:^{
[parent dismissViewControllerAnimated:NO completion:^{
[parent presentViewController:restore animated:NO completion:nil];
}];
}];
} else {
[controller.presentingViewController dismissViewControllerAnimated:YES completion:nil];
// we verify that controllers added on top of changeRootIndex are all new. Unfortunately modal
// VCs cannot be reshuffled (there are some visual glitches when we try to dismiss then show as
// even non-animated dismissal has delay and updates the screen several times)
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
if ([_presentedModals containsObject:controllers[i]]) {
RCTAssert(false, @"Modally presented controllers are being reshuffled, this is not allowed");
}
}
void (^finish)(void) = ^{
UIViewController *previous = changeRootController;
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
UIViewController *next = controllers[i];
[previous presentViewController:next
animated:(i == controllers.count - 1)
completion:nil];
previous = next;
}
[self->_presentedModals removeAllObjects];
[self->_presentedModals addObjectsFromArray:controllers];
};
if (changeRootController.presentedViewController) {
[changeRootController
dismissViewControllerAnimated:(changeRootIndex == controllers.count)
completion:finish];
} else {
finish();
}
}
- (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
@@ -244,12 +261,18 @@
_controller.view.frame = self.bounds;
}
- (void)invalidate
{
for (UIViewController *controller in _presentedModals) {
[controller dismissViewControllerAnimated:NO completion:nil];
}
[_presentedModals removeAllObjects];
}
- (void)dismissOnReload
{
dispatch_async(dispatch_get_main_queue(), ^{
for (UIViewController *controller in self->_presentedModals) {
[controller dismissViewControllerAnimated:NO completion:nil];
}
[self invalidate];
});
}

View File

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