Compare commits

..

60 Commits

Author SHA1 Message Date
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
Krzysztof Magiera
124e8acb2d Bump version -> 2.0.0-alpha.12 2019-11-22 13:03:35 +01:00
Krzysztof Magiera
2c5f95cea6 Fix updating stack config props. (#231)
When some of config props change or new header items are added we need to perform a header update. This diff adds logic to trigger updating header props when that happens.
2019-11-22 13:03:11 +01:00
Janic Duplessis
0a2336d005 [native-stack] Add flip transition on iOS (#225) 2019-11-20 11:12:50 +01:00
Krzysztof Magiera
58d1791d4a Bump version -> 2.0.0-alpha.11 2019-11-14 09:16:31 +01:00
Krzysztof Magiera
3d56c5d4e2 Bump version -> 2.0.0-alpha.10 2019-11-14 09:16:05 +01:00
Krzysztof Magiera
47658d4d7d Fix issue with stack triggering back on reload on Android. (#223)
There was an issue with back stack listener being triggered after reload caused by the fact we weren't cleaning up stack manager's back stack on reload. As a result after reload listener would get triggered with and empty stack first and only then with a back stack with 1 item. Consequence of the first trigger was that we'd call dismiss and move back from the top screen which wasn't a desirable behavior.
2019-11-13 22:45:08 +01:00
Krzysztof Magiera
258ae419de Bump version -> 2.0.0-alpha.9 2019-11-13 19:47:57 +01:00
Krzysztof Magiera
62123f16f9 Fix screen layout with non-translucent header on iOS. (#222)
After #184 we no longer were acconting for the size of navigation bar when laying out screens on the stack. This was causing elements to be drawn under a non-translucent bar unless SafeAreaView's been used. As this seem not to be desirable in most of the cases (there is no way of seeing items rendered underneath non-translucent header) this change brings back the previous behavior. Instead of using manual method of calculating top inset as it's been done before we rely on edgesForExtendedLayout property of the view controller.
2019-11-13 19:47:37 +01:00
Krzysztof Magiera
a94424192b Fix iOS stack navigator restoring state. (#221)
When more than one screen is initially mounted we were only installing the top navigator with UINavController. This was cauing the back-handling logic to not work properly.
2019-11-13 18:30:30 +01:00
Krzysztof Magiera
752d6c0f04 Bump version -> 2.0.0-alpha.8 2019-11-06 22:04:14 +01:00
Krzysztof Magiera
a017713efc Fix screen container layout on Android. (#217)
This is a similar fix to the one already merged in #215 but for Android this time. We change default screen container to layout its direct children without using flexbox. This is to follow the same pattern as with native stack container.
2019-11-06 21:57:31 +01:00
Krzysztof Magiera
26384b625e Use fragment manager back stack to handle hw back button presses on Android. (#216)
Before this change we'd rely on new androidx OnBackPressedCallback mechanism. It turns out not to work well in a few cases. The biggest problem with it was that when registered it'd never allow for the hw back button press to fallback to a system default behaviour and instead would always "still" that event. After several attempts of trying to make it work I decided to revert back to a FragmentManager's default solution.

This change adds an ability to use FragmentManager's back stack API. There are also a few problems with it we need to workaround though. One of the problem is the fact that we can not modify back stack history. What we do because of that is that we try to keep at most one item on the back stack and reset it each time our native stack updates. In order for that to work we create a fake transaction that hides and shows the same screen that displays on top. Thanks to that when hw back is pressed the built in transaction rollback logic does nothing to the UI and allows us to handle back navigation using back stack change listener.
2019-11-06 21:54:19 +01:00
Krzysztof Magiera
5d5e8bfca6 Fix screen container after rnsscreen layout change. (#215)
For native stack we introduced a new way of layouting screen controllers. We no longer rely on flexbox to layout them so that we can use native stack to provide a correct dimensions and safe area paddings. Unfortunately this change broke the old screen container. With this commit we are adapting layout change to old screen container that will now layout its direct children on the native side by making them take up the whole space of the container.
2019-11-06 21:54:05 +01:00
Sunny Luo
2536837795 Fix compile error on xcode 10 (#212) 2019-11-05 14:05:23 +01:00
25 changed files with 855 additions and 222 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:
@@ -140,6 +140,7 @@ A callback that gets called when the current screen is dismissed by hardware bac
Allows for the customization of how the given screen should appear/dissapear when pushed or popped at the top of the stack. The followin values are currently supported:
- `"default"` uses a platform default animation
- `"fade"` fades screen in or out
- `"flip"` flips the screen, requires `stackPresentation: "modal"` (iOS only)
- `"none"` the screen appears/dissapears without an animation
#### `stackPresentation`

View File

@@ -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,11 +2,13 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.facebook.react.ReactRootView;
@@ -21,25 +23,53 @@ 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 ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
private final ChoreographerCompat.FrameCallback mFrameCallback = new ChoreographerCompat.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
updateIfNeeded();
}
};
private final Runnable mLayoutRunnable = new Runnable() {
@Override
public void run() {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
public ScreenContainer(Context context) {
super(context);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
// no-op
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).layout(0, 0, getWidth(), getHeight());
}
}
@Override
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
}
}
protected void markUpdated() {
@@ -75,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();
}
@@ -83,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
@@ -106,12 +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;
return ((FragmentActivity) context).getSupportFragmentManager();
}
protected FragmentTransaction getOrCreateTransaction() {
if (mCurrentTransaction == null) {
mCurrentTransaction = findRootFragmentActivity().getSupportFragmentManager().beginTransaction();
mCurrentTransaction = mFragmentManager.beginTransaction();
mCurrentTransaction.setReorderingAllowed(true);
}
return mCurrentTransaction;
@@ -119,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;
}
@@ -144,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();
}
@@ -155,6 +249,22 @@ 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
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}
private void updateIfNeeded() {

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();
@@ -43,4 +48,9 @@ public class ScreenContainerViewManager extends ViewGroupManager<ScreenContainer
public View getChildAt(ScreenContainer parent, int index) {
return parent.getScreenAt(index);
}
@Override
public boolean needsCustomLayoutForChildren() {
return true;
}
}

View File

@@ -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,8 @@ package com.swmansion.rnscreens;
import android.content.Context;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import java.util.ArrayList;
@@ -10,11 +12,33 @@ import java.util.Set;
public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
private static final String BACK_STACK_TAG = "RN_SCREEN_LAST";
private final ArrayList<ScreenStackFragment> mStack = new ArrayList<>();
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
private ScreenStackFragment mTopScreen = null;
private boolean mLayoutEnqueued = false;
private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
if (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.
dismiss(mTopScreen);
}
}
};
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);
@@ -45,39 +69,23 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).layout(0, 0, getWidth(), getHeight());
protected void onDetachedFromWindow() {
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();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0, size = getChildCount(); i < size; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}
private final Runnable mLayoutRunnable = new Runnable() {
@Override
public void run() {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
layout(getLeft(), getTop(), getRight(), getBottom());
}
};
@Override
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
}
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
}
@Override
@@ -87,6 +95,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
@@ -95,7 +114,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--) {
@@ -113,28 +136,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
@@ -172,8 +193,60 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
tryCommitTransaction();
if (mTopScreen != null) {
setupBackHandlerIfNeeded(mTopScreen);
}
for (ScreenStackFragment screen : mStack) {
screen.onStackUpdate();
}
}
/**
* The below method sets up fragment manager's back stack in a way that it'd trigger our back
* stack change listener when hw back button is clicked.
*
* Because back stack by default rolls back the transaction the stack entry is associated with we
* generate a "fake" transaction that hides and shows the top fragment. As a result when back
* stack entry is rolled back nothing happens and we are free to handle back navigation on our
* own in `mBackStackListener`.
*
* We pop that "fake" transaction each time we update stack and we add a new one in case the top
* screen is allowed to be dismised using hw back button. This way in the listener we can tell
* if back button was pressed based on the count of the items on back stack. We expect 0 items
* in case hw back is pressed becakse we try to keep the number of items at 1 by always resetting
* and adding new items. In case we don't add a new item to back stack we remove listener so that
* it does not get triggered.
*
* It is important that we don't install back handler when stack contains a single screen as in
* that case we want the parent navigator or activity handler to take over.
*/
private void setupBackHandlerIfNeeded(ScreenStackFragment topScreen) {
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);
if (!mDismissed.contains(screen)) {
firstScreen = screen;
break;
}
}
if (topScreen != firstScreen && topScreen.isDismissable()) {
mFragmentManager
.beginTransaction()
.hide(topScreen)
.show(topScreen)
.addToBackStack(BACK_STACK_TAG)
.setPrimaryNavigationFragment(topScreen)
.commitAllowingStateLoss();
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);
@@ -86,4 +84,23 @@ 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) {
return ((ScreenStackHeaderConfig) child).isDismissable();
}
return true;
}
}

View File

@@ -8,46 +8,37 @@ 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.activity.OnBackPressedCallback;
import androidx.appcompat.app.ActionBar;
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;
private boolean mIsAttachedToWindow = false;
private OnBackPressedCallback mBackCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
ScreenStack stack = getScreenStack();
Screen current = getScreen();
if (stack.getTopScreen() == current) {
stack.dismiss(getScreenFragment());
}
mBackCallback.remove();
}
};
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
@@ -73,6 +64,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
// no-op
}
public void destroy() {
mDestroyed = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -116,27 +111,20 @@ public class ScreenStackHeaderConfig extends ViewGroup {
return null;
}
public boolean isDismissable() {
return mGestureEnabled;
}
public void onUpdate() {
Screen parent = (Screen) getParent();
final ScreenStack stack = getScreenStack();
boolean isRoot = stack == null ? true : stack.getRootScreen() == parent;
boolean isTop = stack == null ? true : stack.getTopScreen() == parent;
// we need to clean up back handler especially in the case given screen is no longer on top
// because we don't want it to capture back event if it is not responsible for handling it
// as that would block other handlers from running
mBackCallback.remove();
if (!mIsAttachedToWindow || !isTop) {
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
return;
}
if (!isRoot && isTop && mGestureEnabled) {
Fragment fragment = getScreenFragment();
fragment.requireActivity().getOnBackPressedDispatcher().addCallback(fragment, mBackCallback);
mBackCallback.setEnabled(true);
}
if (mIsHidden) {
if (mToolbar.getParent() != null) {
getScreenFragment().removeToolbar();
@@ -194,10 +182,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);
@@ -221,32 +225,37 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
view.setLayoutParams(params);
if (view.getParent() == null) {
mToolbar.addView(view);
}
mToolbar.addView(view);
}
}
private void maybeUpdate() {
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();
}
private TextView getTitleTextView() {
@@ -270,7 +279,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);
@@ -52,6 +63,12 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
return true;
}
@Override
protected void onAfterUpdateTransaction(ScreenStackHeaderConfig parent) {
super.onAfterUpdateTransaction(parent);
parent.onUpdate();
}
@ReactProp(name = "title")
public void setTitle(ScreenStackHeaderConfig config, String title) {
config.setTitle(title);
@@ -63,8 +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

@@ -17,6 +17,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
RNSScreenStackAnimationDefault,
RNSScreenStackAnimationNone,
RNSScreenStackAnimationFade,
RNSScreenStackAnimationFlip,
};
@interface RCTConvert (RNSScreen)
@@ -38,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

