react-native-screens 3.9.0 → 3.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +47 -7
  3. package/android/build.gradle +1 -2
  4. package/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt +71 -0
  5. package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +7 -0
  6. package/android/src/main/java/com/swmansion/rnscreens/FragmentBackPressOverrider.kt +29 -0
  7. package/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +2 -1
  8. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +35 -52
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +1 -1
  10. package/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +83 -34
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +38 -33
  12. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +77 -42
  13. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +25 -9
  14. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +8 -0
  15. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +7 -1
  16. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.kt +1 -0
  17. package/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +10 -0
  18. package/android/src/main/java/com/swmansion/rnscreens/ScreenWindowTraits.kt +72 -11
  19. package/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt +107 -0
  20. package/android/src/main/java/com/swmansion/rnscreens/SearchBarView.kt +155 -0
  21. package/android/src/main/java/com/swmansion/rnscreens/SearchViewFormatter.kt +67 -0
  22. package/android/src/main/res/anim/rns_default_enter_in.xml +18 -0
  23. package/android/src/main/res/anim/rns_default_enter_out.xml +19 -0
  24. package/android/src/main/res/anim/rns_default_exit_in.xml +17 -0
  25. package/android/src/main/res/anim/rns_default_exit_out.xml +18 -0
  26. package/android/src/main/res/anim/rns_fade_in.xml +7 -0
  27. package/android/src/main/res/anim/rns_fade_out.xml +7 -0
  28. package/android/src/main/res/anim/rns_no_animation_20.xml +6 -0
  29. package/createNativeStackNavigator/README.md +12 -0
  30. package/ios/RNSScreen.h +10 -0
  31. package/ios/RNSScreen.m +38 -0
  32. package/ios/RNSScreenContainer.m +5 -0
  33. package/ios/RNSScreenStack.m +29 -13
  34. package/ios/RNSScreenStackAnimator.m +45 -14
  35. package/ios/RNSScreenStackHeaderConfig.m +4 -1
  36. package/ios/RNSScreenWindowTraits.h +1 -0
  37. package/ios/RNSScreenWindowTraits.m +20 -0
  38. package/ios/UIViewController+RNScreens.m +10 -0
  39. package/lib/commonjs/index.js +17 -1
  40. package/lib/commonjs/index.js.map +1 -1
  41. package/lib/commonjs/index.native.js +66 -18
  42. package/lib/commonjs/index.native.js.map +1 -1
  43. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js +67 -0
  44. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js.map +1 -0
  45. package/lib/commonjs/native-stack/views/HeaderConfig.js +46 -4
  46. package/lib/commonjs/native-stack/views/HeaderConfig.js.map +1 -1
  47. package/lib/commonjs/native-stack/views/NativeStackView.js +33 -4
  48. package/lib/commonjs/native-stack/views/NativeStackView.js.map +1 -1
  49. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js +60 -0
  50. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  51. package/lib/commonjs/reanimated/ReanimatedScreen.js +7 -79
  52. package/lib/commonjs/reanimated/ReanimatedScreen.js.map +1 -1
  53. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js +61 -0
  54. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js.map +1 -0
  55. package/lib/commonjs/reanimated/index.js +2 -2
  56. package/lib/commonjs/reanimated/index.js.map +1 -1
  57. package/lib/commonjs/utils.js +20 -0
  58. package/lib/commonjs/utils.js.map +1 -0
  59. package/lib/module/index.js +1 -0
  60. package/lib/module/index.js.map +1 -1
  61. package/lib/module/index.native.js +65 -19
  62. package/lib/module/index.native.js.map +1 -1
  63. package/lib/module/native-stack/utils/useBackPressSubscription.js +50 -0
  64. package/lib/module/native-stack/utils/useBackPressSubscription.js.map +1 -0
  65. package/lib/module/native-stack/views/HeaderConfig.js +46 -5
  66. package/lib/module/native-stack/views/HeaderConfig.js.map +1 -1
  67. package/lib/module/native-stack/views/NativeStackView.js +33 -4
  68. package/lib/module/native-stack/views/NativeStackView.js.map +1 -1
  69. package/lib/module/reanimated/ReanimatedNativeStackScreen.js +40 -0
  70. package/lib/module/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  71. package/lib/module/reanimated/ReanimatedScreen.js +6 -73
  72. package/lib/module/reanimated/ReanimatedScreen.js.map +1 -1
  73. package/lib/module/reanimated/ReanimatedScreenProvider.js +49 -0
  74. package/lib/module/reanimated/ReanimatedScreenProvider.js.map +1 -0
  75. package/lib/module/reanimated/index.js +1 -1
  76. package/lib/module/reanimated/index.js.map +1 -1
  77. package/lib/module/utils.js +8 -0
  78. package/lib/module/utils.js.map +1 -0
  79. package/lib/typescript/index.d.ts +1 -0
  80. package/lib/typescript/native-stack/types.d.ts +34 -2
  81. package/lib/typescript/native-stack/utils/useBackPressSubscription.d.ts +16 -0
  82. package/lib/typescript/reanimated/ReanimatedNativeStackScreen.d.ts +5 -0
  83. package/lib/typescript/reanimated/ReanimatedScreen.d.ts +5 -2
  84. package/lib/typescript/reanimated/ReanimatedScreenProvider.d.ts +2 -0
  85. package/lib/typescript/reanimated/index.d.ts +1 -1
  86. package/lib/typescript/types.d.ts +101 -1
  87. package/lib/typescript/utils.d.ts +2 -0
  88. package/native-stack/README.md +70 -8
  89. package/package.json +2 -1
  90. package/reanimated/package.json +6 -0
  91. package/src/index.native.tsx +94 -36
  92. package/src/index.tsx +4 -0
  93. package/src/native-stack/types.tsx +34 -2
  94. package/src/native-stack/utils/useBackPressSubscription.tsx +66 -0
  95. package/src/native-stack/views/HeaderConfig.tsx +46 -3
  96. package/src/native-stack/views/NativeStackView.tsx +33 -4
  97. package/src/reanimated/ReanimatedNativeStackScreen.tsx +61 -0
  98. package/src/reanimated/ReanimatedScreen.tsx +6 -84
  99. package/src/reanimated/ReanimatedScreenProvider.tsx +42 -0
  100. package/src/reanimated/index.tsx +1 -1
  101. package/src/types.tsx +101 -1
  102. package/src/utils.ts +12 -0
