Compare commits

...

51 Commits

Author SHA1 Message Date
Krzysztof Magiera
a1311cd31d Bump version -> 2.0.0-alpha.28 2020-01-22 15:20:22 +01:00
Krzysztof Magiera
f044334464 Fix focus events in native stack on Android. (#292)
This change fixes the issue with auto focusing text input fields inside native stack. Due to the fact we perform hide and show transactions in order to control back stack the hiding part would propagate visibility change event down the view hierarchy. As a result views would receive that visibility event and blur themselves resulting in textinput not getting the focus when the fragment mounts. It turns out however that for the back stack to function properly we don't need to run hide and show operation within the transaction because show is idempotent. So we change our back stack handling transaction to only call show.
2020-01-22 15:19:54 +01:00
Krzysztof Magiera
1c7ebeba39 Fix NPE on nested stack detach on Android. (#291)
This change fixes crash caused when nested stack is detached due to the parent stack being closed. As a result we may end up in a situation when fragment manager is not yet set despite onDetach callback being call. When fragment manager is not set we can also ignore the operations we should've performed in on detach. This change adds a null check before we call into fragment manager in on detach callback.
2020-01-22 15:16:25 +01:00
Krzysztof Magiera
1493d6f1b8 Bump version -> 2.0.0-alpha.27 2020-01-21 11:51:50 +01:00
Krzysztof Magiera
8cf82d1cbe Fix layout of header items on Android native stack. (#289)
This change fixes the issue when left item added to native stack header on Android would by default be shifted from the screen edge by some amount. This turned out to be the default config of the native toolbar which applies some padding on the left side. WIn this change we reset that padding to always be 0 to let the position be specified from react. Note that the setting we reset does not influence the position of the native back navigation button as Android does not apply the padding in case navigation back icon is rendered.
2020-01-20 22:21:07 +01:00
Krzysztof Magiera
e8403582ae Bump version -> 2.0.0-alpha.26 2020-01-20 10:59:13 +01:00
Tuan Luong
5832593980 [Android] update view when go back to previous screen (#286) 2020-01-20 10:58:19 +01:00
dependabot[bot]
78c7745049 Bump handlebars from 4.1.2 to 4.5.3 (#255)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.1.2 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.1.2...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-01-17 22:15:51 +01:00
dependabot[bot]
350a80c29b Bump handlebars from 4.2.0 to 4.5.3 in /Example (#256)
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.2.0 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.2.0...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-01-17 22:15:39 +01:00
Michael Wood
c460341a68 Minor typo/wording tweaks (#264) 2020-01-17 22:15:14 +01:00
Angelika Serwa
3fc74e29ab [android] Fix re-attaching ScreenContainer to window (#272)
Code to reproduce and test: https://snack.expo.io/@angelikaserwa/humiliated-bagel.
Switch to the Settings tab, then go back to the Home tab and press the details button. Nothing happens. It was because after re-attaching we were using a destroyed FragmentManager that was cached inside the ScreenContainer class. 
Then, when we go back from Details to Home screen, using a hardware back button, an exception occured: `The specified child already has a parent. You must call removeView() on the child's parent first`.
I fixed this by calling `removeAllViews()` when detaching container from window and forcing an update on re-attach.
2020-01-17 22:13:57 +01:00
Krzysztof Magiera
518c094657 Revert "[Android] Bottom tab not update" (#285)
This reverts commit ca6319d26e.
2020-01-17 22:11:37 +01:00
Krzysztof Magiera
d71aa2c6ef Bump version -> 2.0.0-alpha.25 2020-01-15 21:26:01 +01:00
Krzysztof Magiera
f21ec66cb4 Fix header transition on Android. (#284)
This change prevents toolbar from updating when there is a transition happening to the view. This fixes the issue where header subviews would disappear when going back from a given screen. The root cause was that the config view manager would trigger subviews removal and re-layout itself. As aresult if we had a custom back button that back button would get removed and the title would move into its place while the screen animation is running. In order to prevent that we hook into View Manager's onDropViewInstance to notify header config that it is about to be destroyed. This in turn prevents any updates to be made to the toolbar config.
2020-01-15 21:25:36 +01:00
Janic Duplessis
27ef6dc900 Fix stack with gestureEnabled=false on iOS (#283)
#254 without breaking changes

Instead of moving gestureEnabled to the screen we find the header config subview.
2020-01-14 15:46:16 -05:00
Krzysztof Magiera
ce819f6356 Bump version -> 2.0.0-alpha.24 2020-01-14 18:28:46 +01:00
Krzysztof Magiera
1ac742610b Fix detaching views rendered under header config component. (#282)
This change fixes the problem when header update would get triggered while the header config subviews are not yet fully unmounted. The root cause of this problem is kind of a mystery, I encouneter a crash when going back using back button from a screen that have custom back button image rendered. Turned out that after implementing removeAll schema the problem no longer occurs. I didn't have enough time to investigate it further but supporting removeAll schema is an improvement regardless so going with this solution for now.
2020-01-14 18:28:19 +01:00
Tuan Luong
28f57240c2 [ios] release view from controller when popped from stack (#258)
This issue is related to #92.

Issue
I got issue when playing video with react-native-video. The video keep playing (still hear sound) when go back.

react-native: 0.61.5
react-native-screens: 2.0.0-alpha.22
Solution
When controller popped, try to assign view to nil to break the cycle.
2020-01-11 10:38:06 -05:00
Tuan Luong
ca6319d26e [Android] Bottom tab not update (#275)
Fixes #259

Issue
Screen doesn't update when switching between tabs.

Problem
When ScreenContainer re-added. mFragmentManager is destroyed.
2020-01-11 10:36:28 -05:00
Krzysztof Magiera
2b1b726723 Bump version -> 2.0.0-alpha.23 2020-01-10 10:07:44 +01:00
Krzysztof Magiera
4ce9469571 Fix bug in modal dismiss logic. (#277)
This change fixes a problem in modal dismiss logic. The problem would occur when we had a nested stack that both have screens pushed and presented modally. In such case when we only wanted to pop one of the screens pushed (even though the screen was obscured by the one presented modally) we'd still run setModalViewControllers method. As a result that method could've find the navigato that is presenting the screens modally and trigger dismiss on them. This was the case because we used to only check in that method whether the top-most unchanged screen is presenting modals. We shouldn't only be checking whether the screen is presenting because it may be presenting modals that belong to a different stack that did not change.

To prevent the issue from happening we make sure that we run dismiss logic only when a change on a give stack occur. We verify that the list of presented VCs is not equal to the list of new controllers before we run push-stack or modal-stack presenting/dismiss logic.

On top of that change I also modified push logic to avoid running animation when the push stack is obscured by modals. This prevents some weird effects when you could see the end of animation after closing the modal immediately after pop done in the hidden stack.
2020-01-10 10:04:45 +01:00
osdnk
cef9b8f393 add onAppear callback 2020-01-09 16:05:14 -05:00
Krzysztof Magiera
ecc4056559 Get rid of lack-of-key warning 2020-01-09 18:10:10 +01:00
Jesse Katsumata
71a02af309 Add missing types (#262)
This issue closes #261 

Added typescript definitions for `<ScreenStack>` and `<ScreenStackHeaderConfig>`
2020-01-07 11:33:03 +01:00
Krzysztof Magiera
c88da09348 Bump version -> 2.0.0-alpha.22 2019-12-20 22:23:47 +01:00
Krzysztof Magiera
9aac0b89f2 Allow for RNN wrapper to customize native stack animations 2019-12-20 22:23:22 +01:00
Krzysztof Magiera
a02dd46eec Bump version -> 2.0.0-alpha.21 2019-12-20 22:00:45 +01:00
Krzysztof Magiera
0919404b2e Add support for customizing back button image. (#253)
For customizing back button image we use platform native functionality that is: `setBackIndicatorImage` on iOS and `setHomeAsUpIndicator` on Android.
The reason we don't do that just by setting left item is that we get a couple of things for free such as handling RTL properly, working accessibility features, handling prop for hiding back button and a couple more.

Unfortunately there are some downsides to that approach too. We need to install the back button as an Image component from the JS side, and the extract the image payload on the native side to set it with the navigator. This is specifically problematic in DEV mode where images are loaded asynchronously over HTTP from the packager. In order for that to work we had to employ a few hacks (more comments on that in the code).
2019-12-20 21:59:34 +01:00
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
Krzysztof Magiera
2b55d60780 Bump version -> 2.0.0-alpha.16 2019-12-02 16:01:49 +01:00
Krzysztof Magiera
744b37fbc3 Fix Android's toolbar config update (#244)
Before this change, updates made to toolbar's subviwews weren't properly reflected. We'd only add new views to toolbar w/o removing stale ones.
2019-12-02 10:47:17 +01:00
Krzysztof Magiera
84d684b52d Support RNN wrapper's animationEnabled option (#243) 2019-12-02 10:45:40 +01:00
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
24 changed files with 675 additions and 145 deletions

View File

@@ -1633,7 +1633,7 @@ combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
commander@^2.19.0, commander@~2.20.0:
commander@^2.19.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -1642,6 +1642,11 @@ commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2487,9 +2492,9 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
handlebars@^4.0.3:
version "4.2.0"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.2.0.tgz#57ce8d2175b9bbb3d8b3cf3e4217b1aec8ddcb2e"
integrity sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==
version "4.5.3"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
@@ -4806,7 +4811,7 @@ react-native-safe-area-view@^0.14.1:
debounce "^1.2.0"
"react-native-screens@file:..":
version "2.0.0-alpha.3"
version "2.0.0-alpha.22"
dependencies:
debounce "^1.2.0"
@@ -5761,11 +5766,11 @@ uglify-es@^3.1.9:
source-map "~0.6.1"
uglify-js@^3.1.4:
version "3.6.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
version "3.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==
dependencies:
commander "~2.20.0"
commander "~2.20.3"
source-map "~0.6.1"
ultron@1.0.x:

View File

@@ -100,12 +100,12 @@ To do that react-native-screens provides you with two components documented belo
### `<ScreenContainer/>`
This component is a container for one or more `Screen` components.
It does not accept other component types are direct children.
The role of container is to control which of its children screens should be attached to the view hierarchy.
It does that by monitoring `active` property of each of its children.
It it possible to have as many `active` children as you'd like but in order for the component to be the most efficient we should keep the number of active screens to the minimum.
In a case of stack navigator or tabs navigator we only want to have one active screen (the top most view on a stack or the selected tab).
Then for the time of transitioning between views we may want to activate a second screen for the duration of transition, and then go back to just one active screen.
It does not accept other component types as direct children.
The role of the container is to control which of its children screens should be attached to the view hierarchy.
It does that by monitoring the `active` property of each of its children.
It is possible to have as many `active` children as you'd like but in order for the component to be the most efficient we should keep the number of active screens to a minimum.
In a case of a stack navigator or tabs navigator we only want to have one active screen (the top most view on a stack or the selected tab).
While transitioning between views we may want to activate a second screen for the duration of the transition, and then go back to just one active screen.
### `<Screen/>`
@@ -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,10 +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);
protected @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() {
@@ -100,6 +105,44 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
markUpdated();
}
protected void removeAllScreens() {
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
mScreenFragments.get(i).getScreen().setContainer(null);
}
mScreenFragments.clear();
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();
}
@@ -108,11 +151,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,16 +181,12 @@ 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;
}
protected FragmentManager getFragmentManager() {
return findRootFragmentActivity().getSupportFragmentManager();
return ((FragmentActivity) context).getSupportFragmentManager();
}
protected FragmentTransaction getOrCreateTransaction() {
if (mCurrentTransaction == null) {
mCurrentTransaction = getFragmentManager().beginTransaction();
mCurrentTransaction = mFragmentManager.beginTransaction();
mCurrentTransaction.setReorderingAllowed(true);
}
return mCurrentTransaction;
@@ -148,6 +194,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;
}
@@ -173,10 +232,16 @@ 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();
mIsAttached = true;
mNeedUpdate = true;
mFragmentManager = findFragmentManager();
updateIfNeeded();
}
@@ -184,6 +249,14 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mIsAttached = false;
// fragment manager is destroyed so we can't do anything with it anymore
mFragmentManager = null;
// so we don't add the same screen twice after re-attach
removeAllViews();
mActiveScreenFragments.clear();
// after re-attach we'll update the screen and add views again
markUpdated();
}
@Override

View File

@@ -34,6 +34,11 @@ public class ScreenContainerViewManager extends ViewGroupManager<ScreenContainer
parent.removeScreenAt(index);
}
@Override
public void removeAllViews(ScreenContainer parent) {
parent.removeAllScreens();
}
@Override
public int getChildCount(ScreenContainer parent) {
return parent.getScreenCount();

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

@@ -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;
@@ -21,7 +22,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (getFragmentManager().getBackStackEntryCount() == 0) {
if (mFragmentManager.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.
@@ -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);
}
@@ -60,17 +70,24 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
@Override
protected void onDetachedFromWindow() {
if (mFragmentManager != null) {
mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
mFragmentManager.unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
if (!mFragmentManager.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.
mFragmentManager.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
super.onDetachedFromWindow();
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
}
@Override
@@ -80,6 +97,17 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
super.removeScreenAt(index);
}
@Override
protected void removeAllScreens() {
mDismissed.clear();
super.removeAllScreens();
}
@Override
protected boolean hasScreen(ScreenFragment screenFragment) {
return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment);
}
@Override
protected void onUpdate() {
// remove all screens previously on stack
@@ -88,7 +116,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--) {
@@ -106,28 +138,26 @@ 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();
}
});
}
getOrCreateTransaction().show(newTop);
if (newTop != null && !newTop.isAdded()) {
getOrCreateTransaction().add(getId(), 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
@@ -165,7 +195,9 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
tryCommitTransaction();
setupBackHandlerIfNeeded(mTopScreen);
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
for (ScreenStackFragment screen : mStack) {
screen.onStackUpdate();
@@ -192,8 +224,14 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
* 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);
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;
}
mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
mFragmentManager.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);
@@ -203,13 +241,13 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
}
if (topScreen != firstScreen && topScreen.isDismissable()) {
getFragmentManager()
mFragmentManager
.beginTransaction()
.hide(topScreen)
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.setPrimaryNavigationFragment(topScreen)
.commitAllowingStateLoss();
getFragmentManager().addOnBackStackChangedListener(mBackStackListener);
mFragmentManager.addOnBackStackChangedListener(mBackStackListener);
}
}
}

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

@@ -8,6 +8,7 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
@@ -15,21 +16,24 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.views.text.ReactFontManager;
import java.util.ArrayList;
public class ScreenStackHeaderConfig extends ViewGroup {
private final ScreenStackHeaderSubview mConfigSubviews[] = new ScreenStackHeaderSubview[3];
private int mSubviewsCount = 0;
private final ArrayList<ScreenStackHeaderSubview> mConfigSubviews = new ArrayList<>(3);
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;
private boolean mIsBackButtonHidden;
private boolean mIsShadowHidden;
private boolean mDestroyed;
private int mTintColor;
private final Toolbar mToolbar;
@@ -47,6 +51,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
setVisibility(View.GONE);
mToolbar = new Toolbar(context);
// reset content insets to be 0 to allow react position custom navbar views. Note that this does
// not affect platform native back button as toolbar does not apply left inset when navigation
// button is specified
mToolbar.setContentInsetsAbsolute(0, 0);
// set primary color as background by default
TypedValue tv = new TypedValue();
@@ -60,6 +68,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
// no-op
}
public void destroy() {
mDestroyed = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -113,7 +125,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
if (!mIsAttachedToWindow || !isTop) {
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
return;
}
@@ -174,10 +186,26 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
// subviews
for (int i = 0; i < mSubviewsCount; i++) {
ScreenStackHeaderSubview view = mConfigSubviews[i];
for (int i = mToolbar.getChildCount() - 1; i >= 0; i--) {
if (mToolbar.getChildAt(i) instanceof ScreenStackHeaderSubview) {
mToolbar.removeViewAt(i);
}
}
for (int i = 0, size = mConfigSubviews.size(); i < size; i++) {
ScreenStackHeaderSubview view = mConfigSubviews.get(i);
ScreenStackHeaderSubview.Type type = view.getType();
if (type == ScreenStackHeaderSubview.Type.BACK) {
// we special case BACK button header config type as we don't add it as a view into toolbar
// but instead just copy the drawable from imageview that's added as a first child to it.
View firstChild = view.getChildAt(0);
if (!(firstChild instanceof ImageView)) {
throw new JSApplicationIllegalArgumentException("Back button header config view should have Image as first child");
}
actionBar.setHomeAsUpIndicator(((ImageView) firstChild).getDrawable());
continue;
}
Toolbar.LayoutParams params =
new Toolbar.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
@@ -201,39 +229,36 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
view.setLayoutParams(params);
if (view.getParent() == null) {
mToolbar.addView(view);
}
mToolbar.addView(view);
}
}
private void maybeUpdate() {
if (getParent() != null) {
if (getParent() != null && !mDestroyed) {
onUpdate();
}
}
public ScreenStackHeaderSubview getConfigSubview(int index) {
return mConfigSubviews[index];
return mConfigSubviews.get(index);
}
public int getConfigSubviewsCount() {
return mSubviewsCount;
return mConfigSubviews.size();
}
public void removeConfigSubview(int index) {
if (mConfigSubviews[index] != null) {
mSubviewsCount--;
}
mConfigSubviews[index] = null;
mConfigSubviews.remove(index);
maybeUpdate();
}
public void removeAllConfigSubviews() {
mConfigSubviews.clear();
maybeUpdate();
}
public void addConfigSubview(ScreenStackHeaderSubview child, int index) {
if (mConfigSubviews[index] == null) {
mSubviewsCount++;
}
mConfigSubviews[index] = child;
mConfigSubviews.add(index, child);
maybeUpdate();
}
@@ -258,7 +283,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mTitleFontFamily = titleFontFamily;
}
public void setTitleFontSize(int titleFontSize) {
public void setTitleFontSize(float titleFontSize) {
mTitleFontSize = titleFontSize;
}

View File

@@ -4,11 +4,12 @@ import android.view.View;
import com.facebook.react.bridge.JSApplicationCausedNativeException;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nonnull;
@ReactModule(name = ScreenStackHeaderConfigViewManager.REACT_CLASS)
public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenStackHeaderConfig> {
@@ -32,6 +33,16 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
parent.addConfigSubview((ScreenStackHeaderSubview) child, index);
}
@Override
public void onDropViewInstance(@Nonnull ScreenStackHeaderConfig view) {
view.destroy();
}
@Override
public void removeAllViews(ScreenStackHeaderConfig parent) {
parent.removeAllConfigSubviews();
}
@Override
public void removeViewAt(ScreenStackHeaderConfig parent, int index) {
parent.removeConfigSubview(index);
@@ -69,8 +80,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

@@ -18,7 +18,8 @@ public class ScreenStackHeaderSubview extends ReactViewGroup {
LEFT,
CENTER,
TITLE,
RIGHT
RIGHT,
BACK
}
private int mReactWidth, mReactHeight;

View File

@@ -47,6 +47,8 @@ public class ScreenStackHeaderSubviewManager extends ReactViewManager {
view.setType(ScreenStackHeaderSubview.Type.TITLE);
} else if ("right".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.RIGHT);
} else if ("back".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.BACK);
}
}
}

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';
@@ -13,6 +12,7 @@ import {
ScreenStack,
Screen,
ScreenStackHeaderConfig,
ScreenStackHeaderBackButtonImage,
ScreenStackHeaderLeftView,
ScreenStackHeaderRightView,
ScreenStackHeaderTitleView,
@@ -27,14 +27,14 @@ 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, descriptor) => {
descriptor.options && descriptor.options.onAppear && descriptor.options.onAppear()
this.props.navigation.dispatch(
StackActions.completeTransition({ toChildKey: route.key })
);
navigation.dispatch(StackActions.completeTransition());
};
_renderHeaderConfig = (index, route, descriptor) => {
@@ -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,
@@ -94,6 +95,15 @@ class StackView extends React.Component {
const children = [];
if (options.backButtonImage) {
children.push(
<ScreenStackHeaderBackButtonImage
key="backImage"
source={options.backButtonImage}
/>
);
}
if (options.headerLeft !== undefined) {
children.push(
<ScreenStackHeaderLeftView key="left">
@@ -154,21 +164,34 @@ 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 = mode;
if (transparentCard || options.cardTransparent) {
stackPresentation =
mode === 'containedModal'
? 'containedTransparentModal'
: 'transparentModal';
}
}
let stackAnimation = options.stackAnimation;
if (options.animationEnabled === false) {
stackAnimation = 'none';
}
const { screenProps } = this.props;
return (
<Screen
key={`screen_${route.key}`}
style={StyleSheet.absoluteFill}
style={options.cardStyle}
stackAnimation={stackAnimation}
stackPresentation={stackPresentation}
onAppear={() => this._onSceneFocus(route, descriptor)}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}
<SceneView
@@ -199,7 +222,6 @@ const styles = StyleSheet.create({
function createStackNavigator(routeConfigMap, stackConfig = {}) {
const router = StackRouter(routeConfigMap, stackConfig);
// Create a navigator with StackView as the view
let Navigator = createNavigator(StackView, router, stackConfig);
// if (!stackConfig.disableKeyboardHandling) {

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

@@ -22,6 +22,7 @@
NSMutableArray<RNSScreenView *> *_reactSubviews;
NSMutableSet<RNSScreenView *> *_dismissedScreens;
NSMutableArray<UIViewController *> *_presentedModals;
__weak UIViewController* recentPopped;
__weak RNSScreenStackManager *_manager;
}
@@ -69,6 +70,10 @@
[_dismissedScreens addObject:[_reactSubviews objectAtIndex:i - 1]];
}
}
if (recentPopped != nil) {
recentPopped.view = nil;
recentPopped = nil;
}
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
@@ -77,7 +82,8 @@
if (operation == UINavigationControllerOperationPush) {
screen = (RNSScreenView *) toVC.view;
} else if (operation == UINavigationControllerOperationPop) {
screen = (RNSScreenView *) fromVC.view;
screen = (RNSScreenView *) fromVC.view;
recentPopped = fromVC;
}
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
@@ -96,7 +102,16 @@
RCTRootContentView *rootView = (RCTRootContentView *)parent;
[rootView.touchHandler cancel];
return _controller.viewControllers.count > 1;
UIView *topView = _controller.viewControllers.lastObject.view;
RNSScreenStackHeaderConfig *config = nil;
for (UIView *subview in topView.reactSubviews) {
if ([subview isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
config = (RNSScreenStackHeaderConfig*) subview;
break;
}
}
return _controller.viewControllers.count > 1 && (config == nil || config.gestureEnabled);
}
- (void)markChildUpdated
@@ -137,41 +152,65 @@
- (void)setModalViewControllers:(NSArray<UIViewController *> *)controllers
{
// when there is no change we return immediately. This check is important because sometime we may
// accidently trigger modal dismiss if we don't verify to run the below code only when an actual
// change in the list of presented modal was made.
if ([_presentedModals isEqualToArray:controllers]) {
return;
}
NSMutableArray<UIViewController *> *newControllers = [NSMutableArray arrayWithArray:controllers];
[newControllers removeObjectsInArray:_presentedModals];
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;
}
};
if (changeRootController.presentedViewController) {
[changeRootController
dismissViewControllerAnimated:(changeRootIndex == controllers.count)
completion:finish];
} else {
finish();
}
[_presentedModals setArray:controllers];
}
- (void)setPushViewControllers:(NSArray<UIViewController *> *)controllers
{
// when there is no change we return immediately
if ([_controller.viewControllers isEqualToArray:controllers]) {
return;
}
UIViewController *top = controllers.lastObject;
UIViewController *lastTop = _controller.viewControllers.lastObject;
@@ -181,7 +220,7 @@
// controller is still there
BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *) lastTop.view).stackAnimation != RNSScreenStackAnimationNone && !_controller.presentedViewController;
if (firstTimePush) {
// nothing pushed yet
@@ -244,12 +283,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

@@ -34,6 +34,7 @@
@end
typedef NS_ENUM(NSInteger, RNSScreenStackHeaderSubviewType) {
RNSScreenStackHeaderSubviewTypeBackButton,
RNSScreenStackHeaderSubviewTypeLeft,
RNSScreenStackHeaderSubviewTypeRight,
RNSScreenStackHeaderSubviewTypeTitle,

View File

@@ -5,6 +5,20 @@
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTShadowView.h>
#import <React/RCTImageLoader.h>
#import <React/RCTImageView.h>
#import <React/RCTImageSource.h>
// Some RN private method hacking below. Couldn't figure out better way to access image data
// of a given RCTImageView. See more comments in the code section processing SubviewTypeBackButton
@interface RCTImageView (Private)
- (UIImage*)image;
@end
@interface RCTImageLoader (Private)
- (id<RCTImageCache>)imageCache;
@end
@interface RNSScreenHeaderItemMeasurements : NSObject
@property (nonatomic, readonly) CGSize headerSize;
@@ -30,6 +44,7 @@
@interface RNSScreenStackHeaderSubview : UIView
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, weak) UIView *reactSuperview;
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
@@ -150,6 +165,15 @@
[navbar setLargeTitleTextAttributes:largeAttrs];
}
}
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
if (backButtonImage) {
navbar.backIndicatorImage = backButtonImage;
navbar.backIndicatorTransitionMaskImage = backButtonImage;
} else if (navbar.backIndicatorImage) {
navbar.backIndicatorImage = nil;
navbar.backIndicatorTransitionMaskImage = nil;
}
}
}
@@ -164,6 +188,59 @@
}
}
+ (UIImage*)loadBackButtonImageInViewController:(UIViewController *)vc
withConfig:(RNSScreenStackHeaderConfig *)config
{
BOOL hasBackButtonImage = NO;
for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) {
hasBackButtonImage = YES;
RCTImageView *imageView = subview.subviews[0];
UIImage *image = imageView.image;
// IMPORTANT!!!
// image can be nil in DEV MODE ONLY
//
// It is so, because in dev mode images are loaded over HTTP from the packager. In that case
// we first check if image is already loaded in cache and if it is, we take it from cache and
// display immediately. Otherwise we wait for the transition to finish and retry updating
// header config.
// Unfortunately due to some problems in UIKit we cannot update the image while the screen
// transition is ongoing. This results in the settings being reset after the transition is done
// to the state from before the transition.
if (image == nil) {
// in DEV MODE we try to load from cache (we use private API for that as it is not exposed
// publically in headers).
RCTImageSource *source = imageView.imageSources[0];
image = [subview.bridge.imageLoader.imageCache
imageForUrl:source.request.URL.absoluteString
size:source.size
scale:source.scale
resizeMode:imageView.resizeMode];
}
if (image == nil) {
// This will be triggered if the image is not in the cache yet. What we do is we wait until
// the end of transition and run header config updates again. We could potentially wait for
// image on load to trigger, but that would require even more private method hacking.
if (vc.transitionCoordinator) {
[vc.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// nothing, we just want completion
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// in order for new back button image to be loaded we need to trigger another change
// in back button props that'd make UIKit redraw the button. Otherwise the changes are
// not reflected. Here we change back button visibility which is then immediately restored
vc.navigationItem.hidesBackButton = YES;
[config updateViewControllerIfNeeded];
}];
}
return [UIImage new];
} else {
return image;
}
}
}
return nil;
}
+ (void)willShowViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
[self updateViewController:vc withConfig:config];
@@ -190,7 +267,6 @@
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
navctr.interactivePopGestureRecognizer.enabled = config.gestureEnabled;
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.gestureEnabled;
@@ -201,7 +277,6 @@
}
navitem.title = config.title;
navitem.hidesBackButton = config.hideBackButton;
if (config.backTitle != nil) {
prevItem.backBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:config.backTitle
@@ -290,11 +365,22 @@
appearance.largeTitleTextAttributes = largeAttrs;
}
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
if (backButtonImage) {
[appearance setBackIndicatorImage:backButtonImage transitionMaskImage:backButtonImage];
} else if (appearance.backIndicatorImage) {
[appearance setBackIndicatorImage:nil transitionMaskImage:nil];
}
navitem.standardAppearance = appearance;
navitem.compactAppearance = appearance;
navitem.scrollEdgeAppearance = appearance;
}
#endif
navitem.hidesBackButton = config.hideBackButton;
navitem.leftBarButtonItem = nil;
navitem.rightBarButtonItem = nil;
navitem.titleView = nil;
for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
switch (subview.type) {
case RNSScreenStackHeaderSubviewTypeLeft: {
@@ -375,6 +461,7 @@ RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
@implementation RCTConvert (RNSScreenStackHeader)
RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
@"back": @(RNSScreenStackHeaderSubviewTypeBackButton),
@"left": @(RNSScreenStackHeaderSubviewTypeLeft),
@"right": @(RNSScreenStackHeaderSubviewTypeRight),
@"title": @(RNSScreenStackHeaderSubviewTypeTitle),
@@ -383,9 +470,7 @@ RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
@end
@implementation RNSScreenStackHeaderSubview {
__weak RCTBridge *_bridge;
}
@implementation RNSScreenStackHeaderSubview
- (instancetype)initWithBridge:(RCTBridge *)bridge
{

View File

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

125
src/screens.d.ts vendored
View File

@@ -3,21 +3,140 @@
declare module 'react-native-screens' {
import { ComponentClass } from 'react';
import { ViewProps, Animated } from 'react-native';
import { ViewProps, Animated, NativeSyntheticEvent, NativeTouchEvent, ImageProps } from 'react-native';
export function useScreens(shouldUseScreens?: boolean): void;
export function enableScreens(shouldEnableScreens?: boolean): void;
export function screensEnabled(): boolean;
export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal';
export type StackAnimationTypes = 'default' | 'fade' | 'flip' | 'none';
export interface ScreenProps extends ViewProps {
active?: 0 | 1 | Animated.AnimatedInterpolation;
onComponentRef?: (view: any) => void;
children?: React.ReactNode;
onAppear?: (e: NativeSyntheticEvent<NativeTouchEvent>) => void;
/**
*@description A callback that gets called when the current screen is dismissed by hardware back (on Android) or dismiss gesture (swipe back or down). The callback takes no arguments.
*/
onDismissed?: (e: NativeSyntheticEvent<NativeTouchEvent>) => void;
/**
* @type "push" the new screen will be pushed onto a stack which on iOS means that the default animation will be slide from the side, the animation on Android may vary depending on the OS version and theme.
* @type "modal" the new screen will be presented modally. In addition this allow for a nested stack to be rendered inside such screens
* @type "transparentModal" the new screen will be presented modally but in addition the second to last screen will remain attached to the stack container such that if the top screen is non opaque the content below can still be seen. If "modal" is used instead the below screen will get unmounted as soon as the transition ends.
*/
stackPresentation: StackPresentationTypes;
/**
*@description 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:
* @type "default" uses a platform default animation
* @type "fade" fades screen in or out
* @type "flip" flips the screen, requires stackPresentation: "modal" (iOS only)
* @type "none" the screen appears/dissapears without an animation
*/
stackAnimation?: StackAnimationTypes;
}
export const Screen: ComponentClass<ScreenProps>;
export type ScreenContainerProps = ViewProps;
export const ScreenContainer: ComponentClass<ScreenContainerProps>;
export interface ScreenStackProps extends ViewProps {
transitioning?: number;
progress?: number;
}
export interface ScreenStackHeaderConfigProps extends ViewProps {
/**
*@description String that representing screen title that will get rendered in the middle section of the header. On iOS the title is centered on the header while on Android it is aligned to the left and placed next to back button (if one is present).
*/
title?: string;
/**
*@description When set to true the header will be hidden while the parent Screen is on the top of the stack. The default value is false.
*/
hidden?: boolean;
/**
*@description Controls the color of items rendered on the header. This includes back icon, back text (iOS only) and title text. If you want the title to have different color use titleColor property.
*/
color?: string;
/**
*@description Customize font family to be used for the title.
*/
titleFontFamily?: string;
/**
*@description Customize the size of the font to be used for the title.
*/
titleFontSize?: number;
/**
*@description Allows for setting text color of the title.
*/
titleColor?: string;
/**
*@description Controlls the color of the navigation header.
*/
backgroundColor?: string;
/**
* @description Boolean that allows for disabling drop shadow under navigation header. The default value is true.
*/
hideShadow?: boolean;
/**
* @description If set to true the back button will not be rendered as a part of navigation header.
*/
hideBackButton?: boolean;
/**
* @description When set to false the back swipe gesture will be disabled when the parent Screen is on top of the stack. The default value is true.
*/
gestureEnabled?: boolean;
/**
* @host (iOS only)
* @description When set to true, it makes native navigation bar on iOS semi transparent with blur effect. It is a common way of presenting navigation bar introduced in iOS 11. The default value is false
*/
translucent?: boolean;
/**
* @host (iOS only)
* @description Allows for controlling the string to be rendered next to back button. By default iOS uses the title of the previous screen.
*/
backTitle?: string;
/**
* @host (iOS only)
* @description Allows for customizing font family to be used for back button title on iOS.
*/
backTitleFontFamily?: string;
/**
* @host (iOS only)
* @description Allows for customizing font size to be used for back button title on iOS.
*/
backTitleFontSize?: number;
/**
* @host (iOS only)
* @description When set to true it makes the title display using the large title effect.
*/
largeTitle?: boolean;
/**
* @host (iOS only)
* @description Customize font family to be used for the large title.
*/
largeTitleFontFamily?: string;
/**
* @host (iOS only)
* @description Customize the size of the font to be used for the large title.
*/
largeTitleFontSize?: number;
/**
* Pass HeaderLeft, HeaderRight and HeaderTitle
*/
children?: React.ReactNode;
}
export const Screen: ComponentClass<ScreenProps>;
export const ScreenContainer: ComponentClass<ScreenContainerProps>;
export const NativeScreen: ComponentClass<ScreenProps>;
export const NativeScreenContainer: ComponentClass<ScreenContainerProps>;
export const ScreenStack: ComponentClass<ScreenStackProps>;
export const ScreenStackHeaderBackButtonImage: ComponentClass<ImageProps>;
export const ScreenStackHeaderLeftView: ComponentClass<ViewProps>;
export const ScreenStackHeaderRightView: ComponentClass<ViewProps>;
export const ScreenStackHeaderTitleView: ComponentClass<ViewProps>;
export const ScreenStackHeaderConfig: ComponentClass<
ScreenStackHeaderConfigProps
>;
}

View File

@@ -4,6 +4,7 @@ import {
requireNativeComponent,
View,
UIManager,
Image,
StyleSheet,
} from 'react-native';
import { version } from 'react-native/Libraries/Core/ReactNativeVersion';
@@ -146,6 +147,14 @@ const styles = StyleSheet.create({
},
});
const ScreenStackHeaderBackButtonImage = props => (
<ScreensNativeModules.NativeScreenStackHeaderSubview
type="back"
style={styles.headerSubview}>
<Image resizeMode="center" fadeDuration={0} {...props} />
</ScreensNativeModules.NativeScreenStackHeaderSubview>
);
const ScreenStackHeaderRightView = props => (
<ScreensNativeModules.NativeScreenStackHeaderSubview
{...props}
@@ -198,6 +207,7 @@ module.exports = {
get ScreenStackHeaderSubview() {
return ScreensNativeModules.NativeScreenStackHeaderSubview;
},
ScreenStackHeaderBackButtonImage,
ScreenStackHeaderRightView,
ScreenStackHeaderLeftView,
ScreenStackHeaderTitleView,

View File

@@ -1741,10 +1741,10 @@ commander@~2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commondir@^1.0.1:
version "1.0.1"
@@ -2765,9 +2765,9 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
handlebars@^4.0.3:
version "4.1.2"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67"
integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==
version "4.5.3"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482"
integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==
dependencies:
neo-async "^2.6.0"
optimist "^0.6.1"
@@ -5855,11 +5855,11 @@ uglify-es@^3.1.9:
source-map "~0.6.1"
uglify-js@^3.1.4:
version "3.6.0"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5"
integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==
version "3.7.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.3.tgz#f918fce9182f466d5140f24bb0ff35c2d32dcc6a"
integrity sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==
dependencies:
commander "~2.20.0"
commander "~2.20.3"
source-map "~0.6.1"
ultron@1.0.x: