react-native-screens 3.35.0-rc.1 → 4.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/swmansion/rnscreens/InsetsObserverProxy.kt +67 -0
- package/android/src/main/java/com/swmansion/rnscreens/RNScreensPackage.kt +2 -0
- package/android/src/main/java/com/swmansion/rnscreens/Screen.kt +101 -4
- package/android/src/main/java/com/swmansion/rnscreens/ScreenContentWrapper.kt +38 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenContentWrapperManager.kt +25 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenFooter.kt +287 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenFooterManager.kt +25 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenFragment.kt +11 -19
- package/android/src/main/java/com/swmansion/rnscreens/ScreenFragmentWrapper.kt +4 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenModalFragment.kt +281 -0
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt +62 -19
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragment.kt +403 -41
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackFragmentWrapper.kt +4 -1
- package/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfig.kt +2 -2
- package/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +95 -11
- package/android/src/main/java/com/swmansion/rnscreens/ScreenWindowTraits.kt +39 -28
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetDialogRootView.kt +104 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/BottomSheetDialogScreen.kt +26 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/DimmingFragment.kt +488 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/DimmingView.kt +66 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/GestureTransparentViewGroup.kt +24 -0
- package/android/src/main/java/com/swmansion/rnscreens/bottomsheet/SheetUtils.kt +127 -0
- package/android/src/main/java/com/swmansion/rnscreens/events/SheetDetentChangedEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/rnscreens/ext/NumericExt.kt +12 -0
- package/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +32 -0
- package/android/src/main/res/base/drawable/rns_rounder_top_corners_shape.xml +8 -0
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenContentWrapperManagerDelegate.java +25 -0
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenContentWrapperManagerInterface.java +16 -0
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenFooterManagerDelegate.java +25 -0
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenFooterManagerInterface.java +16 -0
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java +9 -2
- package/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java +5 -2
- package/ios/RNSConvert.h +5 -3
- package/ios/RNSConvert.mm +14 -20
- package/ios/RNSScreen.h +3 -2
- package/ios/RNSScreen.mm +433 -49
- package/ios/RNSScreenContentWrapper.h +44 -0
- package/ios/RNSScreenContentWrapper.mm +61 -0
- package/ios/RNSScreenFooter.h +30 -0
- package/ios/RNSScreenFooter.mm +137 -0
- package/lib/commonjs/components/Screen.js +6 -2
- package/lib/commonjs/components/Screen.js.map +1 -1
- package/lib/commonjs/components/ScreenContentWrapper.js +19 -0
- package/lib/commonjs/components/ScreenContentWrapper.js.map +1 -0
- package/lib/commonjs/components/ScreenFooter.js +23 -0
- package/lib/commonjs/components/ScreenFooter.js.map +1 -0
- package/lib/commonjs/fabric/ModalScreenNativeComponent.js.map +1 -1
- package/lib/commonjs/fabric/ScreenContentWrapperNativeComponent.js +10 -0
- package/lib/commonjs/fabric/ScreenContentWrapperNativeComponent.js.map +1 -0
- package/lib/commonjs/fabric/ScreenFooterNativeComponent.js +10 -0
- package/lib/commonjs/fabric/ScreenFooterNativeComponent.js.map +1 -0
- package/lib/commonjs/fabric/ScreenNativeComponent.js.map +1 -1
- package/lib/commonjs/index.js +30 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native-stack/views/FooterComponent.js +18 -0
- package/lib/commonjs/native-stack/views/FooterComponent.js.map +1 -0
- package/lib/commonjs/native-stack/views/NativeStackView.js +59 -14
- package/lib/commonjs/native-stack/views/NativeStackView.js.map +1 -1
- package/lib/module/components/Screen.js +6 -2
- package/lib/module/components/Screen.js.map +1 -1
- package/lib/module/components/ScreenContentWrapper.js +12 -0
- package/lib/module/components/ScreenContentWrapper.js.map +1 -0
- package/lib/module/components/ScreenFooter.js +17 -0
- package/lib/module/components/ScreenFooter.js.map +1 -0
- package/lib/module/fabric/ModalScreenNativeComponent.js.map +1 -1
- package/lib/module/fabric/ScreenContentWrapperNativeComponent.js +3 -0
- package/lib/module/fabric/ScreenContentWrapperNativeComponent.js.map +1 -0
- package/lib/module/fabric/ScreenFooterNativeComponent.js +3 -0
- package/lib/module/fabric/ScreenFooterNativeComponent.js.map +1 -0
- package/lib/module/fabric/ScreenNativeComponent.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native-stack/views/FooterComponent.js +11 -0
- package/lib/module/native-stack/views/FooterComponent.js.map +1 -0
- package/lib/module/native-stack/views/NativeStackView.js +61 -15
- package/lib/module/native-stack/views/NativeStackView.js.map +1 -1
- package/lib/typescript/components/Screen.d.ts.map +1 -1
- package/lib/typescript/components/ScreenContentWrapper.d.ts +6 -0
- package/lib/typescript/components/ScreenContentWrapper.d.ts.map +1 -0
- package/lib/typescript/components/ScreenFooter.d.ts +12 -0
- package/lib/typescript/components/ScreenFooter.d.ts.map +1 -0
- package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts +2 -3
- package/lib/typescript/fabric/ModalScreenNativeComponent.d.ts.map +1 -1
- package/lib/typescript/fabric/ScreenContentWrapperNativeComponent.d.ts +7 -0
- package/lib/typescript/fabric/ScreenContentWrapperNativeComponent.d.ts.map +1 -0
- package/lib/typescript/fabric/ScreenFooterNativeComponent.d.ts +7 -0
- package/lib/typescript/fabric/ScreenFooterNativeComponent.d.ts.map +1 -0
- package/lib/typescript/fabric/ScreenNativeComponent.d.ts +9 -3
- package/lib/typescript/fabric/ScreenNativeComponent.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/native-stack/types.d.ts +63 -23
- package/lib/typescript/native-stack/types.d.ts.map +1 -1
- package/lib/typescript/native-stack/views/FooterComponent.d.ts +7 -0
- package/lib/typescript/native-stack/views/FooterComponent.d.ts.map +1 -0
- package/lib/typescript/native-stack/views/NativeStackView.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +42 -17
- package/lib/typescript/types.d.ts.map +1 -1
- package/native-stack/README.md +16 -14
- package/package.json +1 -1
- package/react-native.config.js +18 -16
- package/src/components/Screen.tsx +6 -2
- package/src/components/ScreenContentWrapper.tsx +12 -0
- package/src/components/ScreenFooter.tsx +18 -0
- package/src/fabric/ModalScreenNativeComponent.ts +2 -4
- package/src/fabric/ScreenContentWrapperNativeComponent.ts +9 -0
- package/src/fabric/ScreenFooterNativeComponent.ts +6 -0
- package/src/fabric/ScreenNativeComponent.ts +10 -4
- package/src/index.tsx +10 -0
- package/src/native-stack/types.tsx +57 -23
- package/src/native-stack/views/FooterComponent.tsx +10 -0
- package/src/native-stack/views/NativeStackView.tsx +74 -11
- package/src/types.tsx +41 -16
package/android/build.gradle
CHANGED
|
@@ -159,8 +159,8 @@ repositories {
|
|
|
159
159
|
|
|
160
160
|
dependencies {
|
|
161
161
|
implementation 'com.facebook.react:react-native:+'
|
|
162
|
-
implementation 'androidx.appcompat:appcompat:1.
|
|
163
|
-
implementation 'androidx.fragment:fragment:1.
|
|
162
|
+
implementation 'androidx.appcompat:appcompat:1.6.1'
|
|
163
|
+
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
|
164
164
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
|
165
165
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
166
166
|
implementation 'com.google.android.material:material:1.6.1'
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package com.swmansion.rnscreens
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import androidx.core.view.OnApplyWindowInsetsListener
|
|
5
|
+
import androidx.core.view.ViewCompat
|
|
6
|
+
import androidx.core.view.WindowInsetsCompat
|
|
7
|
+
import java.lang.ref.WeakReference
|
|
8
|
+
|
|
9
|
+
object InsetsObserverProxy : OnApplyWindowInsetsListener {
|
|
10
|
+
private val listeners: ArrayList<OnApplyWindowInsetsListener> = arrayListOf()
|
|
11
|
+
private var eventSourceView: WeakReference<View> = WeakReference(null)
|
|
12
|
+
|
|
13
|
+
// Please note semantics of this property. This is not `isRegistered`, because somebody, could unregister
|
|
14
|
+
// us, without our knowledge, e.g. reanimated or different 3rd party library. This holds only information
|
|
15
|
+
// whether this observer has been initially registered.
|
|
16
|
+
private var hasBeenRegistered: Boolean = false
|
|
17
|
+
|
|
18
|
+
private var shouldForwardInsetsToView = true
|
|
19
|
+
|
|
20
|
+
override fun onApplyWindowInsets(
|
|
21
|
+
v: View,
|
|
22
|
+
insets: WindowInsetsCompat,
|
|
23
|
+
): WindowInsetsCompat {
|
|
24
|
+
var rollingInsets =
|
|
25
|
+
if (shouldForwardInsetsToView) {
|
|
26
|
+
WindowInsetsCompat.toWindowInsetsCompat(
|
|
27
|
+
v.onApplyWindowInsets(insets.toWindowInsets()),
|
|
28
|
+
v,
|
|
29
|
+
)
|
|
30
|
+
} else {
|
|
31
|
+
insets
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
listeners.forEach {
|
|
35
|
+
rollingInsets = it.onApplyWindowInsets(v, insets)
|
|
36
|
+
}
|
|
37
|
+
return rollingInsets
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun addOnApplyWindowInsetsListener(listener: OnApplyWindowInsetsListener) {
|
|
41
|
+
listeners.add(listener)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun removeOnApplyWindowInsetsListener(listener: OnApplyWindowInsetsListener) {
|
|
45
|
+
listeners.remove(listener)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun registerOnView(view: View) {
|
|
49
|
+
if (!hasBeenRegistered) {
|
|
50
|
+
ViewCompat.setOnApplyWindowInsetsListener(view, this)
|
|
51
|
+
eventSourceView = WeakReference(view)
|
|
52
|
+
hasBeenRegistered = true
|
|
53
|
+
} else if (getObservedView() != view) {
|
|
54
|
+
throw IllegalStateException(
|
|
55
|
+
"[RNScreens] Attempt to register InsetsObserverProxy on $view while it has been already registered on ${getObservedView()}",
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun unregister() {
|
|
61
|
+
eventSourceView.get()?.takeIf { hasBeenRegistered }?.let {
|
|
62
|
+
ViewCompat.setOnApplyWindowInsetsListener(it, null)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private fun getObservedView(): View? = eventSourceView.get()
|
|
67
|
+
}
|
|
@@ -10,6 +10,7 @@ import android.view.View
|
|
|
10
10
|
import android.view.ViewGroup
|
|
11
11
|
import android.view.WindowManager
|
|
12
12
|
import android.webkit.WebView
|
|
13
|
+
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
13
14
|
import androidx.core.view.children
|
|
14
15
|
import androidx.fragment.app.Fragment
|
|
15
16
|
import com.facebook.react.bridge.GuardedRunnable
|
|
@@ -17,15 +18,25 @@ import com.facebook.react.bridge.ReactContext
|
|
|
17
18
|
import com.facebook.react.uimanager.PixelUtil
|
|
18
19
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
19
20
|
import com.facebook.react.uimanager.UIManagerModule
|
|
21
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
22
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
20
23
|
import com.swmansion.rnscreens.events.HeaderHeightChangeEvent
|
|
24
|
+
import com.swmansion.rnscreens.events.SheetDetentChangedEvent
|
|
21
25
|
|
|
22
26
|
@SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated.
|
|
23
27
|
class Screen(
|
|
24
|
-
|
|
25
|
-
) : FabricEnabledViewGroup(
|
|
28
|
+
val reactContext: ReactContext,
|
|
29
|
+
) : FabricEnabledViewGroup(reactContext),
|
|
30
|
+
ScreenContentWrapper.OnLayoutCallback {
|
|
26
31
|
val fragment: Fragment?
|
|
27
32
|
get() = fragmentWrapper?.fragment
|
|
28
33
|
|
|
34
|
+
val sheetBehavior: BottomSheetBehavior<Screen>?
|
|
35
|
+
get() = (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior as? BottomSheetBehavior<Screen>
|
|
36
|
+
|
|
37
|
+
val reactEventDispatcher: EventDispatcher?
|
|
38
|
+
get() = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
39
|
+
|
|
29
40
|
var fragmentWrapper: ScreenFragmentWrapper? = null
|
|
30
41
|
var container: ScreenContainer? = null
|
|
31
42
|
var activityState: ActivityState? = null
|
|
@@ -40,6 +51,33 @@ class Screen(
|
|
|
40
51
|
var isStatusBarAnimated: Boolean? = null
|
|
41
52
|
var isBeingRemoved = false
|
|
42
53
|
|
|
54
|
+
// Props for controlling modal presentation
|
|
55
|
+
var isSheetGrabberVisible: Boolean = false
|
|
56
|
+
var sheetCornerRadius: Float = 0F
|
|
57
|
+
set(value) {
|
|
58
|
+
field = value
|
|
59
|
+
(fragment as? ScreenStackFragment)?.onSheetCornerRadiusChange()
|
|
60
|
+
}
|
|
61
|
+
var sheetExpandsWhenScrolledToEdge: Boolean = true
|
|
62
|
+
|
|
63
|
+
// We want to make sure here that at least one value is present in this array all the time.
|
|
64
|
+
// TODO: Model this with custom data structure to guarantee that this invariant is not violated.
|
|
65
|
+
var sheetDetents = mutableListOf(1.0)
|
|
66
|
+
var sheetLargestUndimmedDetentIndex: Int = -1
|
|
67
|
+
var sheetInitialDetentIndex: Int = 0
|
|
68
|
+
var sheetClosesOnTouchOutside = true
|
|
69
|
+
var sheetElevation: Float = 24F
|
|
70
|
+
|
|
71
|
+
var footer: ScreenFooter? = null
|
|
72
|
+
set(value) {
|
|
73
|
+
if (value == null && field != null) {
|
|
74
|
+
sheetBehavior?.let { field!!.unregisterWithSheetBehavior(it) }
|
|
75
|
+
} else if (value != null) {
|
|
76
|
+
sheetBehavior?.let { value.registerWithSheetBehavior(it) }
|
|
77
|
+
}
|
|
78
|
+
field = value
|
|
79
|
+
}
|
|
80
|
+
|
|
43
81
|
init {
|
|
44
82
|
// we set layout params as WindowManager.LayoutParams to workaround the issue with TextInputs
|
|
45
83
|
// not displaying modal menus (e.g., copy/paste or selection). The missing menus are due to the
|
|
@@ -54,6 +92,33 @@ class Screen(
|
|
|
54
92
|
layoutParams = WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION)
|
|
55
93
|
}
|
|
56
94
|
|
|
95
|
+
/**
|
|
96
|
+
* ScreenContentWrapper notifies us here on it's layout. It is essential for implementing
|
|
97
|
+
* `fitToContents` for formSheets, as this is first entry point where we can acquire
|
|
98
|
+
* height of our content.
|
|
99
|
+
*/
|
|
100
|
+
override fun onLayoutCallback(
|
|
101
|
+
changed: Boolean,
|
|
102
|
+
left: Int,
|
|
103
|
+
top: Int,
|
|
104
|
+
right: Int,
|
|
105
|
+
bottom: Int,
|
|
106
|
+
) {
|
|
107
|
+
val height = bottom - top
|
|
108
|
+
|
|
109
|
+
if (sheetDetents.count() == 1 && sheetDetents.first() == SHEET_FIT_TO_CONTENTS) {
|
|
110
|
+
sheetBehavior?.let {
|
|
111
|
+
if (it.maxHeight != height) {
|
|
112
|
+
it.maxHeight = height
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun registerLayoutCallbackForWrapper(wrapper: ScreenContentWrapper) {
|
|
119
|
+
wrapper.delegate = this
|
|
120
|
+
}
|
|
121
|
+
|
|
57
122
|
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
|
|
58
123
|
// do nothing, react native will keep the view hierarchy so no need to serialize/deserialize
|
|
59
124
|
// view's states. The side effect of restoring is that TextInput components would trigger
|
|
@@ -84,6 +149,7 @@ class Screen(
|
|
|
84
149
|
updateScreenSizePaper(width, height)
|
|
85
150
|
}
|
|
86
151
|
|
|
152
|
+
footer?.onParentLayout(changed, l, t, r, b, container!!.height)
|
|
87
153
|
notifyHeaderHeightChange(totalHeight)
|
|
88
154
|
}
|
|
89
155
|
}
|
|
@@ -92,7 +158,6 @@ class Screen(
|
|
|
92
158
|
width: Int,
|
|
93
159
|
height: Int,
|
|
94
160
|
) {
|
|
95
|
-
val reactContext = context as ReactContext
|
|
96
161
|
reactContext.runOnNativeModulesQueueThread(
|
|
97
162
|
object : GuardedRunnable(reactContext.exceptionHandler) {
|
|
98
163
|
override fun runGuarded() {
|
|
@@ -127,7 +192,14 @@ class Screen(
|
|
|
127
192
|
)
|
|
128
193
|
}
|
|
129
194
|
|
|
130
|
-
fun isTransparent(): Boolean =
|
|
195
|
+
fun isTransparent(): Boolean =
|
|
196
|
+
when (stackPresentation) {
|
|
197
|
+
StackPresentation.TRANSPARENT_MODAL,
|
|
198
|
+
StackPresentation.FORM_SHEET,
|
|
199
|
+
-> true
|
|
200
|
+
|
|
201
|
+
else -> false
|
|
202
|
+
}
|
|
131
203
|
|
|
132
204
|
private fun hasWebView(viewGroup: ViewGroup): Boolean {
|
|
133
205
|
for (i in 0 until viewGroup.childCount) {
|
|
@@ -351,10 +423,26 @@ class Screen(
|
|
|
351
423
|
?.dispatchEvent(HeaderHeightChangeEvent(surfaceId, id, headerHeight))
|
|
352
424
|
}
|
|
353
425
|
|
|
426
|
+
internal fun notifySheetDetentChange(
|
|
427
|
+
detentIndex: Int,
|
|
428
|
+
isStable: Boolean,
|
|
429
|
+
) {
|
|
430
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
431
|
+
reactEventDispatcher?.dispatchEvent(
|
|
432
|
+
SheetDetentChangedEvent(
|
|
433
|
+
surfaceId,
|
|
434
|
+
id,
|
|
435
|
+
detentIndex,
|
|
436
|
+
isStable,
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
354
441
|
enum class StackPresentation {
|
|
355
442
|
PUSH,
|
|
356
443
|
MODAL,
|
|
357
444
|
TRANSPARENT_MODAL,
|
|
445
|
+
FORM_SHEET,
|
|
358
446
|
}
|
|
359
447
|
|
|
360
448
|
enum class StackAnimation {
|
|
@@ -390,4 +478,13 @@ class Screen(
|
|
|
390
478
|
NAVIGATION_BAR_TRANSLUCENT,
|
|
391
479
|
NAVIGATION_BAR_HIDDEN,
|
|
392
480
|
}
|
|
481
|
+
|
|
482
|
+
companion object {
|
|
483
|
+
const val TAG = "Screen"
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* This value describes value in sheet detents array that will be treated as `fitToContents` option.
|
|
487
|
+
*/
|
|
488
|
+
const val SHEET_FIT_TO_CONTENTS = -1.0
|
|
489
|
+
}
|
|
393
490
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.swmansion.rnscreens
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import com.facebook.react.bridge.ReactContext
|
|
5
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* When we wrap children of the Screen component inside this component in JS code,
|
|
9
|
+
* we can later use it to get the enclosing frame size of our content as it is rendered by RN.
|
|
10
|
+
*
|
|
11
|
+
* This is useful when adapting form sheet height to its contents height.
|
|
12
|
+
*/
|
|
13
|
+
@SuppressLint("ViewConstructor")
|
|
14
|
+
class ScreenContentWrapper(
|
|
15
|
+
reactContext: ReactContext,
|
|
16
|
+
) : ReactViewGroup(reactContext) {
|
|
17
|
+
internal var delegate: OnLayoutCallback? = null
|
|
18
|
+
|
|
19
|
+
interface OnLayoutCallback {
|
|
20
|
+
fun onLayoutCallback(
|
|
21
|
+
changed: Boolean,
|
|
22
|
+
left: Int,
|
|
23
|
+
top: Int,
|
|
24
|
+
right: Int,
|
|
25
|
+
bottom: Int,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun onLayout(
|
|
30
|
+
changed: Boolean,
|
|
31
|
+
left: Int,
|
|
32
|
+
top: Int,
|
|
33
|
+
right: Int,
|
|
34
|
+
bottom: Int,
|
|
35
|
+
) {
|
|
36
|
+
delegate?.onLayoutCallback(changed, left, top, right, bottom)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.swmansion.rnscreens
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
6
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
7
|
+
import com.facebook.react.viewmanagers.RNSScreenContentWrapperManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.RNSScreenContentWrapperManagerInterface
|
|
9
|
+
|
|
10
|
+
@ReactModule(name = ScreenContentWrapperManager.REACT_CLASS)
|
|
11
|
+
class ScreenContentWrapperManager :
|
|
12
|
+
ViewGroupManager<ScreenContentWrapper>(),
|
|
13
|
+
RNSScreenContentWrapperManagerInterface<ScreenContentWrapper> {
|
|
14
|
+
private val delegate: ViewManagerDelegate<ScreenContentWrapper> = RNSScreenContentWrapperManagerDelegate(this)
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
const val REACT_CLASS = "RNSScreenContentWrapper"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun getName(): String = REACT_CLASS
|
|
21
|
+
|
|
22
|
+
override fun createViewInstance(reactContext: ThemedReactContext): ScreenContentWrapper = ScreenContentWrapper(reactContext)
|
|
23
|
+
|
|
24
|
+
override fun getDelegate(): ViewManagerDelegate<ScreenContentWrapper> = delegate
|
|
25
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
package com.swmansion.rnscreens
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.view.View
|
|
5
|
+
import androidx.core.view.ViewCompat
|
|
6
|
+
import androidx.core.view.WindowInsetsAnimationCompat
|
|
7
|
+
import androidx.core.view.WindowInsetsCompat
|
|
8
|
+
import com.facebook.react.bridge.ReactContext
|
|
9
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
10
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
11
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
|
12
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
|
13
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
|
|
14
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
|
15
|
+
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
|
|
16
|
+
import com.google.android.material.math.MathUtils
|
|
17
|
+
import com.swmansion.rnscreens.bottomsheet.SheetUtils
|
|
18
|
+
import kotlin.math.max
|
|
19
|
+
|
|
20
|
+
@SuppressLint("ViewConstructor")
|
|
21
|
+
class ScreenFooter(
|
|
22
|
+
val reactContext: ReactContext,
|
|
23
|
+
) : ReactViewGroup(reactContext) {
|
|
24
|
+
private var lastContainerHeight: Int = 0
|
|
25
|
+
private var lastStableSheetState: Int = STATE_HIDDEN
|
|
26
|
+
private var isAnimationControlledByKeyboard = false
|
|
27
|
+
private var lastSlideOffset = 0.0f
|
|
28
|
+
private var lastBottomInset = 0
|
|
29
|
+
private var isCallbackRegistered = false
|
|
30
|
+
|
|
31
|
+
// ScreenFooter is supposed to be direct child of Screen
|
|
32
|
+
private val screenParent
|
|
33
|
+
get() = parent as? Screen
|
|
34
|
+
|
|
35
|
+
private val sheetBehavior
|
|
36
|
+
get() = requireScreenParent().sheetBehavior
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// Due to Android restrictions on layout flow, particularly
|
|
40
|
+
// the fact that onMeasure must set `measuredHeight` & `measuredWidth` React calls `measure` on every
|
|
41
|
+
// view group with accurate dimensions computed by Yoga. This is our entry point to get current view dimensions.
|
|
42
|
+
private val reactHeight
|
|
43
|
+
get() = measuredHeight
|
|
44
|
+
|
|
45
|
+
private val reactWidth
|
|
46
|
+
get() = measuredWidth
|
|
47
|
+
|
|
48
|
+
// Main goal of this callback implementation is to handle keyboard appearance. We use it to make sure
|
|
49
|
+
// that the footer respects keyboard during layout.
|
|
50
|
+
// Note `DISPATCH_MODE_STOP` is used here to avoid propagation of insets callback to footer subtree.
|
|
51
|
+
private val insetsAnimation =
|
|
52
|
+
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
|
53
|
+
override fun onStart(
|
|
54
|
+
animation: WindowInsetsAnimationCompat,
|
|
55
|
+
bounds: WindowInsetsAnimationCompat.BoundsCompat,
|
|
56
|
+
): WindowInsetsAnimationCompat.BoundsCompat {
|
|
57
|
+
isAnimationControlledByKeyboard = true
|
|
58
|
+
return super.onStart(animation, bounds)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun onProgress(
|
|
62
|
+
insets: WindowInsetsCompat,
|
|
63
|
+
runningAnimations: MutableList<WindowInsetsAnimationCompat>,
|
|
64
|
+
): WindowInsetsCompat {
|
|
65
|
+
val imeBottomInset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
66
|
+
val navigationBarBottomInset =
|
|
67
|
+
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
|
68
|
+
|
|
69
|
+
// **It looks like** when keyboard is presented its inset does include navigation bar
|
|
70
|
+
// bottom inset, while it is already accounted for somewhere (dunno where).
|
|
71
|
+
// That is why we subtract navigation bar bottom inset here.
|
|
72
|
+
//
|
|
73
|
+
// Situations where keyboard is not visible and navigation bar is present are handled
|
|
74
|
+
// directly in layout function by not allowing lastBottomInset to contribute value less
|
|
75
|
+
// than 0. Alternative would be write logic specific to keyboard animation direction (hide / show).
|
|
76
|
+
lastBottomInset = imeBottomInset - navigationBarBottomInset
|
|
77
|
+
layoutFooterOnYAxis(
|
|
78
|
+
lastContainerHeight,
|
|
79
|
+
reactHeight,
|
|
80
|
+
sheetTopWhileDragging(lastSlideOffset),
|
|
81
|
+
lastBottomInset,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Please note that we do *not* consume any insets here, so that we do not interfere with
|
|
85
|
+
// any other view.
|
|
86
|
+
return insets
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun onEnd(animation: WindowInsetsAnimationCompat) {
|
|
90
|
+
isAnimationControlledByKeyboard = false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
init {
|
|
95
|
+
val rootView = checkNotNull(reactContext.currentActivity) {
|
|
96
|
+
"[RNScreens] Context detached from activity while creating ScreenFooter"
|
|
97
|
+
}.window.decorView
|
|
98
|
+
|
|
99
|
+
// Note that we do override insets animation on given view. I can see it interfering e.g.
|
|
100
|
+
// with reanimated keyboard or even other places in our code. Need to test this.
|
|
101
|
+
ViewCompat.setWindowInsetsAnimationCallback(rootView, insetsAnimation)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun requireScreenParent(): Screen = requireNotNull(screenParent)
|
|
105
|
+
|
|
106
|
+
private fun requireSheetBehavior(): BottomSheetBehavior<Screen> = requireNotNull(sheetBehavior)
|
|
107
|
+
|
|
108
|
+
// React calls `layout` function to set view dimensions, thus this is our entry point for
|
|
109
|
+
// fixing layout up after Yoga repositions it.
|
|
110
|
+
override fun onLayout(
|
|
111
|
+
changed: Boolean,
|
|
112
|
+
left: Int,
|
|
113
|
+
top: Int,
|
|
114
|
+
right: Int,
|
|
115
|
+
bottom: Int,
|
|
116
|
+
) {
|
|
117
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
118
|
+
layoutFooterOnYAxis(
|
|
119
|
+
lastContainerHeight,
|
|
120
|
+
bottom - top,
|
|
121
|
+
sheetTopInStableState(requireSheetBehavior().state),
|
|
122
|
+
lastBottomInset,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private var footerCallback =
|
|
127
|
+
object : BottomSheetCallback() {
|
|
128
|
+
override fun onStateChanged(
|
|
129
|
+
bottomSheet: View,
|
|
130
|
+
newState: Int,
|
|
131
|
+
) {
|
|
132
|
+
if (!SheetUtils.isStateStable(newState)) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
when (newState) {
|
|
137
|
+
STATE_COLLAPSED,
|
|
138
|
+
STATE_HALF_EXPANDED,
|
|
139
|
+
STATE_EXPANDED,
|
|
140
|
+
->
|
|
141
|
+
layoutFooterOnYAxis(
|
|
142
|
+
lastContainerHeight,
|
|
143
|
+
reactHeight,
|
|
144
|
+
sheetTopInStableState(newState),
|
|
145
|
+
lastBottomInset,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
else -> {}
|
|
149
|
+
}
|
|
150
|
+
lastStableSheetState = newState
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override fun onSlide(
|
|
154
|
+
bottomSheet: View,
|
|
155
|
+
slideOffset: Float,
|
|
156
|
+
) {
|
|
157
|
+
lastSlideOffset = max(slideOffset, 0.0f)
|
|
158
|
+
if (!isAnimationControlledByKeyboard) {
|
|
159
|
+
layoutFooterOnYAxis(
|
|
160
|
+
lastContainerHeight,
|
|
161
|
+
reactHeight,
|
|
162
|
+
sheetTopWhileDragging(lastSlideOffset),
|
|
163
|
+
lastBottomInset,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Important to keep this method idempotent! We attempt to (un)register
|
|
170
|
+
// our callback in different places depending on whether the behavior is already created.
|
|
171
|
+
fun registerWithSheetBehavior(behavior: BottomSheetBehavior<Screen>) {
|
|
172
|
+
if (!isCallbackRegistered) {
|
|
173
|
+
behavior.addBottomSheetCallback(footerCallback)
|
|
174
|
+
isCallbackRegistered = true
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Important to keep this method idempotent! We attempt to (un)register
|
|
179
|
+
// our callback in different places depending on whether the behavior is already created.
|
|
180
|
+
fun unregisterWithSheetBehavior(behavior: BottomSheetBehavior<Screen>) {
|
|
181
|
+
if (isCallbackRegistered) {
|
|
182
|
+
behavior.removeBottomSheetCallback(footerCallback)
|
|
183
|
+
isCallbackRegistered = false
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override fun onAttachedToWindow() {
|
|
188
|
+
super.onAttachedToWindow()
|
|
189
|
+
sheetBehavior?.let { registerWithSheetBehavior(it) }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun onDetachedFromWindow() {
|
|
193
|
+
super.onDetachedFromWindow()
|
|
194
|
+
sheetBehavior?.let { unregisterWithSheetBehavior(it) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Calculate position of sheet's top while it is in stable state given concrete sheet state.
|
|
199
|
+
*
|
|
200
|
+
* This method should not be used for sheet in unstable state.
|
|
201
|
+
*
|
|
202
|
+
* @param state sheet state as defined in [BottomSheetBehavior]
|
|
203
|
+
* @return position of sheet's top **relative to container**
|
|
204
|
+
*/
|
|
205
|
+
private fun sheetTopInStableState(state: Int): Int {
|
|
206
|
+
val behavior = requireSheetBehavior()
|
|
207
|
+
return when (state) {
|
|
208
|
+
STATE_COLLAPSED -> lastContainerHeight - behavior.peekHeight
|
|
209
|
+
STATE_HALF_EXPANDED -> (lastContainerHeight * (1 - behavior.halfExpandedRatio)).toInt()
|
|
210
|
+
STATE_EXPANDED -> behavior.expandedOffset
|
|
211
|
+
STATE_HIDDEN -> lastContainerHeight
|
|
212
|
+
else -> throw IllegalArgumentException("[RNScreens] use of stable-state method for unstable state")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Calculate position of sheet's top while it is in dragging / settling state given concrete slide offset
|
|
218
|
+
* as reported by [BottomSheetCallback.onSlide].
|
|
219
|
+
*
|
|
220
|
+
* This method should not be used for sheet in stable state.
|
|
221
|
+
*
|
|
222
|
+
* @param slideOffset sheet offset as reported by [BottomSheetCallback.onSlide]
|
|
223
|
+
* @return position of sheet's top **relative to container**
|
|
224
|
+
*/
|
|
225
|
+
private fun sheetTopWhileDragging(slideOffset: Float): Int =
|
|
226
|
+
MathUtils
|
|
227
|
+
.lerp(
|
|
228
|
+
sheetTopInStableState(STATE_COLLAPSED).toFloat(),
|
|
229
|
+
sheetTopInStableState(
|
|
230
|
+
STATE_EXPANDED,
|
|
231
|
+
).toFloat(),
|
|
232
|
+
slideOffset,
|
|
233
|
+
).toInt()
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parent Screen will call this on it's layout. We need to be notified on any update to Screen's content
|
|
237
|
+
* or its container dimensions change. This is also our entrypoint to acquiring container height.
|
|
238
|
+
*/
|
|
239
|
+
fun onParentLayout(
|
|
240
|
+
changed: Boolean,
|
|
241
|
+
left: Int,
|
|
242
|
+
top: Int,
|
|
243
|
+
right: Int,
|
|
244
|
+
bottom: Int,
|
|
245
|
+
containerHeight: Int,
|
|
246
|
+
) {
|
|
247
|
+
lastContainerHeight = containerHeight
|
|
248
|
+
layoutFooterOnYAxis(
|
|
249
|
+
containerHeight,
|
|
250
|
+
reactHeight,
|
|
251
|
+
sheetTopInStableState(requireSheetBehavior().state),
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Layouts this component within parent screen. It takes care only of vertical axis, leaving
|
|
257
|
+
* horizontal axis solely for React to handle.
|
|
258
|
+
*
|
|
259
|
+
* This is a bit against Android rules, that parents should layout their children,
|
|
260
|
+
* however I wanted to keep this logic away from Screen component to avoid introducing
|
|
261
|
+
* complexity there and have footer logic as much separated as it is possible.
|
|
262
|
+
*
|
|
263
|
+
* Please note that React has no clue about updates enforced in below method.
|
|
264
|
+
*
|
|
265
|
+
* @param containerHeight this should be the height of the screen (sheet) container used
|
|
266
|
+
* to calculate sheet properties when configuring behavior (pixels)
|
|
267
|
+
* @param footerHeight summarized height of this component children (pixels)
|
|
268
|
+
* @param sheetTop current bottom sheet top (Screen top) **relative to container** (pixels)
|
|
269
|
+
* @param bottomInset current bottom inset, used to offset the footer by keyboard height (pixels)
|
|
270
|
+
*/
|
|
271
|
+
fun layoutFooterOnYAxis(
|
|
272
|
+
containerHeight: Int,
|
|
273
|
+
footerHeight: Int,
|
|
274
|
+
sheetTop: Int,
|
|
275
|
+
bottomInset: Int = 0,
|
|
276
|
+
) {
|
|
277
|
+
// max(bottomInset, 0) is just a hack to avoid double offset of navigation bar.
|
|
278
|
+
val newTop = containerHeight - footerHeight - sheetTop - max(bottomInset, 0)
|
|
279
|
+
val heightBeforeUpdate = reactHeight
|
|
280
|
+
this.top = max(newTop, 0)
|
|
281
|
+
this.bottom = this.top + heightBeforeUpdate
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
companion object {
|
|
285
|
+
const val TAG = "ScreenFooter"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package com.swmansion.rnscreens
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewGroupManager
|
|
6
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
7
|
+
import com.facebook.react.viewmanagers.RNSScreenFooterManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.RNSScreenFooterManagerInterface
|
|
9
|
+
|
|
10
|
+
@ReactModule(name = ScreenFooterManager.REACT_CLASS)
|
|
11
|
+
class ScreenFooterManager :
|
|
12
|
+
ViewGroupManager<ScreenFooter>(),
|
|
13
|
+
RNSScreenFooterManagerInterface<ScreenFooter> {
|
|
14
|
+
private val delegate: ViewManagerDelegate<ScreenFooter> = RNSScreenFooterManagerDelegate(this)
|
|
15
|
+
|
|
16
|
+
override fun getName(): String = REACT_CLASS
|
|
17
|
+
|
|
18
|
+
override fun createViewInstance(context: ThemedReactContext) = ScreenFooter(context)
|
|
19
|
+
|
|
20
|
+
override fun getDelegate(): ViewManagerDelegate<ScreenFooter> = delegate
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
const val REACT_CLASS = "RNSScreenFooter"
|
|
24
|
+
}
|
|
25
|
+
}
|