react-native-screens 3.7.2 → 3.10.1

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 (83) hide show
  1. package/README.md +69 -3
  2. package/android/build.gradle +8 -7
  3. package/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt +71 -0
  4. package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +7 -0
  5. package/android/src/main/java/com/swmansion/rnscreens/FragmentBackPressOverrider.kt +29 -0
  6. package/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +2 -1
  7. package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +7 -41
  8. package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +55 -40
  9. package/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +19 -1
  10. package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +30 -101
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +76 -14
  12. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +13 -4
  13. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +8 -0
  14. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +7 -1
  15. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.kt +1 -0
  16. package/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt +90 -0
  17. package/android/src/main/java/com/swmansion/rnscreens/SearchBarView.kt +150 -0
  18. package/android/src/main/java/com/swmansion/rnscreens/SearchViewFormatter.kt +40 -0
  19. package/ios/RNSScreen.h +1 -0
  20. package/ios/RNSScreen.m +35 -0
  21. package/ios/RNSScreenContainer.h +2 -0
  22. package/ios/RNSScreenStack.m +24 -6
  23. package/ios/RNSScreenStackHeaderConfig.m +45 -2
  24. package/ios/RNSScreenWindowTraits.h +5 -0
  25. package/ios/RNSScreenWindowTraits.m +29 -0
  26. package/lib/commonjs/index.js +24 -1
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/index.native.js +103 -17
  29. package/lib/commonjs/index.native.js.map +1 -1
  30. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js +67 -0
  31. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js.map +1 -0
  32. package/lib/commonjs/native-stack/views/HeaderConfig.js +46 -4
  33. package/lib/commonjs/native-stack/views/HeaderConfig.js.map +1 -1
  34. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js +60 -0
  35. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  36. package/lib/commonjs/reanimated/ReanimatedScreen.js +7 -79
  37. package/lib/commonjs/reanimated/ReanimatedScreen.js.map +1 -1
  38. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js +61 -0
  39. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js.map +1 -0
  40. package/lib/commonjs/reanimated/index.js +2 -2
  41. package/lib/commonjs/reanimated/index.js.map +1 -1
  42. package/lib/commonjs/utils.js +20 -0
  43. package/lib/commonjs/utils.js.map +1 -0
  44. package/lib/module/index.js +5 -0
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.native.js +99 -19
  47. package/lib/module/index.native.js.map +1 -1
  48. package/lib/module/native-stack/utils/useBackPressSubscription.js +50 -0
  49. package/lib/module/native-stack/utils/useBackPressSubscription.js.map +1 -0
  50. package/lib/module/native-stack/views/HeaderConfig.js +46 -5
  51. package/lib/module/native-stack/views/HeaderConfig.js.map +1 -1
  52. package/lib/module/reanimated/ReanimatedNativeStackScreen.js +40 -0
  53. package/lib/module/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  54. package/lib/module/reanimated/ReanimatedScreen.js +6 -73
  55. package/lib/module/reanimated/ReanimatedScreen.js.map +1 -1
  56. package/lib/module/reanimated/ReanimatedScreenProvider.js +49 -0
  57. package/lib/module/reanimated/ReanimatedScreenProvider.js.map +1 -0
  58. package/lib/module/reanimated/index.js +1 -1
  59. package/lib/module/reanimated/index.js.map +1 -1
  60. package/lib/module/utils.js +8 -0
  61. package/lib/module/utils.js.map +1 -0
  62. package/lib/typescript/index.d.ts +2 -0
  63. package/lib/typescript/native-stack/types.d.ts +0 -2
  64. package/lib/typescript/native-stack/utils/useBackPressSubscription.d.ts +16 -0
  65. package/lib/typescript/reanimated/ReanimatedNativeStackScreen.d.ts +5 -0
  66. package/lib/typescript/reanimated/ReanimatedScreen.d.ts +5 -2
  67. package/lib/typescript/reanimated/ReanimatedScreenProvider.d.ts +2 -0
  68. package/lib/typescript/reanimated/index.d.ts +1 -1
  69. package/lib/typescript/types.d.ts +46 -1
  70. package/lib/typescript/utils.d.ts +2 -0
  71. package/native-stack/README.md +35 -7
  72. package/package.json +5 -2
  73. package/src/index.native.tsx +138 -43
  74. package/src/index.tsx +10 -0
  75. package/src/native-stack/types.tsx +0 -2
  76. package/src/native-stack/utils/useBackPressSubscription.tsx +66 -0
  77. package/src/native-stack/views/HeaderConfig.tsx +46 -3
  78. package/src/reanimated/ReanimatedNativeStackScreen.tsx +61 -0
  79. package/src/reanimated/ReanimatedScreen.tsx +6 -84
  80. package/src/reanimated/ReanimatedScreenProvider.tsx +42 -0
  81. package/src/reanimated/index.tsx +1 -1
  82. package/src/types.tsx +46 -1
  83. package/src/utils.ts +12 -0
