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.
- package/README.md +61 -3
- package/android/build.gradle +0 -2
- package/android/src/main/java/com/swmansion/rnscreens/CustomSearchView.kt +71 -0
- package/android/src/main/java/com/swmansion/rnscreens/CustomToolbar.kt +7 -0
- package/android/src/main/java/com/swmansion/rnscreens/FragmentBackPressOverrider.kt +29 -0
- package/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +2 -1
- package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +7 -41
- package/android/src/main/java/com/swmansion/rnscreens/ScreenContainer.kt +55 -40
- package/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +19 -1
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +30 -5
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +77 -12
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +13 -4
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +8 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubview.kt +7 -1
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderSubviewManager.kt +1 -0
- package/android/src/main/java/com/swmansion/rnscreens/SearchBarManager.kt +90 -0
- package/android/src/main/java/com/swmansion/rnscreens/SearchBarView.kt +150 -0
- package/android/src/main/java/com/swmansion/rnscreens/SearchViewFormatter.kt +40 -0
- package/ios/RNSScreen.m +35 -0
- package/ios/RNSScreenStack.m +24 -6
- package/ios/RNSScreenStackHeaderConfig.m +41 -0
- package/lib/commonjs/index.js +24 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.native.js +101 -11
- package/lib/commonjs/index.native.js.map +1 -1
- package/lib/commonjs/native-stack/utils/useBackPressSubscription.js +67 -0
- package/lib/commonjs/native-stack/utils/useBackPressSubscription.js.map +1 -0
- package/lib/commonjs/native-stack/views/HeaderConfig.js +46 -4
- package/lib/commonjs/native-stack/views/HeaderConfig.js.map +1 -1
- package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js +60 -0
- package/lib/commonjs/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
- package/lib/commonjs/reanimated/ReanimatedScreen.js +7 -79
- package/lib/commonjs/reanimated/ReanimatedScreen.js.map +1 -1
- package/lib/commonjs/reanimated/ReanimatedScreenProvider.js +61 -0
- package/lib/commonjs/reanimated/ReanimatedScreenProvider.js.map +1 -0
- package/lib/commonjs/reanimated/index.js +2 -2
- package/lib/commonjs/reanimated/index.js.map +1 -1
- package/lib/commonjs/utils.js +20 -0
- package/lib/commonjs/utils.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.native.js +97 -13
- package/lib/module/index.native.js.map +1 -1
- package/lib/module/native-stack/utils/useBackPressSubscription.js +50 -0
- package/lib/module/native-stack/utils/useBackPressSubscription.js.map +1 -0
- package/lib/module/native-stack/views/HeaderConfig.js +46 -5
- package/lib/module/native-stack/views/HeaderConfig.js.map +1 -1
- package/lib/module/reanimated/ReanimatedNativeStackScreen.js +40 -0
- package/lib/module/reanimated/ReanimatedNativeStackScreen.js.map +1 -0
- package/lib/module/reanimated/ReanimatedScreen.js +6 -73
- package/lib/module/reanimated/ReanimatedScreen.js.map +1 -1
- package/lib/module/reanimated/ReanimatedScreenProvider.js +49 -0
- package/lib/module/reanimated/ReanimatedScreenProvider.js.map +1 -0
- package/lib/module/reanimated/index.js +1 -1
- package/lib/module/reanimated/index.js.map +1 -1
- package/lib/module/utils.js +8 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/native-stack/types.d.ts +0 -2
- package/lib/typescript/native-stack/utils/useBackPressSubscription.d.ts +16 -0
- package/lib/typescript/reanimated/ReanimatedNativeStackScreen.d.ts +5 -0
- package/lib/typescript/reanimated/ReanimatedScreen.d.ts +5 -2
- package/lib/typescript/reanimated/ReanimatedScreenProvider.d.ts +2 -0
- package/lib/typescript/reanimated/index.d.ts +1 -1
- package/lib/typescript/types.d.ts +46 -1
- package/lib/typescript/utils.d.ts +2 -0
- package/native-stack/README.md +35 -7
- package/package.json +5 -2
- package/src/index.native.tsx +134 -38
- package/src/index.tsx +10 -0
- package/src/native-stack/types.tsx +0 -2
- package/src/native-stack/utils/useBackPressSubscription.tsx +66 -0
- package/src/native-stack/views/HeaderConfig.tsx +46 -3
- package/src/reanimated/ReanimatedNativeStackScreen.tsx +61 -0
- package/src/reanimated/ReanimatedScreen.tsx +6 -84
- package/src/reanimated/ReanimatedScreenProvider.tsx +42 -0
- package/src/reanimated/index.tsx +1 -1
- package/src/types.tsx +46 -1
- 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 =
|
|
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:
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
override fun onAnimationEnd(animation: Animation) {
|
|
230
|
+
mFragment.onViewAnimationEnd()
|
|
231
|
+
}
|
|
180
232
|
|
|
181
|
-
|
|
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:
|
|
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
|
-
|
|
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) :
|
|
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
|
|
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];
|
package/ios/RNSScreenStack.m
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
575
|
+
if (![topScreen isKindOfClass:[RNSScreenView class]] || !topScreen.gestureEnabled ||
|
|
576
|
+
_controller.viewControllers.count < 2) {
|
|
559
577
|
return NO;
|
|
560
578
|
}
|
|
561
579
|
|