@@ -3,7 +3,6 @@ package com.swmansion.rnscreens
3
3
  import android.content.Context
4
4
  import android.graphics.Canvas
5
5
  import android.view.View
6
- import androidx.fragment.app.FragmentTransaction
7
6
  import com.facebook.react.bridge.ReactContext
8
7
  import com.facebook.react.uimanager.UIManagerModule
9
8
  import com.swmansion.rnscreens.Screen.StackAnimation
@@ -110,7 +109,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
110
109
  }
111
110
  }
112
111
  var shouldUseOpenAnimation = true
113
- var transition = FragmentTransaction.TRANSIT_FRAGMENT_OPEN
114
112
  var stackAnimation: StackAnimation? = null
115
113
  if (!mStack.contains(newTop)) {
116
114
  // if new top screen wasn't on stack we do "open animation" so long it is not the very first
@@ -121,21 +119,15 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
121
119
  // before, probably replace or reset was called, so we play the "close animation".
122
120
  // Otherwise it's open animation
123
121
  val containsTopScreen = mTopScreen?.let { mScreenFragments.contains(it) } == true
124
- shouldUseOpenAnimation = containsTopScreen || newTop.screen.replaceAnimation !== Screen.ReplaceAnimation.POP
125
- stackAnimation = newTop.screen.stackAnimation
122
+ val isPushReplace = newTop.screen.replaceAnimation === Screen.ReplaceAnimation.PUSH
123
+ shouldUseOpenAnimation = containsTopScreen || isPushReplace
124
+ // if the replace animation is `push`, the new top screen provides the animation, otherwise the previous one
125
+ stackAnimation = if (shouldUseOpenAnimation) newTop.screen.stackAnimation else mTopScreen?.screen?.stackAnimation
126
126
  } else if (mTopScreen == null && newTop != null) {
127
127
  // mTopScreen was not present before so newTop is the first screen added to a stack
128
- // and we don't want the animation when it is entering, but we want to send the
129
- // willAppear and Appear events to the user, which won't be sent by default if Screen's
130
- // stack animation is not NONE (see check for stackAnimation in onCreateAnimation in
131
- // ScreenStackFragment).
132
- // We don't do it if the stack is nested since the parent will trigger these events in child
128
+ // and we don't want the animation when it is entering
133
129
  stackAnimation = StackAnimation.NONE
134
- if (newTop.screen.stackAnimation !== StackAnimation.NONE && !isNested) {
135
- goingForward = true
136
- newTop.dispatchOnWillAppear()
137
- newTop.dispatchOnAppear()
138
- }
130
+ goingForward = true
139
131
  }