@@ -3,8 +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.Fragment
7
- import androidx.fragment.app.FragmentManager
8
6
  import androidx.fragment.app.FragmentTransaction
9
7
  import com.facebook.react.bridge.ReactContext
10
8
  import com.facebook.react.uimanager.UIManagerModule
@@ -20,21 +18,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
20
18
  private val drawingOpPool: MutableList<DrawingOp> = ArrayList()
21
19
  private val drawingOps: MutableList<DrawingOp> = ArrayList()
22
20
  private var mTopScreen: ScreenStackFragment? = null
23
- private val mBackStackListener = FragmentManager.OnBackStackChangedListener {
24
- if (mFragmentManager?.backStackEntryCount == 0) {
25
- // when back stack entry count hits 0 it means the user's navigated back using hw back
26
- // button. As the "fake" transaction we installed on the back stack does nothing we need
27
- // to handle back navigation on our own.
28
- mTopScreen?.let { dismiss(it) }
29
- }
30
- }
31
- private val mLifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
32
- override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
33
- if (mTopScreen === f) {
34
- setupBackHandlerIfNeeded(f)
35
- }
36
- }
37
- }
38
21
  private var mRemovalTransitionStarted = false
39
22
  private var isDetachingCurrentScreen = false
40
23
  private var reverseLastTwoChildren = false
@@ -65,28 +48,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
65
48
  return ScreenStackFragment(screen)
66
49
  }
67
50
 
68
- override fun onDetachedFromWindow() {
69
- mFragmentManager?.let {
70
- it.removeOnBackStackChangedListener(mBackStackListener)
71
- it.unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks)
72
- if (!it.isStateSaved && !it.isDestroyed) {
73
- // State save means that the container where fragment manager was installed has been
74
- // unmounted.
75
- // This could happen as a result of dismissing nested stack. In such a case we don't need to
76
- // reset back stack as it'd result in a crash caused by the fact the fragment manager is no
77
- // longer attached.
78
- it.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
79
- }
80
- }
81
- super.onDetachedFromWindow()
82
- }
83
-
84
- override fun onAttachedToWindow() {
85
- super.onAttachedToWindow()
86
- val fragmentManager = requireNotNull(mFragmentManager, { "mFragmentManager is null when ScreenStack attached to window" })
87
- fragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false)
88
- }
89
-
90
51
  override fun startViewTransition(view: View) {
91
52
  super.startViewTransition(view)
92
53
  mRemovalTransitionStarted = true
@@ -159,11 +120,11 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
159
120
  // if the previous top screen does not exist anymore and the new top was not on the stack
160
121
  // before, probably replace or reset was called, so we play the "close animation".
161
122
  // Otherwise it's open animation
162
- shouldUseOpenAnimation = (
163
- mScreenFragments.contains(mTopScreen) ||
164
- newTop.screen.replaceAnimation !== Screen.ReplaceAnimation.POP
165
- )
166
- stackAnimation = newTop.screen.stackAnimation
123
+ val containsTopScreen = mTopScreen?.let { mScreenFragments.contains(it) } == true
124
+ val isPushReplace = newTop.screen.replaceAnimation === Screen.ReplaceAnimation.PUSH
125
+ shouldUseOpenAnimation = containsTopScreen || isPushReplace
126
+ // if the replace animation is `push`, the new top screen provides the animation, otherwise the previous one
127
+ stackAnimation = if (shouldUseOpenAnimation) newTop.screen.stackAnimation else mTopScreen?.screen?.stackAnimation
167
128
  } else if (mTopScreen == null && newTop != null) {
168
129
  // mTopScreen was not present before so newTop is the first screen added to a stack
169
130
  // and we don't want the animation when it is entering, but we want to send the
@@ -277,69 +238,38 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
277
238
  mTopScreen = newTop
278
239
  mStack.clear()
279
240
  mStack.addAll(mScreenFragments)
241
+
242
+ turnOffA11yUnderTransparentScreen(visibleBottom)
243
+
280
244
  it.commitNowAllowingStateLoss()
281
- mTopScreen?.let { screen -> setupBackHandlerIfNeeded(screen) }
282
245
  }
283
246
  }
284
247
 
285
- override fun notifyContainerUpdate() {
286
- for (screen in mStack) {
287
- screen.onContainerUpdate()
248
+ // only top visible screen should be accessible
249
+ private fun turnOffA11yUnderTransparentScreen(visibleBottom: ScreenStackFragment?) {
250
+ if (mScreenFragments.size > 1 && visibleBottom != null) {
251
+ mTopScreen?.let {
252
+ if (isTransparent(it)) {
253
+ val screenFragmentsBeneathTop = mScreenFragments.slice(0 until mScreenFragments.size - 1).asReversed()
254
+ // go from the top of the stack excluding the top screen
255
+ for (screenFragment in screenFragmentsBeneathTop) {
256
+ screenFragment.screen.changeAccessibilityMode(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
257
+
258
+ // don't change a11y below non-transparent screens
259
+ if (screenFragment == visibleBottom) {
260
+ break
261
+ }
262
+ }
263
+ }
264
+ }
288
265
  }
266
+
267
+ topScreen?.changeAccessibilityMode(IMPORTANT_FOR_ACCESSIBILITY_AUTO)
289
268
  }
290
269
 
291
- /**
292
- * The below method sets up fragment manager's back stack in a way that it'd trigger our back
293
- * stack change listener when hw back button is clicked.
294
- *
295
- *
296
- * Because back stack by default rolls back the transaction the stack entry is associated with
297
- * we generate a "fake" transaction that hides and shows the top fragment. As a result when back
298
- * stack entry is rolled back nothing happens and we are free to handle back navigation on our own
299
- * in `mBackStackListener`.
300
- *
301
- *
302
- * We pop that "fake" transaction each time we update stack and we add a new one in case the
303
- * top screen is allowed to be dismissed using hw back button. This way in the listener we can
304
- * tell if back button was pressed based on the count of the items on back stack. We expect 0
305
- * items in case hw back is pressed because we try to keep the number of items at 1 by always
306
- * resetting and adding new items. In case we don't add a new item to back stack we remove
307
- * listener so that it does not get triggered.
308
- *
309
- *
310
- * It is important that we don't install back handler when stack contains a single screen as in
311
- * that case we want the parent navigator or activity handler to take over.
312
- */
313
- private fun setupBackHandlerIfNeeded(topScreen: ScreenStackFragment) {
314
- if (mTopScreen?.isResumed != true) {
315
- // if the top fragment is not in a resumed state, adding back stack transaction would throw.
316
- // In such a case we skip installing back handler and use FragmentLifecycleCallbacks to get
317
- // notified when it gets resumed so that we can install the handler.
318
- return
319
- }
320
- mFragmentManager?.let {
321
- it.removeOnBackStackChangedListener(mBackStackListener)
322
- it.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE)
323
- var firstScreen: ScreenStackFragment? = null
324
- var i = 0
325
- val size = mStack.size
326
- while (i < size) {
327
- val screen = mStack[i]
328
- if (!mDismissed.contains(screen)) {
329
- firstScreen = screen
330
- break
331
- }
332
- i++
333
- }
334
- if (topScreen !== firstScreen && topScreen.isDismissible) {
335
- it
336
- .beginTransaction()
337
- .show(topScreen)
338
- .addToBackStack(BACK_STACK_TAG)
339
- .setPrimaryNavigationFragment(topScreen)
340
- .commitNowAllowingStateLoss()
341
- it.addOnBackStackChangedListener(mBackStackListener)
342
- }
270
+ override fun notifyContainerUpdate() {
271
+ for (screen in mStack) {
272
+ screen.onContainerUpdate()
343
273
  }
344
274
  }
345
275
 
@@ -418,7 +348,6 @@ class ScreenStack(context: Context?) : ScreenContainer<ScreenStackFragment>(cont
418
348
  }
419
349
 
420
350
  companion object {
421
- private const val BACK_STACK_TAG = "RN_SCREEN_LAST"
422
351
  private fun isSystemAnimation(stackAnimation: StackAnimation): Boolean {
423
352
  return stackAnimation === StackAnimation.DEFAULT || stackAnimation === StackAnimation.FADE || stackAnimation === StackAnimation.NONE
424
353
  }
@@ -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
@@ -24,6 +27,9 @@ class ScreenStackFragment : ScreenFragment {
24
27
  private var mShadowHidden = false
25
28
  private var mIsTranslucent = false
26
29
 
30
+ var searchView: CustomSearchView? = null
31
+ var onSearchViewCreate: ((searchView: CustomSearchView) -> Unit)? = null
32
+
27
33
  @SuppressLint("ValidFragment")
28
34
  constructor(screenView: Screen) : super(screenView)
29
35
 
@@ -64,7 +70,8 @@ class ScreenStackFragment : ScreenFragment {
64
70
  fun setToolbarTranslucent(translucent: Boolean) {
65
71
  if (mIsTranslucent != translucent) {
66
72
  val params = screen.layoutParams
67
- (params as CoordinatorLayout.LayoutParams).behavior = if (translucent) null else ScrollingViewBehavior()
73
+ (params as CoordinatorLayout.LayoutParams).behavior =
74
+ if (translucent) null else ScrollingViewBehavior()
68
75
  mIsTranslucent = translucent
69
76
  }
70
77
  }
@@ -120,7 +127,8 @@ class ScreenStackFragment : ScreenFragment {
120
127
  container: ViewGroup?,
121
128
  savedInstanceState: Bundle?
122
129
  ): View? {
123
- val view: NotifyingCoordinatorLayout? = context?.let { NotifyingCoordinatorLayout(it, this) }
130
+ val view: ScreensCoordinatorLayout? =
131
+ context?.let { ScreensCoordinatorLayout(it, this) }
124
132
  val params = CoordinatorLayout.LayoutParams(
125
133
  LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT
126
134
  )
@@ -142,11 +150,48 @@ class ScreenStackFragment : ScreenFragment {
142
150
  mAppBarLayout?.targetElevation = 0f
143
151
  }
144
152
  mToolbar?.let { mAppBarLayout?.addView(recycleView(it)) }
153
+ setHasOptionsMenu(true)
145
154
  return view
146
155
  }
147
156
 
148
- val isDismissible: Boolean
149
- get() = screen.isGestureEnabled
157
+ override fun onPrepareOptionsMenu(menu: Menu) {
158
+ updateToolbarMenu(menu)
159
+ return super.onPrepareOptionsMenu(menu)
160
+ }
161
+
162
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
163
+ updateToolbarMenu(menu)
164
+ return super.onCreateOptionsMenu(menu, inflater)
165
+ }
166
+
167
+ private fun shouldShowSearchBar(): Boolean {
168
+ val config = screen.headerConfig
169
+ val numberOfSubViews = config?.configSubviewsCount ?: 0
170
+ if (config != null && numberOfSubViews > 0) {
171
+ for (i in 0 until numberOfSubViews) {
172
+ val subView = config.getConfigSubview(i)
173
+ if (subView.type == ScreenStackHeaderSubview.Type.SEARCH_BAR) {
174
+ return true
175
+ }
176
+ }
177
+ }
178
+ return false
179
+ }
180
+
181
+ private fun updateToolbarMenu(menu: Menu) {
182
+ menu.clear()
183
+ if (shouldShowSearchBar()) {
184
+ val currentContext = context
185
+ if (searchView == null && currentContext != null) {
186
+ val newSearchView = CustomSearchView(currentContext, this)
187
+ searchView = newSearchView
188
+ onSearchViewCreate?.invoke(newSearchView)
189
+ }
190
+ val item = menu.add("")
191
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
192
+ item.actionView = searchView
193
+ }
194
+ }
150
195
 
151
196
  fun canNavigateBack(): Boolean {
152
197
  val container: ScreenContainer<*>? = screen.container
@@ -171,18 +216,22 @@ class ScreenStackFragment : ScreenFragment {
171
216
  container.dismiss(this)
172
217
  }
173
218
 
174
- private class NotifyingCoordinatorLayout(context: Context, private val mFragment: ScreenFragment) : CoordinatorLayout(context) {
175
- private val mAnimationListener: Animation.AnimationListener = object : Animation.AnimationListener {
176
- override fun onAnimationStart(animation: Animation) {
177
- mFragment.onViewAnimationStart()
178
- }
219
+ private class ScreensCoordinatorLayout(
220
+ context: Context,
221
+ private val mFragment: ScreenFragment
222
+ ) : CoordinatorLayout(context) {
223
+ private val mAnimationListener: Animation.AnimationListener =
224
+ object : Animation.AnimationListener {
225
+ override fun onAnimationStart(animation: Animation) {
226
+ mFragment.onViewAnimationStart()
227
+ }
179
228
 
180
- override fun onAnimationEnd(animation: Animation) {
181
- mFragment.onViewAnimationEnd()
182
- }
229
+ override fun onAnimationEnd(animation: Animation) {
230
+ mFragment.onViewAnimationEnd()
231
+ }
183
232
 
184
- override fun onAnimationRepeat(animation: Animation) {}
185
- }
233
+ override fun onAnimationRepeat(animation: Animation) {}
234
+ }
186
235
 
187
236
  override fun startAnimation(animation: Animation) {
188
237
  // For some reason View##onAnimationEnd doesn't get called for
@@ -208,6 +257,19 @@ class ScreenStackFragment : ScreenFragment {
208
257
  super.startAnimation(set)
209
258
  }
210
259
  }
260
+
261
+ /**
262
+ * This method implements a workaround for RN's autoFocus functionality. Because of the way
263
+ * autoFocus is implemented it dismisses soft keyboard in fragment transition
264
+ * due to change of visibility of the view at the start of the transition. Here we override the
265
+ * call to `clearFocus` when the visibility of view is `INVISIBLE` since `clearFocus` triggers the
266
+ * hiding of the keyboard in `ReactEditText.java`.
267
+ */
268
+ override fun clearFocus() {
269
+ if (visibility != INVISIBLE) {
270
+ super.clearFocus()
271
+ }
272
+ }
211
273
  }
212
274
 
213
275
  private class ScreensAnimation(private val mFragment: ScreenFragment) : Animation() {
@@ -17,11 +17,13 @@ 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
25
27
  private var mTitle: String? = null
26
28
  private var mTitleColor = 0
27
29
  private var mTitleFontFamily: String? = null
@@ -62,6 +64,11 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
62
64
  }
63
65
  }
64
66
 
67
+ private fun sendEvent(eventName: String, eventContent: WritableMap?) {
68
+ (context as ReactContext).getJSModule(RCTEventEmitter::class.java)
69
+ ?.receiveEvent(id, eventName, eventContent)
70
+ }
71
+
65
72
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
66
73
  // no-op
67
74
  }
@@ -73,12 +80,14 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
73
80
  override fun onAttachedToWindow() {
74
81
  super.onAttachedToWindow()
75
82
  mIsAttachedToWindow = true
83
+ sendEvent("onAttached", null)
76
84
  onUpdate()
77
85
  }
78
86
 
79
87
  override fun onDetachedFromWindow() {
80
88
  super.onDetachedFromWindow()
81
89
  mIsAttachedToWindow = false
90
+ sendEvent("onDetached", null)
82
91
  }
83
92
 
84
93
  private val screen: Screen?
@@ -99,7 +108,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
99
108
  }
100
109
  return null
101
110
  }
102
- private val screenFragment: ScreenStackFragment?
111
+ val screenFragment: ScreenStackFragment?
103
112
  get() {
104
113
  val screen = parent
105
114
  if (screen is Screen) {
@@ -369,7 +378,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
369
378
  mDirection = direction
370
379
  }
371
380
 
372
- private class DebugMenuToolbar(context: Context) : Toolbar(context) {
381
+ private class DebugMenuToolbar(context: Context, config: ScreenStackHeaderConfig) : CustomToolbar(context, config) {
373
382
  override fun showOverflowMenu(): Boolean {
374
383
  (context.applicationContext as ReactApplication)
375
384
  .reactNativeHost
@@ -381,7 +390,7 @@ class ScreenStackHeaderConfig(context: Context) : ViewGroup(context) {
381
390
 
382
391
  init {
383
392
  visibility = GONE
384
- toolbar = if (BuildConfig.DEBUG) DebugMenuToolbar(context) else Toolbar(context)
393
+ toolbar = if (BuildConfig.DEBUG) DebugMenuToolbar(context, this) else CustomToolbar(context, this)
385
394
  mDefaultStartInset = toolbar.contentInsetStart
386
395
  mDefaultStartInsetWithNavigation = toolbar.contentInsetStartWithNavigation
387
396
 
@@ -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
  }
@@ -0,0 +1,90 @@
1
+ package com.swmansion.rnscreens
2
+
3
+ import com.facebook.react.bridge.JSApplicationIllegalArgumentException
4
+ import com.facebook.react.common.MapBuilder
5
+ import com.facebook.react.module.annotations.ReactModule
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.ViewGroupManager
8
+ import com.facebook.react.uimanager.annotations.ReactProp
9
+
10
+ @ReactModule(name = SearchBarManager.REACT_CLASS)
11
+ class SearchBarManager : ViewGroupManager<SearchBarView>() {
12
+ override fun getName(): String {
13
+ return REACT_CLASS
14
+ }
15
+
16
+ override fun createViewInstance(context: ThemedReactContext): SearchBarView {
17
+ return SearchBarView(context)
18
+ }
19
+
20
+ override fun onAfterUpdateTransaction(view: SearchBarView) {
21
+ super.onAfterUpdateTransaction(view)
22
+ view.onUpdate()
23
+ }
24
+
25
+ @ReactProp(name = "autoCapitalize")
26
+ fun setAutoCapitalize(view: SearchBarView, autoCapitalize: String?) {
27
+ view.autoCapitalize = when (autoCapitalize) {
28
+ null, "none" -> SearchBarView.SearchBarAutoCapitalize.NONE
29
+ "words" -> SearchBarView.SearchBarAutoCapitalize.WORDS
30
+ "sentences" -> SearchBarView.SearchBarAutoCapitalize.SENTENCES
31
+ "characters" -> SearchBarView.SearchBarAutoCapitalize.CHARACTERS
32
+ else -> throw JSApplicationIllegalArgumentException(
33
+ "Forbidden auto capitalize value passed"
34
+ )
35
+ }
36
+ }
37
+
38
+ @ReactProp(name = "autoFocus")
39
+ fun setAutoFocus(view: SearchBarView, autoFocus: Boolean?) {
40
+ view.autoFocus = autoFocus ?: false
41
+ }
42
+
43
+ @ReactProp(name = "barTintColor", customType = "Color")
44
+ fun setTintColor(view: SearchBarView, color: Int?) {
45
+ view.tintColor = color
46
+ }
47
+
48
+ @ReactProp(name = "disableBackButtonOverride")
49
+ fun setDisableBackButtonOverride(view: SearchBarView, disableBackButtonOverride: Boolean?) {
50
+ view.shouldOverrideBackButton = disableBackButtonOverride != true
51
+ }
52
+
53
+ @ReactProp(name = "inputType")
54
+ fun setInputType(view: SearchBarView, inputType: String?) {
55
+ view.inputType = when (inputType) {
56
+ null, "text" -> SearchBarView.SearchBarInputTypes.TEXT
57
+ "phone" -> SearchBarView.SearchBarInputTypes.PHONE
58
+ "number" -> SearchBarView.SearchBarInputTypes.NUMBER
59
+ "email" -> SearchBarView.SearchBarInputTypes.EMAIL
60
+ else -> throw JSApplicationIllegalArgumentException(
61
+ "Forbidden input type value"
62
+ )
63
+ }
64
+ }
65
+
66
+ @ReactProp(name = "placeholder")
67
+ fun setPlaceholder(view: SearchBarView, placeholder: String?) {
68
+ view.placeholder = placeholder
69
+ }
70
+
71
+ @ReactProp(name = "textColor", customType = "Color")
72
+ fun setTextColor(view: SearchBarView, color: Int?) {
73
+ view.textColor = color
74
+ }
75
+
76
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
77
+ return MapBuilder.builder<String, Any>()
78
+ .put("onChangeText", MapBuilder.of("registrationName", "onChangeText"))
79
+ .put("onSearchButtonPress", MapBuilder.of("registrationName", "onSearchButtonPress"))
80
+ .put("onFocus", MapBuilder.of("registrationName", "onFocus"))
81
+ .put("onBlur", MapBuilder.of("registrationName", "onBlur"))
82
+ .put("onClose", MapBuilder.of("registrationName", "onClose"))
83
+ .put("onOpen", MapBuilder.of("registrationName", "onOpen"))
84
+ .build()
85
+ }
86
+
87
+ companion object {
88
+ const val REACT_CLASS = "RNSSearchBar"
89
+ }
90
+ }
@@ -0,0 +1,150 @@
1
+ package com.swmansion.rnscreens
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.text.InputType
5
+ import androidx.appcompat.widget.SearchView
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.ReactContext
8
+ import com.facebook.react.bridge.WritableMap
9
+ import com.facebook.react.uimanager.events.RCTEventEmitter
10
+ import com.facebook.react.views.view.ReactViewGroup
11
+
12
+ @SuppressLint("ViewConstructor")
13
+ class SearchBarView(reactContext: ReactContext?) : ReactViewGroup(reactContext) {
14
+ var inputType: SearchBarInputTypes = SearchBarInputTypes.TEXT
15
+ var autoCapitalize: SearchBarAutoCapitalize = SearchBarAutoCapitalize.NONE
16
+ var textColor: Int? = null
17
+ var tintColor: Int? = null
18
+ var placeholder: String? = null
19
+ var shouldOverrideBackButton: Boolean = true
20
+ var autoFocus: Boolean = false
21
+
22
+ private var mSearchViewFormatter: SearchViewFormatter? = null
23
+
24
+ private var mAreListenersSet: Boolean = false
25
+
26
+ private val screenStackFragment: ScreenStackFragment?
27
+ get() {
28
+ val currentParent = parent
29
+ if (currentParent is ScreenStackHeaderSubview) {
30
+ return currentParent.config?.screenFragment
31
+ }
32
+ return null
33
+ }
34
+
35
+ fun onUpdate() {
36
+ setSearchViewProps()
37
+ }
38
+
39
+ private fun setSearchViewProps() {
40
+ val searchView = screenStackFragment?.searchView
41
+ if (searchView != null) {
42
+ if (!mAreListenersSet) {
43
+ setSearchViewListeners(searchView)
44
+ mAreListenersSet = true
45
+ }
46
+
47
+ searchView.inputType = inputType.toAndroidInputType(autoCapitalize)
48
+ searchView.queryHint = placeholder
49
+ mSearchViewFormatter?.setTextColor(textColor)
50
+ mSearchViewFormatter?.setTintColor(tintColor)
51
+ searchView.overrideBackAction = shouldOverrideBackButton
52
+ }
53
+ }
54
+
55
+ override fun onAttachedToWindow() {
56
+ super.onAttachedToWindow()
57
+
58
+ screenStackFragment?.onSearchViewCreate = { newSearchView ->
59
+ if (mSearchViewFormatter == null) mSearchViewFormatter =
60
+ SearchViewFormatter(newSearchView)
61
+ setSearchViewProps()
62
+ if (autoFocus) {
63
+ screenStackFragment?.searchView?.focus()
64
+ }
65
+ }
66
+ }
67
+
68
+ private fun setSearchViewListeners(searchView: SearchView) {
69
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
70
+ override fun onQueryTextChange(newText: String?): Boolean {
71
+ handleTextChange(newText)
72
+ return true
73
+ }
74
+
75
+ override fun onQueryTextSubmit(query: String?): Boolean {
76
+ handleTextSubmit(query)
77
+ return true
78
+ }
79
+ })
80
+ searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
81
+ handleFocusChange(hasFocus)
82
+ }
83
+ searchView.setOnCloseListener {
84
+ handleClose()
85
+ false
86
+ }
87
+ searchView.setOnSearchClickListener {
88
+ handleOpen()
89
+ }
90
+ }
91
+
92
+ private fun handleTextChange(newText: String?) {
93
+ val event = Arguments.createMap()
94
+ event.putString("text", newText)
95
+ sendEvent("onChangeText", event)
96
+ }
97
+
98
+ private fun handleFocusChange(hasFocus: Boolean) {
99
+ sendEvent(if (hasFocus) "onFocus" else "onBlur", null)
100
+ }
101
+
102
+ private fun handleClose() {
103
+ sendEvent("onClose", null)
104
+ }
105
+
106
+ private fun handleOpen() {
107
+ sendEvent("onOpen", null)
108
+ }
109
+
110
+ private fun handleTextSubmit(newText: String?) {
111
+ val event = Arguments.createMap()
112
+ event.putString("text", newText)
113
+ sendEvent("onSearchButtonPress", event)
114
+ }
115
+
116
+ private fun sendEvent(eventName: String, eventContent: WritableMap?) {
117
+ (context as ReactContext).getJSModule(RCTEventEmitter::class.java)
118
+ ?.receiveEvent(id, eventName, eventContent)
119
+ }
120
+
121
+ enum class SearchBarAutoCapitalize {
122
+ NONE, WORDS, SENTENCES, CHARACTERS
123
+ }
124
+
125
+ enum class SearchBarInputTypes {
126
+ TEXT {
127
+ override fun toAndroidInputType(capitalize: SearchBarAutoCapitalize) =
128
+ when (capitalize) {
129
+ SearchBarAutoCapitalize.NONE -> InputType.TYPE_CLASS_TEXT
130
+ SearchBarAutoCapitalize.WORDS -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
131
+ SearchBarAutoCapitalize.SENTENCES -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
132
+ SearchBarAutoCapitalize.CHARACTERS -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
133
+ }
134
+ },
135
+ PHONE {
136
+ override fun toAndroidInputType(capitalize: SearchBarAutoCapitalize) =
137
+ InputType.TYPE_CLASS_PHONE
138
+ },
139
+ NUMBER {
140
+ override fun toAndroidInputType(capitalize: SearchBarAutoCapitalize) =
141
+ InputType.TYPE_CLASS_NUMBER
142
+ },
143
+ EMAIL {
144
+ override fun toAndroidInputType(capitalize: SearchBarAutoCapitalize) =
145
+ InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
146
+ };
147
+
148
+ abstract fun toAndroidInputType(capitalize: SearchBarAutoCapitalize): Int
149
+ }
150
+ }