Compare commits

..

41 Commits

Author SHA1 Message Date
Krzysztof Magiera
bdf610bcad Bump version -> 2.0.0-beta.3 2020-02-14 15:13:53 +01:00
Krzysztof Magiera
102880c18b Trigger transition finish event from native stack (#330)
This change adds new event that gets dispatched from native stack when screen transitioning is finished.

The new event is used by react-navigation to update the library internal state that the triggered action has been finished. Previously we were relying solely on onAppear and onDisappear events, however those does not get triggered when new iOS 13 modals are used or when transparent modals are displayed. This is because with transparent modals the view below is still visible. We therefore needed another way of notifying the library that screen transition have finished despite the fact that disappear event couldn't be triggered.

As a part of this change I also refactored invalid ref cycle-break code on iOS which was ought to remove reference cycle between view and view controller. This code have been moved to viewWillDisappear callback.

Also on Android part small refactoring has been done and we removed the necessity to keep mActiveScreens array which was occasionally getting out of sync with the list of active fragments.
2020-02-14 15:13:00 +01:00
Matt Oakes
24b70abd64 fix(android): NullPointerException crash due to null top screen (#323)
The library would crash when the `mTopScreen` instance variable is null. This change checkes if it null before calling the `getScreen()` method. It also marks the method as having a nullable return value. The only place this method is used is already setup to handle `null` responses.
2020-02-11 23:35:10 +01:00
Krzysztof Magiera
f7b6c22591 Force reload back indicator image view when image is nil. (#324)
This fixes a problem when image attribute of image view would get reset and we wouldn't be able to access it even in release mode. This could happen when the app is backgrounded as at that point image views release their resources. Another case that was quite frequent was that the image would start loading before the image view frame was properly set. The solution is to trigger image reload in cases when image is missing and to force the image view frame to match the size of an image.

This change also fixes a problem on iOS pre 13 where we were updating back indicator inside of the method which would run during the transition. Turns out updating this property isn't supported during animation and so we moved it out such that it is updated before the transition starts.
2020-02-11 23:33:41 +01:00
Krzysztof Magiera
ed997ef4ec Skip saving/restoring state for unmounted fragments. (#313)
We currently cannot take any adventage of saving and restoring fragment state because view hierarchy of the unmounted fragment is retained by RN core anyways. Skipping sabe/restore will allow us to avoid unnecessary serializiation/deserialization of the view hierarchy but will also help circumvent some bugs which comes from the side effects of restoring. One of the bugs have been reported in #162 and in that particular case restoring result in InputText change event being triggered with stale value. This was happening in the situation when we were going back to a screen with InputText component.
2020-02-06 20:17:02 +01:00
Krzysztof Magiera
10a0badee2 Fix text selection inside screen container on Android. (#310)
This change fixes the problem with missing text selection modals when text input component is rendered under screen container. The reason turned out to be a problem with Android TextView implementation which expect the component to be attached to window at the moment when layout happens (see Editor.java#prepareCursorControllers and TextView.java$makeNewLayout). Apparently, within screens container layout will happen before the component of a given screen are attached (because attaching is managed by the fragment manager which delays the moment at which the screen is attached). As a result the textView component does not get a chance to fetch proper window layout params such that it can tell whether it should support selection mode or not. We workaround this issue by providing window layout params directly from Screen class which is used instead of the DecorWindow in case TextView is not yet attached (it searches for the top-most parent when not attached).
2020-02-05 23:18:30 +01:00
Krzysztof Magiera
fd7fcfbc5b Bump version -> 2.0.0-beta.2 2020-02-05 16:22:32 +01:00
Krzysztof Magiera
06b928f5c1 Fix showing sw back button on nested stack on Android. (#308)
This change fixes a problem with native stack on Android where we'd display a sw back button on screens that are nested stack navigatodespite the fact those screens were the initial screens in the whole container stack. In #306 we introuced a change that would allow for displaying sw back in nested stacks, however that change did not handle the case where screen is a root screen of a container that is nested and placed as an initial screen in another container.
2020-02-05 16:22:10 +01:00
Krzysztof Magiera
acb038e213 Upgrading to beta -> 2.0.0-beta.1 2020-02-04 22:21:50 +01:00
Krzysztof Magiera
22d4400b93 Render Android back button for root screens of nested stack navi… (#306)
This change allows for root screens of nested stack navigators on Android to display back button in the navigation bar. Navigating back is still possible using hw back because of the way hw back is handled by nested fragment containers. However, despite hw back functioning properly, before this change we would not allow for the soft back button to be rendered in the header. This change adds this possibility to keep software back consistent with the hw back button functionality. This behavior can still be disabled/adjusted using hideBackButton property of the header config component.
2020-02-04 22:20:41 +01:00
Krzysztof Magiera
f78c264419 Fix updating navigation bar for modal transitions. (#305)
This change fixes the problem when navigation bar would not get properly updated for modal transitions. The problem was due to the fact that for modal transitions the animateAlongsideTransition block was not getting trriggered. This was noticeable on iOS versions before 13, as starting with iOS 13 we can use NavigationItem to specify most of the nav bar customization. On older versions however it is required that for a lot of navbar settings we need to update them durion animation. This wasn't the case for navigators presented modally. In case we show a modal we can update nav bar without animation because it actually renders a completely new navigation bar which isn't shared.

On top of that this PR fixes the problem with bridge not being set for header subviews.
2020-02-04 16:45:55 +01:00
Krzysztof Magiera
65bd436cbe Bump version -> 2.0.0-alpha.34 2020-02-03 21:12:27 +01:00
Krzysztof Magiera
a0745762ba Remove references to ScreenStackHeaderTitleView. 2020-02-03 16:30:31 +01:00
Krzysztof Magiera
89658d9361 Delete Title subview and code for handling title view scaling. (#304)
This change removes ScreenStackHeaderTitleView component and code that used to handle title view scaling. There were multiple issues related to scaling toolbar title views and it is not a priority at the moment to us to work on addressing those. The most frequent usecase is to put a fixed-dimensions view (e.g. logo or text) which can be now handled w/o this extra code.
2020-02-03 16:27:00 +01:00
osdnk
074aabc33e Bump version -> 2.0.0-alpha.33 2020-02-02 17:09:15 +01:00
Satyajit Sahoo
eb2a1ae8fd Fix type definition for gestureEnabled (#302) 2020-01-29 23:51:32 +01:00
Krzysztof Magiera
9721353e7b Bump version -> 2.0.0-alpha.32 2020-01-28 11:27:05 +01:00
Krzysztof Magiera
523b3d8f3a Fix native stack navigator wrapper after #254 (#299) 2020-01-27 19:26:51 +01:00
Krzysztof Magiera
f851c26642 Bump version -> 2.0.0-alpha.31 2020-01-27 15:46:49 +01:00
Krzysztof Magiera
f8f5d66bf9 Fix updating native header on iOS while transition is ongoing. (#298)
This change fixes a bug when header property updates happens during or at the same time of stack transition, e.g., when we both pop a screen and change parent VC header properties. When this happens we trigger header update code, but because we used to compare current VC to visibleViewController, which at that moment points to controller that we are transitioning away from, the update would've been prevented. With this change instead of using visibleViewController to compare with updated VC, we check if there is transition happening (using transitransitionCoordinator property), and in such case we use topController which points to the controller we are transitioning to.
2020-01-27 15:34:40 +01:00
osdnk
72037e20fb Bump version -> 2.0.0-alpha.30 2020-01-27 15:32:48 +01:00
Michał Osadnik
69a23f1c9f Remove blocking touch interactions if more than one active scree… (#296)
We believe that it might be better to manage the logic of blocking interactions from JS with setNativeProps and pointerEvents.

The current logic leads to some issues we're facing within React Navigation. There might be a state when more that one screen can be active. E.g. in modals we want to have two screens visible on the stack and have enabled interaction on the bottom one.

New code should not give any issues, because even though we need to handle activation of each screen from JS and this will just introduce an additional native call for enabling / disabling pointer events.

On iOS, we're not using React Native Screens in React Navigation for now and we'd like to focus on it later because right now we're not observing that crucial performance issues.
2020-01-27 11:07:07 +01:00
Janic Duplessis
d32463ee83 Move gestureEnabled config to screen instead of heade… (#254)
When you have 2 screens in a stack with the bottom one with gestureEnabled=false using the back gesture causes the screen to become unresponsive. This seems to be caused by setting interactivePopGestureRecognizer.enabled = NO as soon as the gesture starts. This causes the gesture to cancel immediately and leaves the screen in an unresponsive state.

<Stack><Screen gestureEnabled={false} /><Screen /></Stack>
To fix this instead of using interactivePopGestureRecognizer.enabled we can leverage the existing delegate that we have in RNScreenStack. In gestureRecognizerShouldBegin we can check if the top screen has gestures enabled.

To make this simpler I moved the gestureEnabled config to Screen instead of HeaderConfig. I think it also makes more sense conceptually since the gesture is tied to the screen and not the header. It also simplifies the android code a bit.

This is a breaking change.

Update

This now only moves the config to screen since a separate fix was merged for the bug.
2020-01-24 01:19:38 +01:00
Satyajit Sahoo
2da04f37e6 Fix typo in onDismissed prop name (#288)
The prop is called `onDismissed` according to the code

e00a08a3dc/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.java (L65)
e00a08a3dc/ios/RNSScreen.h (L43)
2020-01-24 01:18:26 +01:00
Krzysztof Magiera
623f9452cb Bump version -> 2.0.0-alpha.29 2020-01-22 21:56:21 +01:00
Krzysztof Magiera
c646a4e7ba Fix rendering title view in native header on Android. (#294)
This change removes the behavior that's been added for an unclear reason. We may consider reverting this one, however I was unable to reproduce a problem when setting header title view layout measurements was necessary. To the contrary it was causing issues in the case when the title header view updates. In such a case we would never update width/height so the updated view might be cropped. In addition we are changing the way we schedule native layout updates. Previously we'd use handler.post but it was causing one frame delay so this change migrates us to use choreographer.
2020-01-22 21:52:56 +01:00
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
25 changed files with 497 additions and 376 deletions

View File

@@ -219,7 +219,7 @@ PODS:
- ReactCommon/jscallinvoker (= 0.61.2)
- RNGestureHandler (1.3.0):
- React
- RNScreens (2.0.0-alpha.3):
- RNScreens (2.0.0-alpha.23):
- React
- Yoga (1.14.0)
@@ -256,7 +256,7 @@ DEPENDENCIES:
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
https://github.com/cocoapods/specs.git:
https://github.com/CocoaPods/Specs.git:
- boost-for-react-native
EXTERNAL SOURCES:
@@ -342,9 +342,9 @@ SPEC CHECKSUMS:
React-RCTVibration: fb54c732fd20405a76598e431aa2f8c2bf527de9
ReactCommon: 5848032ed2f274fcb40f6b9ec24067787c42d479
RNGestureHandler: 5329a942fce3d41c68b84c2c2276ce06a696d8b0
RNScreens: a32a406ec4884f0ba383ef835332e8379f75e3da
RNScreens: 55c735f525774e894be67848c250c95a9c3194c0
Yoga: 14927e37bd25376d216b150ab2a561773d57911f
PODFILE CHECKSUM: 1a141b811c7076eb11c48f2e22336181f52531b5
COCOAPODS: 1.7.3
COCOAPODS: 1.8.4

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/>`
@@ -131,7 +131,7 @@ Screen stack component expects one or more `Screen` components as direct childre
`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:
#### `onDismiss`
#### `onDismissed`
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.
@@ -155,9 +155,8 @@ Defines how the method that should be used to present the given screen. It is a
The config component is expected to be rendered as a direct children of `<Screen>`. It provides an ability to configure native navigation header that gets rendered as a part of native screen stack. The component acts as a "virtual" element that is not directly rendered under `Screen`. You can use its properties to customize platform native header for the parent screen and also render react-native components that you'd like to be displayed inside the header (e.g. in the title are or on the side).
Along with this component properties that can be used to customize header behavior one can also use one or the below component containers to render custom react-native content in different areas of the native header:
- `ScreenStackHeaderTitleView` react native views rendered as children will appear on the navigation bar in the place of title. Note that title is aligned next to back button on Android while it is centered on iOS.
- `ScreenStackHeaderCenterView` the childern will render in the center of the native navigation bar (on iOS this has the same behavior as `ScreenStackHeaderTitleView` container)
- `ScreenStackHeaderRightView` the children will render on the right hand side of the navigation bar (or on the left hand side in case LTR locales are set on the user's device)
- `ScreenStackHeaderCenterView` the childern will render in the center of the native navigation bar.
- `ScreenStackHeaderRightView` the children will render on the right hand side of the navigation bar (or on the left hand side in case LTR locales are set on the user's device).
- `ScreenStackHeaderLeftView` the children will render on the left hand side of the navigation bar (or on the right hand side in case LTR locales are set on the user's device).
Below is a list of properties that can be set with `ScreenStackHeaderConfig` component:

View File

@@ -2,21 +2,21 @@ package com.swmansion.rnscreens;
import android.content.Context;
import android.graphics.Paint;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.uimanager.UIManagerModule;
public class Screen extends ViewGroup implements ReactPointerEventsView {
public class Screen extends ViewGroup {
public enum StackPresentation {
PUSH,
@@ -46,15 +46,47 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
}
};
private @Nullable Fragment mFragment;
private @Nullable ScreenFragment mFragment;
private @Nullable ScreenContainer mContainer;
private boolean mActive;
private boolean mTransitioning;
private StackPresentation mStackPresentation = StackPresentation.PUSH;
private StackAnimation mStackAnimation = StackAnimation.DEFAULT;
private boolean mGestureEnabled = true;
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
if (mFragment != null) {
mFragment.onViewAnimationEnd();
}
}
public Screen(ReactContext context) {
super(context);
// we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
// not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
// fact that TextView implementation is expected to be attached to window when layout happens.
// Then, at the moment of layout it checks whether window type is in a reasonable range to tell
// whether it should enable selection controlls (see Editor.java#prepareCursorControllers).
// With screens, however, the text input component can be laid out before it is attached, in that
// case TextView tries to get window type property from the oldest existing parent, which in this
// case is a Screen class, as it is the root of the screen that is about to be attached. Setting
// params this way is not the most elegant way to solve this problem but workarounds it for the
// time being
setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION));
}
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
// do nothing, react native will keep the view hierarchy so no need to serialize/deserialize
// view's states. The side effect of restoring is that TextInput components would trigger set-text
// events which may confuse text input handling.
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
// ignore restoring instance state too as we are not saving anything anyways.
}
@Override
@@ -122,6 +154,10 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
mStackAnimation = stackAnimation;
}
public void setGestureEnabled(boolean gestureEnabled) {
mGestureEnabled = gestureEnabled;
}
public StackAnimation getStackAnimation() {
return mStackAnimation;
}
@@ -130,11 +166,6 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
return mStackPresentation;
}
@Override
public PointerEvents getPointerEvents() {
return mTransitioning ? PointerEvents.NONE : PointerEvents.AUTO;
}
@Override
public void setLayerType(int layerType, @Nullable Paint paint) {
// ignore - layer type is controlled by `transitioning` prop
@@ -144,11 +175,11 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
mContainer = container;
}
protected void setFragment(Fragment fragment) {
protected void setFragment(ScreenFragment fragment) {
mFragment = fragment;
}
protected @Nullable Fragment getFragment() {
protected @Nullable ScreenFragment getFragment() {
return mFragment;
}
@@ -169,4 +200,8 @@ public class Screen extends ViewGroup implements ReactPointerEventsView {
public boolean isActive() {
return mActive;
}
public boolean isGestureEnabled() {
return mGestureEnabled;
}
}

View File

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -22,17 +23,15 @@ import java.util.Set;
public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected final ArrayList<T> mScreenFragments = new ArrayList<>();
private final Set<ScreenFragment> mActiveScreenFragments = new HashSet<>();
private final ArrayList<Runnable> mAfterTransitionRunnables = new ArrayList<>(1);
private @Nullable FragmentManager mFragmentManager;
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() {
@Override
public void doFrame(long frameTimeNanos) {
@@ -40,9 +39,9 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
}
};
private final Runnable mLayoutRunnable = new Runnable() {
private final ChoreographerCompat.FrameCallback mLayoutCallback = new ChoreographerCompat.FrameCallback() {
@Override
public void run() {
public void doFrame(long frameTimeNanos) {
mLayoutEnqueued = false;
measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
@@ -66,9 +65,13 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
public void requestLayout() {
super.requestLayout();
if (!mLayoutEnqueued) {
if (!mLayoutEnqueued && mLayoutCallback != null) {
mLayoutEnqueued = true;
post(mLayoutRunnable);
// we use NATIVE_ANIMATED_MODULE choreographer queue because it allows us to catch the current
// looper loop instead of enqueueing the update in the next loop causing a one frame delay.
ReactChoreographer.getInstance().postFrameCallback(
ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
mLayoutCallback);
}
}
@@ -113,36 +116,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
markUpdated();
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mIsTransitioning = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mIsTransitioning) {
mIsTransitioning = false;
notifyTransitionFinished();
}
}
public boolean isTransitioning() {
return mIsTransitioning || mProcessingTransaction != null;
}
public void postAfterTransition(Runnable runnable) {
mAfterTransitionRunnables.add(runnable);
}
protected void notifyTransitionFinished() {
for (int i = 0, size = mAfterTransitionRunnables.size(); i < size; i++) {
mAfterTransitionRunnables.get(i).run();
}
mAfterTransitionRunnables.clear();
}
protected int getScreenCount() {
return mScreenFragments.size();
}
@@ -184,16 +157,9 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return ((FragmentActivity) context).getSupportFragmentManager();
}
protected final FragmentManager getFragmentManager() {
if (mFragmentManager == null) {
mFragmentManager = findFragmentManager();
}
return mFragmentManager;
}
protected FragmentTransaction getOrCreateTransaction() {
if (mCurrentTransaction == null) {
mCurrentTransaction = getFragmentManager().beginTransaction();
mCurrentTransaction = mFragmentManager.beginTransaction();
mCurrentTransaction.setReorderingAllowed(true);
}
return mCurrentTransaction;
@@ -221,7 +187,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private void attachScreen(ScreenFragment screenFragment) {
getOrCreateTransaction().add(getId(), screenFragment);
mActiveScreenFragments.add(screenFragment);
}
private void moveToFront(ScreenFragment screenFragment) {
@@ -232,7 +197,6 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
private void detachScreen(ScreenFragment screenFragment) {
getOrCreateTransaction().remove(screenFragment);
mActiveScreenFragments.remove(screenFragment);
}
protected boolean isScreenActive(ScreenFragment screenFragment) {
@@ -243,24 +207,12 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
return mScreenFragments.contains(screenFragment);
}
protected void ensureFragmentManager() {
if (mFragmentManager != null && mFragmentManager.isDestroyed()) {
// When fragmentManager is destroyed, try to remove current fragment's views
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
removeView(screenFragment.getScreenRootView());
}
mFragmentManager = null;
mActiveScreenFragments.clear();
mNeedUpdate = true;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttached = true;
ensureFragmentManager();
mNeedUpdate = true;
mFragmentManager = findFragmentManager();
updateIfNeeded();
}
@@ -268,6 +220,13 @@ 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();
// after re-attach we'll update the screen and add views again
markUpdated();
}
@Override
@@ -288,11 +247,11 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
protected void onUpdate() {
// detach screens that are no longer active
Set<ScreenFragment> orphaned = new HashSet<>(mActiveScreenFragments);
Set<Fragment> orphaned = new HashSet<>(mFragmentManager.getFragments());
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
boolean isActive = isScreenActive(screenFragment);
if (!isActive && mActiveScreenFragments.contains(screenFragment)) {
if (!isActive && screenFragment.isAdded()) {
detachScreen(screenFragment);
}
orphaned.remove(screenFragment);
@@ -318,7 +277,7 @@ public class ScreenContainer<T extends ScreenFragment> extends ViewGroup {
for (int i = 0, size = mScreenFragments.size(); i < size; i++) {
ScreenFragment screenFragment = mScreenFragments.get(i);
boolean isActive = isScreenActive(screenFragment);
if (isActive && !mActiveScreenFragments.contains(screenFragment)) {
if (isActive && !screenFragment.isAdded()) {
addedBefore = true;
attachScreen(screenFragment);
} else if (isActive && addedBefore) {

View File

@@ -1,13 +1,11 @@
package com.swmansion.rnscreens;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
@@ -35,10 +33,6 @@ public class ScreenFragment extends Fragment {
return mScreenView;
}
protected ViewGroup getScreenRootView() {
return mScreenView;
}
public Screen getScreen() {
return mScreenView;
}
@@ -51,19 +45,15 @@ public class ScreenFragment extends Fragment {
}
@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();
}
public void onResume() {
super.onResume();
}
public void onViewAnimationEnd() {
// onViewAnimationEnd is triggered from View#onAnimationEnd method of the fragment's root view.
// We override Screen#onAnimationEnd and an appropriate method of the StackFragment's root view
// in order to achieve this.
dispatchOnAppear();
}
@Override

View File

@@ -1,11 +1,15 @@
package com.swmansion.rnscreens;
import android.content.Context;
import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
@@ -18,11 +22,12 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
private final Set<ScreenStackFragment> mDismissed = new HashSet<>();
private ScreenStackFragment mTopScreen = null;
private boolean mRemovalTransitionStarted = false;
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.
@@ -50,7 +55,7 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
}
public Screen getTopScreen() {
return mTopScreen.getScreen();
return mTopScreen != null ? mTopScreen.getScreen() : null;
}
public Screen getRootScreen() {
@@ -70,24 +75,52 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
FragmentManager fm = getFragmentManager();
fm.removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
if (!fm.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.
fm.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
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();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
}
@Override
public void startViewTransition(View view) {
super.startViewTransition(view);
mRemovalTransitionStarted = true;
}
@Override
public void endViewTransition(View view) {
super.endViewTransition(view);
if (mRemovalTransitionStarted) {
mRemovalTransitionStarted = false;
dispatchOnFinishTransitioning();
}
}
public void onViewAppearTransitionEnd() {
if (!mRemovalTransitionStarted) {
dispatchOnFinishTransitioning();
}
}
private void dispatchOnFinishTransitioning() {
((ReactContext) getContext())
.getNativeModule(UIManagerModule.class)
.getEventDispatcher()
.dispatchEvent(new StackFinishTransitioningEvent(getId()));
}
@Override
@@ -230,8 +263,8 @@ public class ScreenStack extends ScreenContainer<ScreenStackFragment> {
// notified when it gets resumed so that we can install the handler.
return;
}
getFragmentManager().removeOnBackStackChangedListener(mBackStackListener);
getFragmentManager().popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
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);
@@ -241,14 +274,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

@@ -1,22 +1,48 @@
package com.swmansion.rnscreens;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment;
import com.facebook.react.uimanager.PixelUtil;
import com.google.android.material.appbar.AppBarLayout;
public class ScreenStackFragment extends ScreenFragment {
private static class NotifyingCoordinatorLayout extends CoordinatorLayout {
private final ScreenFragment mFragment;
public NotifyingCoordinatorLayout(@NonNull Context context, ScreenFragment fragment) {
super(context);
mFragment = fragment;
}
@Override
public void startAnimation(Animation animation) {
super.startAnimation(animation);
}
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
mFragment.onViewAnimationEnd();
}
}
private static final float TOOLBAR_ELEVATION = PixelUtil.toPixelFromDIP(4);
private AppBarLayout mAppBarLayout;
@@ -61,7 +87,7 @@ public class ScreenStackFragment extends ScreenFragment {
}
private CoordinatorLayout configureView() {
CoordinatorLayout view = new CoordinatorLayout(getContext());
CoordinatorLayout view = new NotifyingCoordinatorLayout(getContext(), this);
CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);
params.setBehavior(new AppBarLayout.ScrollingViewBehavior());
@@ -85,6 +111,30 @@ public class ScreenStackFragment extends ScreenFragment {
return view;
}
@Override
public void onViewAnimationEnd() {
super.onViewAnimationEnd();
notifyViewAppearTransitionEnd();
}
@Nullable
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (enter && transit == 0) {
// this means that the fragment will appear without transition, in this case onViewAnimationEnd
// won't be called and we need to notify stack directly from here.
notifyViewAppearTransitionEnd();
}
return null;
}
private void notifyViewAppearTransitionEnd() {
ViewParent screenStack = getView().getParent();
if (screenStack instanceof ScreenStack) {
((ScreenStack) screenStack).onViewAppearTransitionEnd();
}
}
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@@ -96,16 +146,36 @@ public class ScreenStackFragment extends ScreenFragment {
return mScreenRootView;
}
@Override
protected ViewGroup getScreenRootView() {
return mScreenRootView;
public boolean isDismissable() {
return mScreenView.isGestureEnabled();
}
public boolean isDismissable() {
View child = mScreenView.getChildAt(0);
if (child instanceof ScreenStackHeaderConfig) {
return ((ScreenStackHeaderConfig) child).isDismissable();
public boolean canNavigateBack() {
ScreenContainer container = mScreenView.getContainer();
if (container instanceof ScreenStack) {
if (((ScreenStack) container).getRootScreen() == getScreen()) {
// this screen is the root of the container, if it is nested we can check parent container
// if it is also a root or not
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof ScreenStackFragment) {
return ((ScreenStackFragment) parentFragment).canNavigateBack();
} else {
return false;
}
} else {
return true;
}
} else {
throw new IllegalStateException("ScreenStackFragment added into a non-stack container");
}
}
public void dismiss() {
ScreenContainer container = mScreenView.getContainer();
if (container instanceof ScreenStack) {
((ScreenStack) container).dismiss(this);
} else {
throw new IllegalStateException("ScreenStackFragment added into a non-stack container");
}
return true;
}
}

View File

@@ -30,9 +30,9 @@ public class ScreenStackHeaderConfig extends ViewGroup {
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;
@@ -41,7 +41,16 @@ public class ScreenStackHeaderConfig extends ViewGroup {
private OnClickListener mBackClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
getScreenStack().dismiss(getScreenFragment());
ScreenStack stack = getScreenStack();
ScreenStackFragment fragment = getScreenFragment();
if (stack.getRootScreen() == fragment.getScreen()) {
Fragment parentFragment = fragment.getParentFragment();
if (parentFragment instanceof ScreenStackFragment) {
((ScreenStackFragment) parentFragment).dismiss();
}
} else {
fragment.dismiss();
}
}
};
@@ -50,6 +59,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();
@@ -63,6 +76,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
// no-op
}
public void destroy() {
mDestroyed = true;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -106,17 +123,12 @@ 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;
if (!mIsAttachedToWindow || !isTop) {
if (!mIsAttachedToWindow || !isTop || mDestroyed) {
return;
}
@@ -136,7 +148,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
ActionBar actionBar = activity.getSupportActionBar();
// hide back button
actionBar.setDisplayHomeAsUpEnabled(isRoot ? false : !mIsBackButtonHidden);
actionBar.setDisplayHomeAsUpEnabled(getScreenFragment().canNavigateBack() ? !mIsBackButtonHidden : false);
// when setSupportActionBar is called a toolbar wrapper gets initialized that overwrites
// navigation click listener. The default behavior set in the wrapper is to call into
@@ -211,11 +223,10 @@ public class ScreenStackHeaderConfig extends ViewGroup {
case RIGHT:
params.gravity = Gravity.RIGHT;
break;
case TITLE:
params.width = LayoutParams.MATCH_PARENT;
mToolbar.setTitle(null);
case CENTER:
params.width = LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER_HORIZONTAL;
mToolbar.setTitle(null);
break;
}
@@ -225,7 +236,7 @@ public class ScreenStackHeaderConfig extends ViewGroup {
}
private void maybeUpdate() {
if (getParent() != null) {
if (getParent() != null && !mDestroyed) {
onUpdate();
}
}
@@ -294,10 +305,6 @@ public class ScreenStackHeaderConfig extends ViewGroup {
mIsShadowHidden = hideShadow;
}
public void setGestureEnabled(boolean gestureEnabled) {
mGestureEnabled = gestureEnabled;
}
public void setHideBackButton(boolean hideBackButton) {
mIsBackButtonHidden = hideBackButton;
}

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,11 @@ 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();
@@ -93,11 +99,6 @@ public class ScreenStackHeaderConfigViewManager extends ViewGroupManager<ScreenS
config.setHideShadow(hideShadow);
}
@ReactProp(name = "gestureEnabled", defaultBoolean = true)
public void setGestureEnabled(ScreenStackHeaderConfig config, boolean gestureEnabled) {
config.setGestureEnabled(gestureEnabled);
}
@ReactProp(name = "hideBackButton")
public void setHideBackButton(ScreenStackHeaderConfig config, boolean hideBackButton) {
config.setHideBackButton(hideBackButton);

View File

@@ -9,21 +9,14 @@ import com.facebook.react.views.view.ReactViewGroup;
public class ScreenStackHeaderSubview extends ReactViewGroup {
public class Measurements {
public int width;
public int height;
}
public enum Type {
LEFT,
CENTER,
TITLE,
RIGHT,
BACK
}
private int mReactWidth, mReactHeight;
private final UIManagerModule mUIManager;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -43,29 +36,13 @@ public class ScreenStackHeaderSubview extends ReactViewGroup {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed && (mType == Type.CENTER || mType == Type.TITLE)) {
Measurements measurements = new Measurements();
measurements.width = right - left;
if (mType == Type.CENTER) {
// if we want the view to be centered we need to account for the fact that right and left
// paddings may not be equal.
View parent = (View) getParent();
int parentWidth = parent.getWidth();
int rightPadding = parentWidth - right;
int leftPadding = left;
measurements.width = Math.max(0, parentWidth - 2 * Math.max(rightPadding, leftPadding));
}
measurements.height = bottom - top;
mUIManager.setViewLocalData(getId(), measurements);
}
super.onLayout(changed, left, top, right, bottom);
// no-op
}
private Type mType = Type.RIGHT;
public ScreenStackHeaderSubview(ReactContext context) {
super(context);
mUIManager = context.getNativeModule(UIManagerModule.class);
}
public void setType(Type type) {

View File

@@ -1,8 +1,6 @@
package com.swmansion.rnscreens;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.ReactViewGroup;
@@ -11,15 +9,6 @@ import com.facebook.react.views.view.ReactViewManager;
@ReactModule(name = ScreenStackHeaderSubviewManager.REACT_CLASS)
public class ScreenStackHeaderSubviewManager extends ReactViewManager {
private static class SubviewShadowNode extends LayoutShadowNode {
@Override
public void setLocalData(Object data) {
ScreenStackHeaderSubview.Measurements measurements = (ScreenStackHeaderSubview.Measurements) data;
setStyleWidth(measurements.width);
setStyleHeight(measurements.height);
}
}
protected static final String REACT_CLASS = "RNSScreenStackHeaderSubview";
@Override
@@ -32,19 +21,12 @@ public class ScreenStackHeaderSubviewManager extends ReactViewManager {
return new ScreenStackHeaderSubview(context);
}
@Override
public LayoutShadowNode createShadowNodeInstance(ReactApplicationContext context) {
return new SubviewShadowNode();
}
@ReactProp(name = "type")
public void setType(ScreenStackHeaderSubview view, String type) {
if ("left".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.LEFT);
} else if ("center".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.CENTER);
} else if ("title".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.TITLE);
} else if ("right".equals(type)) {
view.setType(ScreenStackHeaderSubview.Type.RIGHT);
} else if ("back".equals(type)) {

View File

@@ -57,6 +57,11 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
}
}
@ReactProp(name = "gestureEnabled", defaultBoolean = true)
public void setGestureEnabled(Screen view, boolean gestureEnabled) {
view.setGestureEnabled(gestureEnabled);
}
@Nullable
@Override
public Map getExportedCustomDirectEventTypeConstants() {
@@ -64,6 +69,8 @@ public class ScreenViewManager extends ViewGroupManager<Screen> {
ScreenDismissedEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onDismissed"),
ScreenAppearEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onAppear"));
MapBuilder.of("registrationName", "onAppear"),
StackFinishTransitioningEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onFinishTransitioning"));
}
}

View File

@@ -0,0 +1,29 @@
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 StackFinishTransitioningEvent extends Event<ScreenAppearEvent> {
public static final String EVENT_NAME = "topFinishTransitioning";
public StackFinishTransitioningEvent(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

@@ -15,7 +15,7 @@ import {
ScreenStackHeaderBackButtonImage,
ScreenStackHeaderLeftView,
ScreenStackHeaderRightView,
ScreenStackHeaderTitleView,
ScreenStackHeaderCenterView,
} from 'react-native-screens';
function renderComponentOrThunk(componentOrThunk, props) {
@@ -27,16 +27,36 @@ function renderComponentOrThunk(componentOrThunk, props) {
class StackView extends React.Component {
_removeScene = route => {
console.warn('REMOVE SCENE');
this.props.navigation.dispatch(StackActions.pop({ key: route.key }));
};
_onSceneFocus = (route, descriptor) => {
descriptor.options && descriptor.options.onAppear && descriptor.options.onAppear()
_onAppear = (route, descriptor) => {
descriptor.options &&
descriptor.options.onAppear &&
descriptor.options.onAppear();
this.props.navigation.dispatch(
StackActions.completeTransition({ toChildKey: route.key })
StackActions.completeTransition({
toChildKey: route.key,
key: this.props.navigation.state.key,
})
);
};
_onFinishTransitioning = () => {
console.warn('FINISH TRANSITIONING');
const { routes } = this.props.navigation.state;
let lastRoute = routes && routes.length && routes[routes.length - 1];
if (lastRoute) {
this.props.navigation.dispatch(
StackActions.completeTransition({
toChildKey: lastRoute.key,
key: this.props.navigation.state.key,
})
);
}
};
_renderHeaderConfig = (index, route, descriptor) => {
const { navigationConfig } = this.props;
const { options } = descriptor;
@@ -50,7 +70,6 @@ class StackView extends React.Component {
headerBackTitle,
headerBackTitleVisible,
headerTintColor,
gestureEnabled,
largeTitle,
headerLargeTitleStyle,
translucent,
@@ -75,7 +94,6 @@ class StackView extends React.Component {
headerBackTitleStyle && headerBackTitleStyle.fontFamily,
backTitleFontSize: headerBackTitleStyle && headerBackTitleStyle.fontSize,
color: headerTintColor,
gestureEnabled: gestureEnabled === undefined ? true : gestureEnabled,
largeTitle,
largeTitleFontFamily:
headerLargeTitleStyle && headerLargeTitleStyle.fontFamily,
@@ -141,9 +159,9 @@ class StackView extends React.Component {
headerOptions.title = options.headerTitle;
} else {
children.push(
<ScreenStackHeaderTitleView key="title">
<ScreenStackHeaderCenterView key="center">
{renderComponentOrThunk(options.headerTitle, { scene })}
</ScreenStackHeaderTitleView>
</ScreenStackHeaderCenterView>
);
}
}
@@ -191,7 +209,10 @@ class StackView extends React.Component {
style={options.cardStyle}
stackAnimation={stackAnimation}
stackPresentation={stackPresentation}
onAppear={() => this._onSceneFocus(route, descriptor)}
gestureEnabled={
options.gestureEnabled === undefined ? true : options.gestureEnabled
}
onAppear={() => this._onAppear(route, descriptor)}
onDismissed={() => this._removeScene(route)}>
{this._renderHeaderConfig(index, route, descriptor)}
<SceneView
@@ -207,7 +228,9 @@ class StackView extends React.Component {
const { navigation, descriptors } = this.props;
return (
<ScreenStack style={styles.scenes}>
<ScreenStack
style={styles.scenes}
onFinishTransitioning={this._onFinishTransitioning}>
{navigation.state.routes.map((route, i) =>
this._renderScene(i, route, descriptors[route.key])
)}

View File

@@ -44,6 +44,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
@property (weak, nonatomic) UIView<RNSScreenContainerDelegate> *reactSuperview;
@property (nonatomic, retain) UIViewController *controller;
@property (nonatomic) BOOL active;
@property (nonatomic) BOOL gestureEnabled;
@property (nonatomic) RNSScreenStackAnimation stackAnimation;
@property (nonatomic) RNSScreenStackPresentation stackPresentation;

View File

@@ -26,6 +26,7 @@
_controller = [[RNSScreen alloc] initWithView:self];
_stackPresentation = RNSScreenStackPresentationPush;
_stackAnimation = RNSScreenStackAnimationDefault;
_gestureEnabled = YES;
}
return self;
@@ -190,6 +191,19 @@
[_touchHandler reset];
}
- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
return _gestureEnabled;
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:)
withObject:presentationController];
}
}
@end
@implementation RNSScreen {
@@ -246,6 +260,8 @@
if (self.parentViewController == nil && self.presentingViewController == nil) {
// screen dismissed, send event
[((RNSScreenView *)self.view) notifyDismissed];
_view = self.view;
self.view = nil;
}
}
@@ -276,6 +292,7 @@
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(active, BOOL)
RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);

View File

@@ -4,6 +4,8 @@
@interface RNSScreenStackView : UIView <RNSScreenContainerDelegate, RCTInvalidating>
@property (nonatomic, copy) RCTDirectEventBlock onFinishTransitioning;
- (void)markChildUpdated;
- (void)didUpdateChildren;

View File

@@ -9,7 +9,7 @@
#import <React/RCTRootContentView.h>
#import <React/RCTTouchHandler.h>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIGestureRecognizerDelegate>
@interface RNSScreenStackView () <UINavigationControllerDelegate, UIAdaptivePresentationControllerDelegate, UIGestureRecognizerDelegate>
@end
@interface RNSScreenStackAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@@ -22,7 +22,6 @@
NSMutableArray<RNSScreenView *> *_reactSubviews;
NSMutableSet<RNSScreenView *> *_dismissedScreens;
NSMutableArray<UIViewController *> *_presentedModals;
__weak UIViewController* recentPopped;
__weak RNSScreenStackManager *_manager;
}
@@ -64,15 +63,36 @@
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
for (NSUInteger i = _reactSubviews.count; i > 0; i--) {
if ([viewController isEqual:[_reactSubviews objectAtIndex:i - 1].controller]) {
RNSScreenView *screenView = [_reactSubviews objectAtIndex:i - 1];
if ([viewController isEqual:screenView.controller]) {
break;
} else {
[_dismissedScreens addObject:[_reactSubviews objectAtIndex:i - 1]];
} else if (screenView.stackPresentation == RNSScreenStackPresentationPush) {
[_dismissedScreens addObject:screenView];
}
}
if (recentPopped != nil) {
recentPopped.view = nil;
recentPopped = nil;
if (self.onFinishTransitioning) {
self.onFinishTransitioning(nil);
}
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
// We don't directly set presentation delegate but instead rely on the ScreenView's delegate to
// forward certain calls to the container (Stack).
UIView *screenView = presentationController.presentedViewController.view;
if ([screenView isKindOfClass:[RNSScreenView class]]) {
[_dismissedScreens addObject:(RNSScreenView *)screenView];
[_presentedModals removeObject:presentationController.presentedViewController];
if (self.onFinishTransitioning) {
// instead of directly triggering onFinishTransitioning this time we enqueue the event on the
// main queue. We do that because onDismiss event is also enqueued and we want for the transition
// finish event to arrive later than onDismiss (see RNSScreen#notifyDismiss)
dispatch_async(dispatch_get_main_queue(), ^{
if (self.onFinishTransitioning) {
self.onFinishTransitioning(nil);
}
});
}
}
}
@@ -83,7 +103,6 @@
screen = (RNSScreenView *) toVC.view;
} else if (operation == UINavigationControllerOperationPop) {
screen = (RNSScreenView *) fromVC.view;
recentPopped = fromVC;
}
if (screen != nil && (screen.stackAnimation == RNSScreenStackAnimationFade || screen.stackAnimation == RNSScreenStackAnimationNone)) {
return [[RNSScreenStackAnimator alloc] initWithOperation:operation];
@@ -102,7 +121,9 @@
RCTRootContentView *rootView = (RCTRootContentView *)parent;
[rootView.touchHandler cancel];
return _controller.viewControllers.count > 1;
RNSScreenView *topScreen = (RNSScreenView *)_controller.viewControllers.lastObject.view;
return _controller.viewControllers.count > 1 && topScreen.gestureEnabled;
}
- (void)markChildUpdated
@@ -121,11 +142,13 @@
RCTLogError(@"ScreenStack only accepts children of type Screen");
return;
}
subview.reactSuperview = self;
[_reactSubviews insertObject:subview atIndex:atIndex];
}
- (void)removeReactSubview:(RNSScreenView *)subview
{
subview.reactSuperview = nil;
[_reactSubviews removeObject:subview];
[_dismissedScreens removeObject:subview];
}
@@ -174,15 +197,27 @@
}
}
__weak RNSScreenStackView *weakSelf = self;
void (^dispatchFinishTransitioning)(void) = ^{
if (weakSelf.onFinishTransitioning) {
weakSelf.onFinishTransitioning(nil);
}
};
void (^finish)(void) = ^{
UIViewController *previous = changeRootController;
for (NSUInteger i = changeRootIndex; i < controllers.count; i++) {
UIViewController *next = controllers[i];
BOOL animate = (i == controllers.count - 1);
[previous presentViewController:next
animated:(i == controllers.count - 1)
completion:nil];
animated:animate
completion:animate ? dispatchFinishTransitioning : nil];
previous = next;
}
if (changeRootIndex >= controllers.count) {
dispatchFinishTransitioning();
}
};
if (changeRootController.presentedViewController) {
@@ -297,8 +332,7 @@
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(transitioning, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(progress, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(onFinishTransitioning, RCTDirectEventBlock);
- (UIView *)view
{

View File

@@ -23,7 +23,6 @@
@property (nonatomic) BOOL hideBackButton;
@property (nonatomic) BOOL hideShadow;
@property (nonatomic) BOOL translucent;
@property (nonatomic) BOOL gestureEnabled;
+ (void)willShowViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig*)config;

View File

@@ -20,34 +20,26 @@
@end
@interface RNSScreenHeaderItemMeasurements : NSObject
@property (nonatomic, readonly) CGSize headerSize;
@property (nonatomic, readonly) CGFloat leftPadding;
@property (nonatomic, readonly) CGFloat rightPadding;
- (instancetype)initWithHeaderSize:(CGSize)headerSize leftPadding:(CGFloat)leftPadding rightPadding:(CGFloat)rightPadding;
@end
@implementation RNSScreenHeaderItemMeasurements
- (instancetype)initWithHeaderSize:(CGSize)headerSize leftPadding:(CGFloat)leftPadding rightPadding:(CGFloat)rightPadding
{
if (self = [super init]) {
_headerSize = headerSize;
_leftPadding = leftPadding;
_rightPadding = rightPadding;
}
return self;
}
@end
@interface RNSScreenStackHeaderSubview : UIView
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, weak) UIView *reactSuperview;
@property (nonatomic) RNSScreenStackHeaderSubviewType type;
- (instancetype)initWithBridge:(RCTBridge*)bridge;
@end
@implementation RNSScreenStackHeaderSubview
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
}
return self;
}
@end
@implementation RNSScreenStackHeaderConfig {
@@ -60,7 +52,6 @@
self.hidden = YES;
_translucent = YES;
_reactSubviews = [NSMutableArray new];
_gestureEnabled = YES;
}
return self;
}
@@ -96,7 +87,16 @@
{
UIViewController *vc = _screenView.controller;
UINavigationController *nav = (UINavigationController*) vc.parentViewController;
if (vc != nil && nav.visibleViewController == vc) {
UIViewController *nextVC = nav.visibleViewController;
if (nav.transitionCoordinator != nil) {
// if navigator is performing transition instead of allowing to update of `visibleConttroller`
// we look at `topController`. This is because during transitiong the `visibleController` won't
// point to the controller that is going to be revealed after transition. This check fixes the
// problem when config gets updated while the transition is ongoing.
nextVC = nav.topViewController;
}
if (vc != nil && nextVC == vc) {
[RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self];
}
}
@@ -118,9 +118,12 @@
UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar;
[navbar setTintColor:config.color];
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
// font customized on the navigation item level, so nothing to do here
} else {
} else
#endif
{
BOOL hideShadow = config.hideShadow;
if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) {
@@ -165,15 +168,6 @@
[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;
}
}
}
@@ -196,6 +190,20 @@
if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) {
hasBackButtonImage = YES;
RCTImageView *imageView = subview.subviews[0];
if (imageView.image == nil) {
// This is yet another workaround for loading custom back icon. It turns out that under
// certain circumstances image attribute can be null despite the app running in production
// mode (when images are loaded from the filesystem). This can happen because image attribute
// is reset when image view is detached from window, and also in some cases initialization
// does not populate the frame of the image view before the loading start. The latter result
// in the image attribute not being updated. We manually set frame to the size of an image
// in order to trigger proper reload that'd update the image attribute.
RCTImageSource *source = imageView.imageSources[0];
[imageView reactSetFrame:CGRectMake(imageView.frame.origin.x,
imageView.frame.origin.y,
source.size.width,
source.size.height)];
}
UIImage *image = imageView.image;
// IMPORTANT!!!
// image can be nil in DEV MODE ONLY
@@ -267,10 +275,9 @@
}
[navctr setNavigationBarHidden:shouldHide animated:YES];
navctr.interactivePopGestureRecognizer.enabled = config.gestureEnabled;
#ifdef __IPHONE_13_0
if (@available(iOS 13.0, *)) {
vc.modalInPresentation = !config.gestureEnabled;
vc.modalInPresentation = !config.screenView.gestureEnabled;
}
#endif
if (shouldHide) {
@@ -376,8 +383,20 @@
navitem.standardAppearance = appearance;
navitem.compactAppearance = appearance;
navitem.scrollEdgeAppearance = appearance;
}
} else
#endif
{
// updating backIndicatotImage does not work when called during transition. On iOS pre 13 we need
// to update it before the navigation starts.
UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config];
if (backButtonImage) {
navctr.navigationBar.backIndicatorImage = backButtonImage;
navctr.navigationBar.backIndicatorTransitionMaskImage = backButtonImage;
} else if (navctr.navigationBar.backIndicatorImage) {
navctr.navigationBar.backIndicatorImage = nil;
navctr.navigationBar.backIndicatorTransitionMaskImage = nil;
}
}
navitem.hidesBackButton = config.hideBackButton;
navitem.leftBarButtonItem = nil;
navitem.rightBarButtonItem = nil;
@@ -396,17 +415,22 @@
}
case RNSScreenStackHeaderSubviewTypeCenter:
case RNSScreenStackHeaderSubviewTypeTitle: {
subview.translatesAutoresizingMaskIntoConstraints = NO;
navitem.titleView = subview;
break;
}
}
}
if (vc.transitionCoordinator != nil && !wasHidden) {
[vc.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
} completion:nil];
if (vc.transitionCoordinator != nil
&& vc.transitionCoordinator.presentationStyle == UIModalPresentationNone
&& !wasHidden) {
// when there is an ongoing transition we may need to update navbar setting in animation block
// using animateAlongsideTransition. However, we only do that given the transition is not a modal
// transition (presentationStyle == UIModalPresentationNone) and that the bar was not previously
// hidden. This is because both for modal transitions and transitions from screen with hidden bar
// the transition animation block does not get triggered. This is ok, because with both of those
// types of transitions there is no "shared" navigation bar that needs to be updated in an animated
// way.
[vc.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self setAnimatedConfig:vc withConfig:config];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
@@ -455,7 +479,6 @@ RCT_EXPORT_VIEW_PROPERTY(hideShadow, BOOL)
// `hidden` is an UIView property, we need to use different name internally
RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL)
RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL)
RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
@end
@@ -471,67 +494,6 @@ RCT_ENUM_CONVERTER(RNSScreenStackHeaderSubviewType, (@{
@end
@implementation RNSScreenStackHeaderSubview
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (!self.translatesAutoresizingMaskIntoConstraints) {
CGSize size = self.superview.frame.size;
CGFloat right = size.width - self.frame.size.width - self.frame.origin.x;
CGFloat left = self.frame.origin.x;
[_bridge.uiManager
setLocalData:[[RNSScreenHeaderItemMeasurements alloc]
initWithHeaderSize:size
leftPadding:left rightPadding:right]
forView:self];
}
}
- (void)reactSetFrame:(CGRect)frame
{
if (self.translatesAutoresizingMaskIntoConstraints) {
[super reactSetFrame:frame];
}
}
- (CGSize)intrinsicContentSize
{
return UILayoutFittingExpandedSize;
}
@end
@interface RNSScreenStackHeaderSubviewShadow : RCTShadowView
@end
@implementation RNSScreenStackHeaderSubviewShadow
- (void)setLocalData:(RNSScreenHeaderItemMeasurements *)data
{
self.width = (YGValue){data.headerSize.width - data.leftPadding - data.rightPadding, YGUnitPoint};
self.height = (YGValue){data.headerSize.height, YGUnitPoint};
if (data.leftPadding > data.rightPadding) {
self.paddingLeft = (YGValue){0, YGUnitPoint};
self.paddingRight = (YGValue){data.leftPadding - data.rightPadding, YGUnitPoint};
} else {
self.paddingLeft = (YGValue){data.rightPadding - data.leftPadding, YGUnitPoint};
self.paddingRight = (YGValue){0, YGUnitPoint};
}
[self didSetProps:@[@"width", @"height", @"paddingLeft", @"paddingRight"]];
}
@end
@implementation RNSScreenStackHeaderSubviewManager
RCT_EXPORT_MODULE()
@@ -543,9 +505,4 @@ RCT_EXPORT_VIEW_PROPERTY(type, RNSScreenStackHeaderSubviewType)
return [[RNSScreenStackHeaderSubview alloc] initWithBridge:self.bridge];
}
- (RCTShadowView *)shadowView
{
return [RNSScreenStackHeaderSubviewShadow new];
}
@end

View File

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

20
src/screens.d.ts vendored
View File

@@ -3,7 +3,13 @@
declare module 'react-native-screens' {
import { ComponentClass } from 'react';
import { ViewProps, Animated, NativeSyntheticEvent, NativeTouchEvent, ImageProps } from 'react-native';
import {
ViewProps,
Animated,
NativeSyntheticEvent,
NativeTouchEvent,
ImageProps,
} from 'react-native';
export function useScreens(shouldUseScreens?: boolean): void;
export function enableScreens(shouldEnableScreens?: boolean): void;
@@ -35,11 +41,14 @@ declare module 'react-native-screens' {
* @type "none" the screen appears/dissapears without an animation
*/
stackAnimation?: StackAnimationTypes;
/**
* @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;
}
export type ScreenContainerProps = ViewProps;
export interface ScreenStackProps extends ViewProps {
transitioning?: number;
progress?: number;
@@ -82,10 +91,6 @@ declare module 'react-native-screens' {
* @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
@@ -135,7 +140,6 @@ declare module 'react-native-screens' {
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

@@ -171,14 +171,6 @@ const ScreenStackHeaderLeftView = props => (
/>
);
const ScreenStackHeaderTitleView = props => (
<ScreensNativeModules.NativeScreenStackHeaderSubview
{...props}
type="title"
style={styles.headerSubview}
/>
);
const ScreenStackHeaderCenterView = props => (
<ScreensNativeModules.NativeScreenStackHeaderSubview
{...props}
@@ -210,7 +202,6 @@ module.exports = {
ScreenStackHeaderBackButtonImage,
ScreenStackHeaderRightView,
ScreenStackHeaderLeftView,
ScreenStackHeaderTitleView,
ScreenStackHeaderCenterView,
enableScreens,

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: