mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-03-29 00:38:26 +08:00
Compare commits
9 Commits
2.0.0-alph
...
2.0.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e61b8b3bd6 | ||
|
|
77d877f0c1 | ||
|
|
15c27e3fe7 | ||
|
|
ed478a829d | ||
|
|
a3335b1384 | ||
|
|
e00a08a3dc | ||
|
|
656e82de9f | ||
|
|
1958cf37ea | ||
|
|
75fb558cd3 |
@@ -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:
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#import <React/RCTUIManagerObserverCoordinator.h>
|
||||
#import "RNSScreenContainer.h"
|
||||
|
||||
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate>
|
||||
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate, RCTInvalidating>
|
||||
|
||||
- (void)markChildUpdated;
|
||||
- (void)didUpdateChildren;
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user