react-native-screens 3.8.0 → 3.10.2

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 (79) hide show
  1. package/README.md +61 -3
  2. package/android/build.gradle +0 -2
  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 -5
  11. package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +77 -12
  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.m +35 -0
  20. package/ios/RNSScreenStack.m +24 -6
  21. package/ios/RNSScreenStackHeaderConfig.m +41 -0
  22. package/lib/commonjs/index.js +24 -1
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/index.native.js +101 -11
  25. package/lib/commonjs/index.native.js.map +1 -1
  26. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js +67 -0
  27. package/lib/commonjs/native-stack/utils/useBackPressSubscription.js.map +1 -0
  28. package/lib/commonjs/native-stack/views/HeaderConfig.js +46 -4
  29. package/lib/commonjs/native-stack/views/HeaderConfig.js.map +1 -1
  30. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js +60 -0
  31. package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  32. package/lib/commonjs/reanimated/ReanimatedScreen.js +7 -79
  33. package/lib/commonjs/reanimated/ReanimatedScreen.js.map +1 -1
  34. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js +61 -0
  35. package/lib/commonjs/reanimated/ReanimatedScreenProvider.js.map +1 -0
  36. package/lib/commonjs/reanimated/index.js +2 -2
  37. package/lib/commonjs/reanimated/index.js.map +1 -1
  38. package/lib/commonjs/utils.js +20 -0
  39. package/lib/commonjs/utils.js.map +1 -0
  40. package/lib/module/index.js +5 -0
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/index.native.js +97 -13
  43. package/lib/module/index.native.js.map +1 -1
  44. package/lib/module/native-stack/utils/useBackPressSubscription.js +50 -0
  45. package/lib/module/native-stack/utils/useBackPressSubscription.js.map +1 -0
  46. package/lib/module/native-stack/views/HeaderConfig.js +46 -5
  47. package/lib/module/native-stack/views/HeaderConfig.js.map +1 -1
  48. package/lib/module/reanimated/ReanimatedNativeStackScreen.js +40 -0
  49. package/lib/module/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
  50. package/lib/module/reanimated/ReanimatedScreen.js +6 -73
  51. package/lib/module/reanimated/ReanimatedScreen.js.map +1 -1
  52. package/lib/module/reanimated/ReanimatedScreenProvider.js +49 -0
  53. package/lib/module/reanimated/ReanimatedScreenProvider.js.map +1 -0
  54. package/lib/module/reanimated/index.js +1 -1
  55. package/lib/module/reanimated/index.js.map +1 -1
  56. package/lib/module/utils.js +8 -0
  57. package/lib/module/utils.js.map +1 -0
  58. package/lib/typescript/index.d.ts +2 -0
  59. package/lib/typescript/native-stack/types.d.ts +0 -2
  60. package/lib/typescript/native-stack/utils/useBackPressSubscription.d.ts +16 -0
  61. package/lib/typescript/reanimated/ReanimatedNativeStackScreen.d.ts +5 -0
  62. package/lib/typescript/reanimated/ReanimatedScreen.d.ts +5 -2
  63. package/lib/typescript/reanimated/ReanimatedScreenProvider.d.ts +2 -0
  64. package/lib/typescript/reanimated/index.d.ts +1 -1
  65. package/lib/typescript/types.d.ts +46 -1
  66. package/lib/typescript/utils.d.ts +2 -0
  67. package/native-stack/README.md +35 -7
  68. package/package.json +5 -2
  69. package/src/index.native.tsx +134 -38
  70. package/src/index.tsx +10 -0
  71. package/src/native-stack/types.tsx +0 -2
  72. package/src/native-stack/utils/useBackPressSubscription.tsx +66 -0
  73. package/src/native-stack/views/HeaderConfig.tsx +46 -3
  74. package/src/reanimated/ReanimatedNativeStackScreen.tsx +61 -0
  75. package/src/reanimated/ReanimatedScreen.tsx +6 -84
  76. package/src/reanimated/ReanimatedScreenProvider.tsx +42 -0
  77. package/src/reanimated/index.tsx +1 -1
  78. package/src/types.tsx +46 -1
  79. package/src/utils.ts +12 -0
@@ -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,9 +150,49 @@ 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
 
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
+ }
195
+
148
196
  fun canNavigateBack(): Boolean {
149
197
  val container: ScreenContainer<*>? = screen.container
150
198
  check(container is ScreenStack) { "ScreenStackFragment added into a non-stack container" }
@@ -168,18 +216,22 @@ class ScreenStackFragment : ScreenFragment {
168
216
  container.dismiss(this)
169
217
  }
170
218
 
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
- }
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
+ }
176
228
 
177
- override fun onAnimationEnd(animation: Animation) {
178
- mFragment.onViewAnimationEnd()
179
- }
229
+ override fun onAnimationEnd(animation: Animation) {
230
+ mFragment.onViewAnimationEnd()
231
+ }
180
232
 
181
- override fun onAnimationRepeat(animation: Animation) {}
182
- }
233
+ override fun onAnimationRepeat(animation: Animation) {}
234
+ }
183
235
 
184
236
  override fun startAnimation(animation: Animation) {
185
237
  // For some reason View##onAnimationEnd doesn't get called for
@@ -205,6 +257,19 @@ class ScreenStackFragment : ScreenFragment {
205
257
  super.startAnimation(set)
206
258
  }
207
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
+ }
208
273
  }
209
274
 
210
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
+ }
@@ -0,0 +1,40 @@
1
+ package com.swmansion.rnscreens
2
+
3
+ import android.graphics.drawable.Drawable
4
+ import android.view.View
5
+ import android.widget.EditText
6
+ import androidx.appcompat.widget.SearchView
7
+
8
+ class SearchViewFormatter(var searchView: SearchView) {
9
+ private var mDefaultTextColor: Int? = null
10
+ private var mDefaultTintBackground: Drawable? = null
11
+
12
+ private val searchEditText
13
+ get() = searchView.findViewById<View>(androidx.appcompat.R.id.search_src_text) as? EditText
14
+ private val searchTextPlate
15
+ get() = searchView.findViewById<View>(androidx.appcompat.R.id.search_plate)
16
+
17
+ fun setTextColor(textColor: Int?) {
18
+ val currentDefaultTextColor = mDefaultTextColor
19
+ if (textColor != null) {
20
+ if (mDefaultTextColor == null) {
21
+ mDefaultTextColor = searchEditText?.textColors?.defaultColor
22
+ }
23
+ searchEditText?.setTextColor(textColor)
24
+ } else if (currentDefaultTextColor != null) {
25
+ searchEditText?.setTextColor(currentDefaultTextColor)
26
+ }
27
+ }
28
+
29
+ fun setTintColor(tintColor: Int?) {
30
+ val currentDefaultTintColor = mDefaultTintBackground
31
+ if (tintColor != null) {
32
+ if (mDefaultTintBackground == null) {
33
+ mDefaultTintBackground = searchTextPlate.background
34
+ }
35
+ searchTextPlate.setBackgroundColor(tintColor)
36
+ } else if (currentDefaultTintColor != null) {
37
+ searchTextPlate.background = currentDefaultTintColor
38
+ }
39
+ }
40
+ }
package/ios/RNSScreen.m CHANGED
@@ -564,6 +564,8 @@
564
564
  _shouldNotify = NO;
565
565
  }
566
566
 
567
+ [self hideHeaderIfNecessary];
568
+
567
569
  // as per documentation of these methods
568
570
  _goingForward = [self isBeingPresented] || [self isMovingToParentViewController];
569
571
 
@@ -575,6 +577,35 @@
575
577
  }
576
578
  }
577
579
 
580
+ - (void)hideHeaderIfNecessary
581
+ {
582
+ #if !TARGET_OS_TV
583
+ // On iOS >=13, there is a bug when user transitions from screen with active search bar to screen without header
584
+ // In that case default iOS header will be shown. To fix this we hide header when the screens that appears has header
585
+ // hidden and search bar was active on previous screen. We need to do it asynchronously, because default header is
586
+ // added after viewWillAppear.
587
+ if (@available(iOS 13.0, *)) {
588
+ NSUInteger currentIndex = [self.navigationController.viewControllers indexOfObject:self];
589
+
590
+ if (currentIndex > 0 && [self.view.reactSubviews[0] isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
591
+ UINavigationItem *prevNavigationItem =
592
+ [self.navigationController.viewControllers objectAtIndex:currentIndex - 1].navigationItem;
593
+ RNSScreenStackHeaderConfig *config = ((RNSScreenStackHeaderConfig *)self.view.reactSubviews[0]);
594
+
595
+ BOOL wasSearchBarActive = prevNavigationItem.searchController.active;
596
+ BOOL shouldHideHeader = config.hide;
597
+
598
+ if (wasSearchBarActive && shouldHideHeader) {
599
+ dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0);
600
+ dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
601
+ [self.navigationController setNavigationBarHidden:YES animated:NO];
602
+ });
603
+ }
604
+ }
605
+ }
606
+ #endif
607
+ }
608
+
578
609
  - (void)viewWillDisappear:(BOOL)animated
579
610
  {
580
611
  [super viewWillDisappear:animated];
@@ -657,6 +688,10 @@
657
688
 
658
689
  - (void)traverseForScrollView:(UIView *)view
659
690
  {
691
+ if (![[self.view valueForKey:@"_bridge"] valueForKey:@"_jsThread"]) {
692
+ // we don't want to send `scrollViewDidEndDecelerating` event to JS before the JS thread is ready
693
+ return;
694
+ }
660
695
  if ([view isKindOfClass:[UIScrollView class]] &&
661
696
  ([[(UIScrollView *)view delegate] respondsToSelector:@selector(scrollViewDidEndDecelerating:)])) {
662
697
  [[(UIScrollView *)view delegate] scrollViewDidEndDecelerating:(id)view];
@@ -425,12 +425,12 @@
425
425
  // controller is still there
426
426
  BOOL firstTimePush = ![lastTop isKindOfClass:[RNSScreen class]];
427
427
 
428
- BOOL shouldAnimate = !firstTimePush && ((RNSScreenView *)lastTop.view).stackAnimation != RNSScreenStackAnimationNone;
429
-
430
428
  if (firstTimePush) {
431
429
  // nothing pushed yet
432
430
  [_controller setViewControllers:controllers animated:NO];
433
431
  } else if (top != lastTop) {
432
+ // we always provide `animated:YES` since, if the user does not want the animation, he will provide
433
+ // `stackAnimation: 'none'`, which will resolve in no animation anyways.
434
434
  if (![controllers containsObject:lastTop]) {
435
435
  // if the previous top screen does not exist anymore and the new top was not on the stack before, probably replace
436
436
  // was called, so we check the animation
@@ -445,7 +445,7 @@
445
445
  NSMutableArray *newControllers = [NSMutableArray arrayWithArray:controllers];
446
446
  [newControllers addObject:lastTop];
447
447
  [_controller setViewControllers:newControllers animated:NO];
448
- [_controller popViewControllerAnimated:shouldAnimate];
448
+ [_controller popViewControllerAnimated:YES];
449
449
  }
450
450
  } else if (![_controller.viewControllers containsObject:top]) {
451
451
  // new top controller is not on the stack
@@ -454,11 +454,11 @@
454
454
  NSMutableArray *newControllers = [NSMutableArray arrayWithArray:controllers];
455
455
  [newControllers removeLastObject];
456
456
  [_controller setViewControllers:newControllers animated:NO];
457
- [_controller pushViewController:top animated:shouldAnimate];
457
+ [_controller pushViewController:top animated:YES];
458
458
  } else {
459
459
  // don't really know what this case could be, but may need to handle it
460
460
  // somehow
461
- [_controller setViewControllers:controllers animated:shouldAnimate];
461
+ [_controller setViewControllers:controllers animated:NO];
462
462
  }
463
463
  } else {
464
464
  // change wasn't on the top of the stack. We don't need animation.
@@ -489,6 +489,23 @@
489
489
  [self setModalViewControllers:modalControllers];
490
490
  }
491
491
 
492
+ // By default, the header buttons that are not inside the native hit area
493
+ // cannot be clicked, so we check it by ourselves
494
+ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
495
+ {
496
+ if (CGRectContainsPoint(_controller.navigationBar.frame, point)) {
497
+ // headerConfig should be the first subview of the topmost screen
498
+ UIView *headerConfig = [[_reactSubviews.lastObject reactSubviews] firstObject];
499
+ if ([headerConfig isKindOfClass:[RNSScreenStackHeaderConfig class]]) {
500
+ UIView *headerHitTestResult = [headerConfig hitTest:point withEvent:event];
501
+ if (headerHitTestResult != nil) {
502
+ return headerHitTestResult;
503
+ }
504
+ }
505
+ }
506
+ return [super hitTest:point withEvent:event];
507
+ }
508
+
492
509
  - (void)layoutSubviews
493
510
  {
494
511
  [super layoutSubviews];
@@ -555,7 +572,8 @@
555
572
  {
556
573
  RNSScreenView *topScreen = (RNSScreenView *)_controller.viewControllers.lastObject.view;
557
574
 
558
- if (!topScreen.gestureEnabled || _controller.viewControllers.count < 2) {
575
+ if (![topScreen isKindOfClass:[RNSScreenView class]] || !topScreen.gestureEnabled ||
576
+ _controller.viewControllers.count < 2) {
559
577
  return NO;
560
578
  }
561
579