@@ -8,33 +8,6 @@
#import <React/RCTShadowView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenFrameData : NSObject
@property (nonatomic, readonly) CGFloat rightInset;
@property (nonatomic, readonly) CGFloat topInset;
@property (nonatomic, readonly) CGFloat bottomInset;
@property (nonatomic, readonly) CGFloat leftInset;
@property (nonatomic, readonly) CGFloat navbarOffset;
- (instancetype)initWithInsets:(UIEdgeInsets)insets;
@end
@implementation RNSScreenFrameData
- (instancetype)initWithInsets:(UIEdgeInsets)insets andNavbarOffset:(CGFloat)navbarOffset
{
if (self = [super init]) {
_topInset = insets.top;
_bottomInset = insets.bottom;
_leftInset = insets.left;
_rightInset = insets.right;
_navbarOffset = navbarOffset;
}
return self;
}
@end
@interface RNSScreenView () <UIAdaptivePresentationControllerDelegate>
@end
@@ -60,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
}
@@ -122,6 +98,9 @@
case RNSScreenStackAnimationFade:
_controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
break;
case RNSScreenStackAnimationFlip:
_controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
break;
case RNSScreenStackAnimationNone:
case RNSScreenStackAnimationDefault:
// Default
@@ -138,6 +117,8 @@
{
if (![view isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
[super addSubview:view];
} else {
((RNSScreenStackHeaderConfig*) view).screenView = self;
}
}
@@ -157,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) {
@@ -257,6 +249,12 @@
}
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[((RNSScreenView *)self.view) notifyAppear];
}
- (void)notifyFinishTransitioning
{
[_previousFirstResponder becomeFirstResponder];
@@ -280,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
@@ -302,8 +301,10 @@ RCT_ENUM_CONVERTER(RNSScreenStackPresentation, (@{
RCT_ENUM_CONVERTER(RNSScreenStackAnimation, (@{
@"default": @(RNSScreenStackAnimationDefault),
@"none": @(RNSScreenStackAnimationNone),
@"fade": @(RNSScreenStackAnimationFade)
@"fade": @(RNSScreenStackAnimationFade),
@"flip": @(RNSScreenStackAnimationFlip),
}), RNSScreenStackAnimationDefault, integerValue)
@end

View File

@@ -54,6 +54,7 @@
{
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
}
- (void)removeReactSubview:(RNSScreenView *)subview
@@ -159,6 +160,10 @@
[super layoutSubviews];
[self reactAddControllerToClosestParent:_controller];
_controller.view.frame = self.bounds;
for (RNSScreenView *subview in _reactSubviews) {
subview.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
[subview setNeedsLayout];
}
}
@end

View File

@@ -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,9 +82,10 @@
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 != RNSScreenStackAnimationDefault) {
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
}
return nil;
@@ -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,11 +220,11 @@
// 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
[_controller setViewControllers:@[top] animated:NO];
[_controller setViewControllers:controllers animated:NO];
} else if (top != lastTop) {
if (![controllers containsObject:lastTop]) {
// last top controller is no longer on stack
@@ -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

@@ -1,8 +1,12 @@
#import <React/RCTViewManager.h>
#import <React/RCTConvert.h>
#import "RNSScreen.h"
@interface RNSScreenStackHeaderConfig : UIView
@property (nonatomic, weak) RNSScreenView *screenView;
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *titleFontFamily;
@property (nonatomic, retain) NSNumber *titleFontSize;
@@ -30,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;
@@ -66,13 +81,36 @@
return _reactSubviews;
}
- (UIViewController*)screen
- (UIView *)reactSuperview
{
UIView *superview = self.superview;
if ([superview isKindOfClass:[RNSScreenView class]]) {
return ((RNSScreenView *)superview).controller;
return _screenView;
}
- (void)removeFromSuperview
{
[super removeFromSuperview];
_screenView = nil;
}
- (void)updateViewControllerIfNeeded
{
UIViewController *vc = _screenView.controller;
UINavigationController *nav = (UINavigationController*) vc.parentViewController;
if (vc != nil && nav.visibleViewController == vc) {
[RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self];
}
return nil;
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
[self updateViewControllerIfNeeded];
}
- (void)didUpdateReactSubviews
{
[super didUpdateReactSubviews];
[self updateViewControllerIfNeeded];
}
+ (void)setAnimatedConfig:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
@@ -127,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;
}
}
}
@@ -141,7 +188,65 @@
}
}
+ (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];
}
+ (void)updateViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config
{
UINavigationItem *navitem = vc.navigationItem;
UINavigationController *navctr = (UINavigationController *)vc.parentViewController;
@@ -152,8 +257,16 @@
BOOL wasHidden = navctr.navigationBarHidden;
BOOL shouldHide = config == nil || config.hide;
if (!shouldHide && !config.translucent) {
// when nav bar is not translucent we chage edgesForExtendedLayout to avoid system laying out
// the screen underneath navigation controllers
vc.edgesForExtendedLayout = UIRectEdgeNone;
} else {
// system default is UIRectEdgeAll
vc.edgesForExtendedLayout = UIRectEdgeAll;
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
navctr.interactivePopGestureRecognizer.enabled = config.gestureEnabled;
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.gestureEnabled;
@@ -164,7 +277,6 @@
}
navitem.title = config.title;
navitem.hidesBackButton = config.hideBackButton;
if (config.backTitle != nil) {
prevItem.backBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:config.backTitle
@@ -191,7 +303,7 @@
}
navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever;
}
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
UINavigationBarAppearance *appearance = [UINavigationBarAppearance new];
@@ -253,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: {
@@ -338,6 +461,7 @@ RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
@implementation RCTConvert (RNSScreenStackHeader)
RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
@"back": @(RNSScreenStackHeaderSubviewTypeBackButton),
@"left": @(RNSScreenStackHeaderSubviewTypeLeft),
@"right": @(RNSScreenStackHeaderSubviewTypeRight),
@"title": @(RNSScreenStackHeaderSubviewTypeTitle),
@@ -346,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.7",
"version": "2.0.0-alpha.26",
"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: