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.
- package/README.md +69 -3
- package/android/build.gradle +8 -7
- 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 -101
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +76 -14
- 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.h +1 -0
- package/ios/RNSScreen.m +35 -0
- package/ios/RNSScreenContainer.h +2 -0
- package/ios/RNSScreenStack.m +24 -6
- package/ios/RNSScreenStackHeaderConfig.m +45 -2
- package/ios/RNSScreenWindowTraits.h +5 -0
- package/ios/RNSScreenWindowTraits.m +29 -0
- package/lib/commonjs/index.js +24 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.native.js +103 -17
- 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 +99 -19
- 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 +138 -43
- 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
|
@@ -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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
293
|
-
|
|
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 =
|
|
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,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
|
-
|
|
149
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
229
|
+
override fun onAnimationEnd(animation: Animation) {
|
|
230
|
+
mFragment.onViewAnimationEnd()
|
|
231
|
+
}
|
|
183
232
|
|
|
184
|
-
|
|
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:
|
|
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
|
+
}
|