140
132
  } else if (mTopScreen != null && mTopScreen != newTop) {
141
133
  // otherwise if we are performing top screen change we do "close animation"
@@ -147,40 +139,32 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
147
139
  // animation logic start
148
140
  if (stackAnimation != null) {
149
141
  if (shouldUseOpenAnimation) {
150
- transition = FragmentTransaction.TRANSIT_FRAGMENT_OPEN
151
142
  when (stackAnimation) {
143
+ StackAnimation.DEFAULT -> it.setCustomAnimations(R.anim.rns_default_enter_in, R.anim.rns_default_enter_out)
144
+ StackAnimation.NONE -> it.setCustomAnimations(R.anim.rns_no_animation_20, R.anim.rns_no_animation_20)
145
+ StackAnimation.FADE -> it.setCustomAnimations(R.anim.rns_fade_in, R.anim.rns_fade_out)
152
146
  StackAnimation.SLIDE_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_slide_in_from_right, R.anim.rns_slide_out_to_left)
153
147
  StackAnimation.SLIDE_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_slide_in_from_left, R.anim.rns_slide_out_to_right)
154
148
  StackAnimation.SLIDE_FROM_BOTTOM -> it.setCustomAnimations(
155
149
  R.anim.rns_slide_in_from_bottom, R.anim.rns_no_animation_medium
156
150
  )
157
151
  StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_fade_from_bottom, R.anim.rns_no_animation_350)
158
- else -> {
159
- }
160
152
  }
161
153
  } else {
162
- transition = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE
163
154
  when (stackAnimation) {
155
+ StackAnimation.DEFAULT -> it.setCustomAnimations(R.anim.rns_default_exit_in, R.anim.rns_default_exit_out)
156
+ StackAnimation.NONE -> it.setCustomAnimations(R.anim.rns_no_animation_20, R.anim.rns_no_animation_20)
157
+ StackAnimation.FADE -> it.setCustomAnimations(R.anim.rns_fade_in, R.anim.rns_fade_out)
164
158
  StackAnimation.SLIDE_FROM_RIGHT -> it.setCustomAnimations(R.anim.rns_slide_in_from_left, R.anim.rns_slide_out_to_right)
165
159
  StackAnimation.SLIDE_FROM_LEFT -> it.setCustomAnimations(R.anim.rns_slide_in_from_right, R.anim.rns_slide_out_to_left)
166
160
  StackAnimation.SLIDE_FROM_BOTTOM -> it.setCustomAnimations(
167
161
  R.anim.rns_no_animation_medium, R.anim.rns_slide_out_to_bottom
168
162
  )
169
163
  StackAnimation.FADE_FROM_BOTTOM -> it.setCustomAnimations(R.anim.rns_no_animation_250, R.anim.rns_fade_to_bottom)
170
- else -> {
171
- }
172
164
  }
173
165
  }
174
166
  }
175
- if (stackAnimation === StackAnimation.NONE) {
176
- transition = FragmentTransaction.TRANSIT_NONE
177
- }
178
- if (stackAnimation === StackAnimation.FADE) {
179
- transition = FragmentTransaction.TRANSIT_FRAGMENT_FADE
180
- }
181
- if (stackAnimation != null && isSystemAnimation(stackAnimation)) {
182
- it.setTransition(transition)
183
- }
167
+
184
168
  // animation logic end
185
169
  goingForward = shouldUseOpenAnimation
186
170
 
@@ -236,10 +220,35 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
236
220
  mTopScreen = newTop
237
221
  mStack.clear()
238
222
  mStack.addAll(mScreenFragments)
223
+
224
+ turnOffA11yUnderTransparentScreen(visibleBottom)
225
+
239
226
  it.commitNowAllowingStateLoss()
240
227
  }
241
228
  }
242
229
 
230
+ // only top visible screen should be accessible
231
+ private fun turnOffA11yUnderTransparentScreen(visibleBottom: ScreenStackFragment?) {
232
+ if (mScreenFragments.size > 1 && visibleBottom != null) {
233
+ mTopScreen?.let {
234
+ if (isTransparent(it)) {
235
+ val screenFragmentsBeneathTop = mScreenFragments.slice(0 until mScreenFragments.size - 1).asReversed()
236
+ // go from the top of the stack excluding the top screen
237
+ for (screenFragment in screenFragmentsBeneathTop) {
238
+ screenFragment.screen.changeAccessibilityMode(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
239
+
240
+ // don't change a11y below non-transparent screens
241
+ if (screenFragment == visibleBottom) {
242
+ break
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ topScreen?.changeAccessibilityMode(IMPORTANT_FOR_ACCESSIBILITY_AUTO)
250
+ }
251
+
243
252
  override fun notifyContainerUpdate() {
244
253
  for (screen in mStack) {
245
254
  screen.onContainerUpdate()
@@ -321,10 +330,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
321
330
  }
322
331
 
323
332
  companion object {
324
- private fun isSystemAnimation(stackAnimation: StackAnimation): Boolean {
325
- return stackAnimation === StackAnimation.DEFAULT || stackAnimation === StackAnimation.FADE || stackAnimation === StackAnimation.NONE
326
- }
327
-
328
333
  private fun isTransparent(fragment: ScreenStackFragment): Boolean {
329
334
  return (
330
335
  fragment.screen.stackPresentation
@@ -5,6 +5,9 @@ import android.content.Context
5
5
  import android.graphics.Color
6
6
  import android.os.Bundle
7
7
  import android.view.LayoutInflater
8
+ import android.view.Menu
9
+ import android.view.MenuInflater
10
+ import android.view.MenuItem
8
11
  import android.view.View
9
12
  import android.view.ViewGroup
10
13
  import android.view.animation.Animation
@@ -13,7 +16,6 @@ import android.view.animation.Transformation
13
16
  import android.widget.LinearLayout
14
17
  import androidx.appcompat.widget.Toolbar
15
18
  import androidx.coordinatorlayout.widget.CoordinatorLayout
16
- import com.facebook.react.bridge.UiThreadUtil
17
19
  import com.facebook.react.uimanager.PixelUtil
18
20
  import com.google.android.material.appbar.AppBarLayout
19
21
  import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
@@ -24,6 +26,9 @@ class ScreenStackFragment : ScreenFragment {
24
26
  private var mShadowHidden = false
25
27
  private var mIsTranslucent = false
26
28
 
29
+ var searchView: CustomSearchView? = null
30
+ var onSearchViewCreate: ((searchView: CustomSearchView) -> Unit)? = null
31
+
27
32
  @SuppressLint("ValidFragment")
28
33
  constructor(screenView: Screen) : super(screenView)
29
34
 
@@ -64,7 +69,8 @@ class ScreenStackFragment : ScreenFragment {
64
69
  fun setToolbarTranslucent(translucent: Boolean) {
65
70
  if (mIsTranslucent != translucent) {
66
71
  val params = screen.layoutParams
67
- (params as CoordinatorLayout.LayoutParams).behavior = if (translucent) null else ScrollingViewBehavior()
72
+ (params as CoordinatorLayout.LayoutParams).behavior =
73
+ if (translucent) null else ScrollingViewBehavior()
68
74
  mIsTranslucent = translucent
69
75
  }
70
76
  }
@@ -79,35 +85,6 @@ class ScreenStackFragment : ScreenFragment {
79
85
  notifyViewAppearTransitionEnd()
80
86
  }
81
87
 
82
- override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
83
- // this means that the fragment will appear with a custom transition, in the case
84
- // of animation: 'none', onViewAnimationStart and onViewAnimationEnd
85
- // won't be called and we need to notify stack directly from here.
86
- // When using the Toolbar back button this is called an extra time with transit = 0 but in
87
- // this case we don't want to notify. The way I found to detect is case is check isHidden.
88
- if (transit == 0 && !isHidden &&
89
- screen.stackAnimation === Screen.StackAnimation.NONE
90
- ) {
91
- if (enter) {
92
- // Android dispatches the animation start event for the fragment that is being added first
93
- // however we want the one being dismissed first to match iOS. It also makes more sense
94
- // from a navigation point of view to have the disappear event first.
95
- // Since there are no explicit relationships between the fragment being added / removed
96
- // the practical way to fix this is delaying dispatching the appear events at the end of
97
- // the frame.
98
- UiThreadUtil.runOnUiThread {
99
- dispatchOnWillAppear()
100
- dispatchOnAppear()
101
- }
102
- } else {
103
- dispatchOnWillDisappear()
104
- dispatchOnDisappear()
105
- notifyViewAppearTransitionEnd()
106
- }
107
- }
108
- return null
109
- }
110
-
111
88
  private fun notifyViewAppearTransitionEnd() {
112
89
  val screenStack = view?.parent
113
90
  if (screenStack is ScreenStack) {
@@ -120,7 +97,8 @@ class ScreenStackFragment : ScreenFragment {
120
97
  container: ViewGroup?,
121
98
  savedInstanceState: Bundle?
122
99
  ): View? {
123
- val view: NotifyingCoordinatorLayout? = context?.let { NotifyingCoordinatorLayout(it, this) }
100
+ val view: ScreensCoordinatorLayout? =
101
+ context?.let { ScreensCoordinatorLayout(it, this) }
124
102
  val params = CoordinatorLayout.LayoutParams(
125
103
  LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT
126
104
  )
@@ -142,9 +120,49 @@ class ScreenStackFragment : ScreenFragment {
142
120
  mAppBarLayout?.targetElevation = 0f
143
121
  }
144
122
  mToolbar?.let { mAppBarLayout?.addView(recycleView(it)) }
123
+ setHasOptionsMenu(true)
145
124
  return view
146
125
  }
147
126
 
127
+ override fun onPrepareOptionsMenu(menu: Menu) {
128
+ updateToolbarMenu(menu)
129
+ return super.onPrepareOptionsMenu(menu)
130
+ }
131
+
132
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
133
+ updateToolbarMenu(menu)
134
+ return super.onCreateOptionsMenu(menu, inflater)
135
+ }
136
+
137
+ private fun shouldShowSearchBar(): Boolean {
138
+ val config = screen.headerConfig
139
+ val numberOfSubViews = config?.configSubviewsCount ?: 0
140
+ if (config != null && numberOfSubViews > 0) {
141
+ for (i in 0 until numberOfSubViews) {
142
+ val subView = config.getConfigSubview(i)
143
+ if (subView.type == ScreenStackHeaderSubview.Type.SEARCH_BAR) {
144
+ return true
145
+ }
146
+ }
147
+ }
148
+ return false
149
+ }
150
+
151
+ private fun updateToolbarMenu(menu: Menu) {
152
+ menu.clear()
153
+ if (shouldShowSearchBar()) {
154
+ val currentContext = context
155
+ if (searchView == null && currentContext != null) {
156
+ val newSearchView = CustomSearchView(currentContext, this)
157
+ searchView = newSearchView
158
+ onSearchViewCreate?.invoke(newSearchView)
159
+ }
160
+ val item = menu.add("")
161
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
162
+ item.actionView = searchView
163
+ }
164
+ }
165
+
148
166
  fun canNavigateBack(): Boolean {
149
167
  val container: ScreenContainer<*>? = screen.container
150
168
  check(container is ScreenStack) { "ScreenStackFragment added into a non-stack container" }
@@ -168,18 +186,22 @@ class ScreenStackFragment : ScreenFragment {
168
186
  container.dismiss(this)
169
187
  }
170
188
 
171
- private class NotifyingCoordinatorLayout(context: Context, private val mFragment: ScreenFragment) : CoordinatorLayout(context) {
172
- private val mAnimationListener: Animation.AnimationListener = object : Animation.AnimationListener {
173
- override fun onAnimationStart(animation: Animation) {
174
- mFragment.onViewAnimationStart()
175
- }
189
+ private class ScreensCoordinatorLayout(
190
+ context: Context,
191
+ private val mFragment: ScreenFragment
192
+ ) : CoordinatorLayout(context) {
193
+ private val mAnimationListener: Animation.AnimationListener =
194
+ object : Animation.AnimationListener {
195
+ override fun onAnimationStart(animation: Animation) {
196
+ mFragment.onViewAnimationStart()
197
+ }
176
198
 
177
- override fun onAnimationEnd(animation: Animation) {
178
- mFragment.onViewAnimationEnd()
179
- }
199
+ override fun onAnimationEnd(animation: Animation) {
200
+ mFragment.onViewAnimationEnd()
201
+ }
180
202
 
181
- override fun onAnimationRepeat(animation: Animation) {}
182
- }
203
+ override fun onAnimationRepeat(animation: Animation) {}
204
+ }
183
205
 
184
206
  override fun startAnimation(animation: Animation) {
185
207
  // For some reason View##onAnimationEnd doesn't get called for
@@ -205,6 +227,19 @@ class ScreenStackFragment : ScreenFragment {
205
227
  super.startAnimation(set)
206
228
  }
207
229
  }
230
+
231
+ /**
232
+ * This method implements a workaround for RN's autoFocus functionality. Because of the way
233
+ * autoFocus is implemented it dismisses soft keyboard in fragment transition
234
+ * due to change of visibility of the view at the start of the transition. Here we override the
235
+ * call to `clearFocus` when the visibility of view is `INVISIBLE` since `clearFocus` triggers the
236
+ * hiding of the keyboard in `ReactEditText.java`.
237
+ */
238
+ override fun clearFocus() {
239
+ if (visibility != INVISIBLE) {
240
+ super.clearFocus()
241
+ }
242
+ }
208
243
  }
209
244
 
210
245
  private class ScreensAnimation(private val mFragment: ScreenFragment) : Animation() {
@@ -17,11 +17,14 @@ import androidx.fragment.app.Fragment
17
17
  import com.facebook.react.ReactApplication
18
18
  import com.facebook.react.bridge.JSApplicationIllegalArgumentException
19
19
  import com.facebook.react.bridge.ReactContext
20
+ import com.facebook.react.bridge.WritableMap
21
+ import com.facebook.react.uimanager.events.RCTEventEmitter
20
22
  import com.facebook.react.views.text.ReactTypefaceUtils
21
23
 
22
24
  class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
23
25
  private val mConfigSubviews = ArrayList<ScreenStackHeaderSubview>(3)
24
- val toolbar: Toolbar
26
+ val toolbar: CustomToolbar
27
+ private var headerTopInset: Int? = null
25
28
  private var mTitle: String? = null
26
29
  private var mTitleColor = 0
27
30
  private var mTitleFontFamily: String? = null
@@ -62,6 +65,11 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
62
65
  }
63
66
  }
64
67
 
68
+ private fun sendEvent(eventName: String, eventContent: WritableMap?) {
69
+ (context as ReactContext).getJSModule(RCTEventEmitter::class.java)
70
+ ?.receiveEvent(id, eventName, eventContent)
71
+ }
72
+
65
73
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
66
74
  // no-op
67
75
  }
@@ -73,12 +81,23 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
73
81
  override fun onAttachedToWindow() {
74
82
  super.onAttachedToWindow()
75
83
  mIsAttachedToWindow = true
84
+ sendEvent("onAttached", null)
85
+ // we want to save the top inset before the status bar can be hidden, which would resolve in
86
+ // inset being 0
87
+ if (headerTopInset == null) {
88
+ headerTopInset = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
89
+ rootWindowInsets.systemWindowInsetTop
90
+ else
91
+ // Hacky fallback for old android. Before Marshmallow, the status bar height was always 25
92
+ (25 * resources.displayMetrics.density).toInt()
93
+ }
76
94
  onUpdate()
77
95
  }
78
96
 
79
97
  override fun onDetachedFromWindow() {
80
98
  super.onDetachedFromWindow()
81
99
  mIsAttachedToWindow = false
100
+ sendEvent("onDetached", null)
82
101
  }
83
102
 
84
103
  private val screen: Screen?
@@ -99,7 +118,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
99
118
  }
100
119
  return null
101
120
  }
102
- private val screenFragment: ScreenStackFragment?
121
+ val screenFragment: ScreenStackFragment?
103
122
  get() {
104
123
  val screen = parent
105
124
  if (screen is Screen) {
@@ -150,11 +169,8 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
150
169
  screenFragment?.setToolbar(toolbar)
151
170
  }
152
171
  if (mIsTopInsetEnabled) {
153
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
154
- toolbar.setPadding(0, rootWindowInsets.systemWindowInsetTop, 0, 0)
155
- } else {
156
- // Hacky fallback for old android. Before Marshmallow, the status bar height was always 25
157
- toolbar.setPadding(0, (25 * resources.displayMetrics.density).toInt(), 0, 0)
172
+ headerTopInset.let {
173
+ toolbar.setPadding(0, it ?: 0, 0, 0)
158
174
  }
159
175
  } else {
160
176
  if (toolbar.paddingTop > 0) {
@@ -369,7 +385,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
369
385
  mDirection = direction
370
386
  }
371
387
 
372
- private class DebugMenuToolbar(context: Context) : Toolbar(context) {
388
+ private class DebugMenuToolbar(context: Context, config: ScreenStackHeaderConfig) : CustomToolbar(context, config) {
373
389
  override fun showOverflowMenu(): Boolean {
374
390
  (context.applicationContext as ReactApplication)
375
391
  .reactNativeHost
@@ -381,7 +397,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
381
397
 
382
398
  init {
383
399
  visibility = GONE
384
- toolbar = if (BuildConfig.DEBUG) DebugMenuToolbar(context) else Toolbar(context)
400
+ toolbar = if (BuildConfig.DEBUG) DebugMenuToolbar(context, this) else CustomToolbar(context, this)
385
401
  mDefaultStartInset = toolbar.contentInsetStart
386
402
  mDefaultStartInsetWithNavigation = toolbar.contentInsetStartWithNavigation
387
403
 
@@ -2,6 +2,7 @@ package com.swmansion.rnscreens
2
2
 
3
3
  import android.view.View
4
4
  import com.facebook.react.bridge.JSApplicationCausedNativeException
5
+ import com.facebook.react.common.MapBuilder
5
6
  import com.facebook.react.module.annotations.ReactModule
6
7
  import com.facebook.react.uimanager.ThemedReactContext
7
8
  import com.facebook.react.uimanager.ViewGroupManager
@@ -129,6 +130,13 @@ class ScreenStackHeaderConfigViewManager : ViewGroupManager<ScreenStackHeaderCon
129
130
  config.setDirection(direction)
130
131
  }
131
132
 
133
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
134
+ return MapBuilder.builder<String, Any>()
135
+ .put("onAttached", MapBuilder.of("registrationName", "onAttached"))
136
+ .put("onDetached", MapBuilder.of("registrationName", "onDetached"))
137
+ .build()
138
+ }
139
+
132
140
  companion object {
133
141
  const val REACT_CLASS = "RNSScreenStackHeaderConfig"
134
142
  }
@@ -10,6 +10,12 @@ class ScreenStackHeaderSubview(context: ReactContext?) : ReactViewGroup(context)
10
10
  private var mReactWidth = 0
11
11
  private var mReactHeight = 0
12
12
  var type = Type.RIGHT
13
+
14
+ val config: ScreenStackHeaderConfig?
15
+ get() {
16
+ return (parent as? CustomToolbar)?.config
17
+ }
18
+
13
19
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
14
20
  if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
15
21
  MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY
@@ -31,6 +37,6 @@ class ScreenStackHeaderSubview(context: ReactContext?) : ReactViewGroup(context)
31
37
  }
32
38
 
33
39
  enum class Type {
34
- LEFT, CENTER, RIGHT, BACK
40
+ LEFT, CENTER, RIGHT, BACK, SEARCH_BAR
35
41
  }
36
42
  }
@@ -24,6 +24,7 @@ class ScreenStackHeaderSubviewManager : ReactViewManager() {
24
24
  "center" -> ScreenStackHeaderSubview.Type.CENTER
25
25
  "right" -> ScreenStackHeaderSubview.Type.RIGHT
26
26
  "back" -> ScreenStackHeaderSubview.Type.BACK
27
+ "searchBar" -> ScreenStackHeaderSubview.Type.SEARCH_BAR
27
28
  else -> throw JSApplicationIllegalArgumentException("Unknown type $type")
28
29
  }
29
30
  }
@@ -112,6 +112,16 @@ class ScreenViewManager : ViewGroupManager<Screen>() {
112
112
  view.isStatusBarHidden = statusBarHidden
113
113
  }
114
114
 
115
+ @ReactProp(name = "navigationBarColor", customType = "Color")
116
+ fun setNavigationBarColor(view: Screen, navigationBarColor: Int) {
117
+ view.navigationBarColor = navigationBarColor
118
+ }
119
+
120
+ @ReactProp(name = "navigationBarHidden")
121
+ fun setNavigationBarHidden(view: Screen, navigationBarHidden: Boolean?) {
122
+ view.isNavigationBarHidden = navigationBarHidden
123
+ }
124
+
115
125
  @ReactProp(name = "nativeBackButtonDismissalEnabled")
116
126
  fun setNativeBackButtonDismissalEnabled(
117
127
  view: Screen,
@@ -6,11 +6,15 @@ import android.annotation.SuppressLint
6
6
  import android.annotation.TargetApi
7
7
  import android.app.Activity
8
8
  import android.content.pm.ActivityInfo
9
+ import android.graphics.Color
9
10
  import android.os.Build
10
11
  import android.view.View
11
12
  import android.view.ViewParent
12
13
  import android.view.WindowManager
13
14
  import androidx.core.view.ViewCompat
15
+ import androidx.core.view.WindowCompat
16
+ import androidx.core.view.WindowInsetsCompat
17
+ import androidx.core.view.WindowInsetsControllerCompat
14
18
  import com.facebook.react.bridge.GuardedRunnable
15
19
  import com.facebook.react.bridge.ReactContext
16
20
  import com.facebook.react.bridge.UiThreadUtil
@@ -21,6 +25,7 @@ object ScreenWindowTraits {
21
25
  // https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/statusbar/StatusBarModule.java
22
26
  private var mDidSetOrientation = false
23
27
  private var mDidSetStatusBarAppearance = false
28
+ private var mDidSetNavigationBarAppearance = false
24
29
  private var mDefaultStatusBarColor: Int? = null
25
30
  internal fun applyDidSetOrientation() {
26
31
  mDidSetOrientation = true
@@ -30,6 +35,10 @@ object ScreenWindowTraits {
30
35
  mDidSetStatusBarAppearance = true
31
36
  }
32
37
 
38
+ internal fun applyDidSetNavigationBarAppearance() {
39
+ mDidSetNavigationBarAppearance = true
40
+ }
41
+
33
42
  internal fun setOrientation(screen: Screen, activity: Activity?) {
34
43
  if (activity == null) {
35
44
  return
@@ -72,23 +81,21 @@ object ScreenWindowTraits {
72
81
  }
73
82
 
74
83
  internal fun setStyle(screen: Screen, activity: Activity?, context: ReactContext?) {
75
- if (activity == null || context == null) {
84
+ if (activity == null || context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
76
85
  return
77
86
  }
78
87
  val screenForStyle = findScreenForTrait(screen, WindowTraits.STYLE)
79
88
  val style = screenForStyle?.statusBarStyle ?: "light"
80
89
 
81
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
82
- UiThreadUtil.runOnUiThread {
83
- val decorView = activity.window.decorView
84
- var systemUiVisibilityFlags = decorView.systemUiVisibility
85
- systemUiVisibilityFlags = if ("dark" == style) {
86
- systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
87
- } else {
88
- systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
89
- }
90
- decorView.systemUiVisibility = systemUiVisibilityFlags
90
+ UiThreadUtil.runOnUiThread {
91
+ val decorView = activity.window.decorView
92
+ var systemUiVisibilityFlags = decorView.systemUiVisibility
93
+ systemUiVisibilityFlags = if ("dark" == style) {
94
+ systemUiVisibilityFlags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
95
+ } else {
96
+ systemUiVisibilityFlags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
91
97
  }
98
+ decorView.systemUiVisibility = systemUiVisibilityFlags
92
99
  }
93
100
  }
94
101
 
@@ -144,6 +151,48 @@ object ScreenWindowTraits {
144
151
  }
145
152
  }
146
153
 
154
+ // Methods concerning navigationBar management were taken from `react-native-navigation`'s repo:
155
+ // https://github.com/wix/react-native-navigation/blob/9bb70d81700692141a2c505c081c2d86c7f9c66e/lib/android/app/src/main/java/com/reactnativenavigation/utils/SystemUiUtils.kt
156
+ internal fun setNavigationBarColor(screen: Screen, activity: Activity?) {
157
+ if (activity == null) {
158
+ return
159
+ }
160
+
161
+ val window = activity.window
162
+
163
+ val screenForNavBarColor = findScreenForTrait(screen, WindowTraits.NAVIGATION_BAR_COLOR)
164
+ val color = screenForNavBarColor?.navigationBarColor ?: window.navigationBarColor
165
+
166
+ WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars =
167
+ isColorLight(color)
168
+ window.navigationBarColor = color
169
+ }
170
+
171
+ internal fun setNavigationBarHidden(screen: Screen, activity: Activity?) {
172
+ if (activity == null) {
173
+ return
174
+ }
175
+
176
+ val window = activity.window
177
+
178
+ val screenForNavBarHidden = findScreenForTrait(screen, WindowTraits.NAVIGATION_BAR_HIDDEN)
179
+ val hidden = screenForNavBarHidden?.isNavigationBarHidden ?: false
180
+
181
+ WindowCompat.setDecorFitsSystemWindows(window, hidden)
182
+ if (hidden) {
183
+ WindowInsetsControllerCompat(window, window.decorView).let { controller ->
184
+ controller.hide(WindowInsetsCompat.Type.navigationBars())
185
+ controller.systemBarsBehavior =
186
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
187
+ }
188
+ } else {
189
+ WindowInsetsControllerCompat(
190
+ window,
191
+ window.decorView
192
+ ).show(WindowInsetsCompat.Type.navigationBars())
193
+ }
194
+ }
195
+
147
196
  internal fun trySetWindowTraits(screen: Screen, activity: Activity?, context: ReactContext?) {
148
197
  if (mDidSetOrientation) {
149
198
  setOrientation(screen, activity)
@@ -154,6 +203,10 @@ object ScreenWindowTraits {
154
203
  setTranslucent(screen, activity, context)
155
204
  setHidden(screen, activity)
156
205
  }
206
+ if (mDidSetNavigationBarAppearance) {
207
+ setNavigationBarColor(screen, activity)
208
+ setNavigationBarHidden(screen, activity)
209
+ }
157
210
  }
158
211
 
159
212
  private fun findScreenForTrait(screen: Screen, trait: WindowTraits): Screen? {
@@ -211,6 +264,14 @@ object ScreenWindowTraits {
211
264
  WindowTraits.TRANSLUCENT -> screen.isStatusBarTranslucent != null
212
265
  WindowTraits.HIDDEN -> screen.isStatusBarHidden != null
213
266
  WindowTraits.ANIMATED -> screen.isStatusBarAnimated != null
267
+ WindowTraits.NAVIGATION_BAR_COLOR -> screen.navigationBarColor != null
268
+ WindowTraits.NAVIGATION_BAR_HIDDEN -> screen.isNavigationBarHidden != null
214
269
  }
215
270
  }
271
+
272
+ private fun isColorLight(color: Int): Boolean {
273
+ val darkness: Double =
274
+ 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
275
+ return darkness < 0.5
276
+ }
216
277
  }