@swmansion/react-native-bottom-sheet 0.10.2 → 0.12.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/README.md +125 -25
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetPackage.kt +1 -1
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetSurfaceView.kt +10 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetSurfaceViewManager.kt +27 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +97 -28
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt +11 -0
- package/common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h +4 -0
- package/common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ShadowNodes.h +12 -0
- package/ios/BottomSheetComponentView.mm +21 -2
- package/ios/BottomSheetContentView.h +3 -0
- package/ios/BottomSheetContentView.mm +15 -0
- package/ios/BottomSheetHostingView.swift +104 -34
- package/ios/BottomSheetSurfaceComponentView.h +13 -0
- package/ios/BottomSheetSurfaceComponentView.mm +21 -0
- package/lib/module/BottomSheet.js +17 -4
- package/lib/module/BottomSheet.js.map +1 -1
- package/lib/module/BottomSheetNativeComponent.ts +1 -0
- package/lib/module/BottomSheetSurfaceNativeComponent.ts +9 -0
- package/lib/module/ModalBottomSheet.js.map +1 -1
- package/lib/typescript/src/BottomSheet.d.ts +28 -2
- package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts +1 -0
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetSurfaceNativeComponent.d.ts +6 -0
- package/lib/typescript/src/BottomSheetSurfaceNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -1
- package/package.json +20 -18
- package/react-native.config.js +4 -1
- package/src/BottomSheet.tsx +46 -1
- package/src/BottomSheetNativeComponent.ts +1 -0
- package/src/BottomSheetSurfaceNativeComponent.ts +9 -0
- package/src/ModalBottomSheet.tsx +4 -2
package/README.md
CHANGED
|
@@ -18,6 +18,47 @@ React Native.
|
|
|
18
18
|
- Programmatic‍-‍only detents for snap points unreachable
|
|
19
19
|
by dragging.
|
|
20
20
|
|
|
21
|
+
## How it compares
|
|
22
|
+
|
|
23
|
+
React Native already has strong bottom‍-‍sheet options, but they make
|
|
24
|
+
different tradeoffs. React Native Bottom Sheet gives you composable React Native
|
|
25
|
+
primitives backed by native sheet mechanics: You compose the surface in React,
|
|
26
|
+
while the sheet host, gestures, snapping, and scroll negotiation run in
|
|
27
|
+
native code.
|
|
28
|
+
|
|
29
|
+
[`@gorhom/bottom-sheet`](https://gorhom.dev/react-native-bottom-sheet) is the
|
|
30
|
+
closest match in day‍-‍to‍-‍day functionality: configurable
|
|
31
|
+
detents, dynamic sizing, scrollable coordination, inline sheets, and modal
|
|
32
|
+
presentation. The main difference is the implementation model. React Native
|
|
33
|
+
Bottom Sheet moves the sheet host, gestures, snapping, and scroll negotiation
|
|
34
|
+
into native code, so heavy React rendering and busy JS work are less likely to
|
|
35
|
+
affect drag and snap performance. It also does not require Reanimated or React
|
|
36
|
+
Native Gesture Handler. Because scroll coordination is native, regular React
|
|
37
|
+
Native scrollables work inside the sheet without
|
|
38
|
+
bottom‍-‍sheet‍-‍specific list components or wrapper factories.
|
|
39
|
+
|
|
40
|
+
[Expo UI](https://docs.expo.dev/versions/latest/sdk/ui) sheets,
|
|
41
|
+
[Expo Router form sheets](https://docs.expo.dev/router/advanced/modals/#form-sheet),
|
|
42
|
+
and native modal‍-‍sheet libraries such as
|
|
43
|
+
[True Sheet](https://sheet.lodev09.com) lean into platform presentation APIs.
|
|
44
|
+
That is a good fit when you want a system‍-‍style presented sheet, but
|
|
45
|
+
it also means the platform and presentation system decide more of the behavior.
|
|
46
|
+
React Native Bottom Sheet is built as a lower‍-‍level sheet primitive
|
|
47
|
+
instead: The same native implementation powers both persistent inline sheets and
|
|
48
|
+
modal sheets, you provide the complete sheet surface in React, and detents can
|
|
49
|
+
include app‍-‍level behavior such as programmatic‍-‍only
|
|
50
|
+
snap points.
|
|
51
|
+
|
|
52
|
+
That difference also matters for layering. A platform‍-‍presented sheet
|
|
53
|
+
can disable dimming and allow background interaction, but it is still drawn as a
|
|
54
|
+
presented native sheet over the React Native view hierarchy. `BottomSheet` is
|
|
55
|
+
actually inline: It renders in your screen’s React Native hierarchy and can be
|
|
56
|
+
layered alongside nearby content. When you do need a modal, `ModalBottomSheet`
|
|
57
|
+
is rendered through `BottomSheetProvider`’s portal rather than through a
|
|
58
|
+
separate native window, so global UI such as toasts, menus, floating controls,
|
|
59
|
+
or debug overlays can be arranged above or below it by where you place them
|
|
60
|
+
relative to the provider.
|
|
61
|
+
|
|
21
62
|
## Getting started
|
|
22
63
|
|
|
23
64
|
1. Install React Native Bottom Sheet:
|
|
@@ -29,7 +70,7 @@ React Native.
|
|
|
29
70
|
2. Ensure the peer dependency is installed:
|
|
30
71
|
|
|
31
72
|
```sh
|
|
32
|
-
npm i react-native-safe-area-context
|
|
73
|
+
npm i react-native-safe-area-context
|
|
33
74
|
```
|
|
34
75
|
|
|
35
76
|
3. Wrap your app with `BottomSheetProvider`:
|
|
@@ -41,9 +82,9 @@ React Native.
|
|
|
41
82
|
## Usage
|
|
42
83
|
|
|
43
84
|
The library provides two components: `BottomSheet` (inline) and
|
|
44
|
-
`ModalBottomSheet` (modal). Both render their children as the sheet content
|
|
45
|
-
|
|
46
|
-
and `onIndexChange`. Use `onSettle` for
|
|
85
|
+
`ModalBottomSheet` (modal). Both render their children as the sheet content,
|
|
86
|
+
with a `surface` prop for the background behind it, and are controlled via
|
|
87
|
+
`detents`, `index`, and `onIndexChange`. Use `onSettle` for
|
|
47
88
|
post‍-‍snap observability.
|
|
48
89
|
|
|
49
90
|
### Inline
|
|
@@ -56,14 +97,14 @@ const insets = useSafeAreaInsets();
|
|
|
56
97
|
```
|
|
57
98
|
|
|
58
99
|
```tsx
|
|
59
|
-
<BottomSheet
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
>
|
|
100
|
+
<BottomSheet
|
|
101
|
+
index={index}
|
|
102
|
+
onIndexChange={setIndex}
|
|
103
|
+
surface={
|
|
104
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<View style={{ padding: 16, paddingBottom: insets.bottom + 16 }}>
|
|
67
108
|
<Text>Sheet content</Text>
|
|
68
109
|
</View>
|
|
69
110
|
</BottomSheet>
|
|
@@ -80,14 +121,14 @@ const insets = useSafeAreaInsets();
|
|
|
80
121
|
```
|
|
81
122
|
|
|
82
123
|
```tsx
|
|
83
|
-
<ModalBottomSheet
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
>
|
|
124
|
+
<ModalBottomSheet
|
|
125
|
+
index={index}
|
|
126
|
+
onIndexChange={setIndex}
|
|
127
|
+
surface={
|
|
128
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
<View style={{ padding: 16, paddingBottom: insets.bottom + 16 }}>
|
|
91
132
|
<Text>Sheet content</Text>
|
|
92
133
|
</View>
|
|
93
134
|
</ModalBottomSheet>
|
|
@@ -102,12 +143,64 @@ its color:
|
|
|
102
143
|
<ModalBottomSheet
|
|
103
144
|
index={index}
|
|
104
145
|
onIndexChange={setIndex}
|
|
146
|
+
surface={/* ... */}
|
|
105
147
|
scrimColor="rgba(0, 0, 0, 0.3)"
|
|
106
148
|
>
|
|
107
149
|
{/* ... */}
|
|
108
150
|
</ModalBottomSheet>
|
|
109
151
|
```
|
|
110
152
|
|
|
153
|
+
By default, the scrim fades in as the sheet opens and then holds at full
|
|
154
|
+
opacity, so detents above the first share the same scrim. Use `scrimOpacities`
|
|
155
|
+
to control the opacity at each detent: It takes one value in 0–1 per detent,
|
|
156
|
+
indexed to match `detents`, and interpolates linearly as the sheet is dragged
|
|
157
|
+
between them. A shorter array reuses its last value for any remaining detents.
|
|
158
|
+
|
|
159
|
+
The default maps each detent to 0 when it is closed and 1 otherwise, so the
|
|
160
|
+
scrim is transparent at any closed detent and fully opaque at every open one,
|
|
161
|
+
whatever order the detents are passed in.
|
|
162
|
+
|
|
163
|
+
To keep the scrim deepening across every detent, pass one value per detent:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<ModalBottomSheet
|
|
167
|
+
index={index}
|
|
168
|
+
onIndexChange={setIndex}
|
|
169
|
+
detents={[0, 300, 'content']}
|
|
170
|
+
scrimColor="rgba(0, 0, 0, 0.3)"
|
|
171
|
+
scrimOpacities={[0, 0.5, 1]}
|
|
172
|
+
surface={/* ... */}
|
|
173
|
+
>
|
|
174
|
+
{/* ... */}
|
|
175
|
+
</ModalBottomSheet>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Surface
|
|
179
|
+
|
|
180
|
+
Provide the sheet’s background through the `surface` prop. The library renders
|
|
181
|
+
it behind your content and sizes it natively to cover the whole sheet,
|
|
182
|
+
independently of the content height.
|
|
183
|
+
|
|
184
|
+
Decoupling the surface this way keeps the sheet covered as the content height
|
|
185
|
+
changes. When content shrinks, the sheet animates to its new height without the
|
|
186
|
+
background briefly exposing blank space behind the content.
|
|
187
|
+
|
|
188
|
+
Give the surface a filling style such as `StyleSheet.absoluteFill`. It is
|
|
189
|
+
mounted in a full‍-‍size host, so a surface sized only by its own
|
|
190
|
+
content would collapse and not show.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
<BottomSheet // Or `ModalBottomSheet`.
|
|
194
|
+
index={index}
|
|
195
|
+
onIndexChange={setIndex}
|
|
196
|
+
surface={
|
|
197
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
<Text>Sheet content</Text>
|
|
201
|
+
</BottomSheet>
|
|
202
|
+
```
|
|
203
|
+
|
|
111
204
|
### Scrollable negotiation
|
|
112
205
|
|
|
113
206
|
By default, the sheet coordinates vertical gestures with nested scrollables,
|
|
@@ -121,6 +214,7 @@ set `disableScrollableNegotiation`:
|
|
|
121
214
|
<BottomSheet
|
|
122
215
|
index={index}
|
|
123
216
|
onIndexChange={setIndex}
|
|
217
|
+
surface={/* ... */}
|
|
124
218
|
disableScrollableNegotiation
|
|
125
219
|
>
|
|
126
220
|
{/* ... */}
|
|
@@ -134,18 +228,20 @@ Detents are the points to which the sheet snaps. Each detent is either a number
|
|
|
134
228
|
the available screen height). The default detents are `[0, 'content']`.
|
|
135
229
|
|
|
136
230
|
Sheet children are laid out in a flex container. For a full‍-‍height
|
|
137
|
-
sheet, apply `flex: 1` to your
|
|
138
|
-
`
|
|
231
|
+
sheet, apply `flex: 1` to your content and use the `'content'` detent.
|
|
232
|
+
`surface` is sized by the library, so `flex: 1` only ever belongs on your
|
|
233
|
+
content, never on the surface:
|
|
139
234
|
|
|
140
235
|
```tsx
|
|
141
236
|
<BottomSheet
|
|
142
237
|
// `detents` defaults to `[0, 'content']`.
|
|
143
238
|
index={index}
|
|
144
239
|
onIndexChange={setIndex}
|
|
240
|
+
surface={
|
|
241
|
+
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
|
|
242
|
+
}
|
|
145
243
|
>
|
|
146
|
-
<View style={{ flex: 1
|
|
147
|
-
{/* Full-height sheet content. */}
|
|
148
|
-
</View>
|
|
244
|
+
<View style={{ flex: 1 }}>{/* Full-height sheet content. */}</View>
|
|
149
245
|
</BottomSheet>
|
|
150
246
|
```
|
|
151
247
|
|
|
@@ -168,6 +264,7 @@ const [index, setIndex] = useState(0);
|
|
|
168
264
|
detents={[0, 300, 'content']} // Collapsed, 300 px, content height.
|
|
169
265
|
index={index}
|
|
170
266
|
onIndexChange={setIndex} // Keep controlled state in sync.
|
|
267
|
+
surface={/* ... */}
|
|
171
268
|
onSettle={(nextIndex) => {
|
|
172
269
|
if (nextIndex === 0) console.log('Sheet collapsed.');
|
|
173
270
|
}}
|
|
@@ -190,6 +287,7 @@ drag snapping but can still be targeted via `index` updates.
|
|
|
190
287
|
detents={[0, programmatic(300), 'content']}
|
|
191
288
|
index={index}
|
|
192
289
|
onIndexChange={setIndex}
|
|
290
|
+
surface={/* ... */}
|
|
193
291
|
onSettle={(nextIndex) => {
|
|
194
292
|
console.log(`Settled at ${nextIndex}.`);
|
|
195
293
|
}}
|
|
@@ -207,6 +305,7 @@ pixels from the bottom of the screen to the top of the sheet).
|
|
|
207
305
|
<BottomSheet // Or `ModalBottomSheet`.
|
|
208
306
|
index={index}
|
|
209
307
|
onIndexChange={setIndex}
|
|
308
|
+
surface={/* ... */}
|
|
210
309
|
onPositionChange={(position) => {
|
|
211
310
|
console.log(position);
|
|
212
311
|
}}
|
|
@@ -226,6 +325,7 @@ const position = useSharedValue(0);
|
|
|
226
325
|
<BottomSheet
|
|
227
326
|
index={index}
|
|
228
327
|
onIndexChange={setIndex}
|
|
328
|
+
surface={/* ... */}
|
|
229
329
|
onPositionChange={(nextPosition) => {
|
|
230
330
|
position.value = nextPosition;
|
|
231
331
|
}}
|
|
@@ -10,5 +10,5 @@ class BottomSheetPackage : ReactPackage {
|
|
|
10
10
|
emptyList()
|
|
11
11
|
|
|
12
12
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
|
13
|
-
listOf(BottomSheetViewManager())
|
|
13
|
+
listOf(BottomSheetViewManager(), BottomSheetSurfaceViewManager())
|
|
14
14
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package com.swmansion.reactnativebottomsheet
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
5
|
+
|
|
6
|
+
// Visual surface that sits behind the sheet content. It carries no behavior of
|
|
7
|
+
// its own; the BottomSheetView identifies it by this type and owns its geometry,
|
|
8
|
+
// laying it out to cover the full sheet so a content shrink never exposes blank
|
|
9
|
+
// space. Its React children provide the appearance only.
|
|
10
|
+
class BottomSheetSurfaceView(context: Context) : ReactViewGroup(context)
|
package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetSurfaceViewManager.kt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.swmansion.reactnativebottomsheet
|
|
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.BottomSheetSurfaceViewManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.BottomSheetSurfaceViewManagerInterface
|
|
9
|
+
|
|
10
|
+
@ReactModule(name = BottomSheetSurfaceViewManager.NAME)
|
|
11
|
+
class BottomSheetSurfaceViewManager :
|
|
12
|
+
ViewGroupManager<BottomSheetSurfaceView>(),
|
|
13
|
+
BottomSheetSurfaceViewManagerInterface<BottomSheetSurfaceView> {
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
const val NAME = "BottomSheetSurfaceView"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private val delegate = BottomSheetSurfaceViewManagerDelegate(this)
|
|
20
|
+
|
|
21
|
+
override fun getDelegate(): ViewManagerDelegate<BottomSheetSurfaceView> = delegate
|
|
22
|
+
|
|
23
|
+
override fun getName(): String = NAME
|
|
24
|
+
|
|
25
|
+
override fun createViewInstance(context: ThemedReactContext): BottomSheetSurfaceView =
|
|
26
|
+
BottomSheetSurfaceView(context)
|
|
27
|
+
}
|
|
@@ -83,11 +83,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
83
83
|
private var scrimPressed = false
|
|
84
84
|
private var scrimTouchActive = false
|
|
85
85
|
private var scrimColor = Color.TRANSPARENT
|
|
86
|
+
// The JS layer always supplies a per-detent array; the fully-opaque fallback
|
|
87
|
+
// only guards against empty input (indexing requires a non-empty array).
|
|
88
|
+
private var scrimOpacities = listOf(1f)
|
|
86
89
|
private var scrimProgress = 0f
|
|
87
90
|
private var suppressScrimForClosingTarget = false
|
|
88
91
|
private var scrimPinnedFull = false
|
|
89
92
|
private var maxDetentHeight = Float.NaN
|
|
90
93
|
private var contentHeightMarker: View? = null
|
|
94
|
+
private var surfaceView: View? = null
|
|
91
95
|
|
|
92
96
|
private val contentHeightMarkerLayoutListener =
|
|
93
97
|
View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshDetentsFromLayout() }
|
|
@@ -166,7 +170,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
166
170
|
if (animateIn && isInvalidContentDetentTarget(clampedIndex)) {
|
|
167
171
|
targetIndex = clampedIndex
|
|
168
172
|
pendingIndex = clampedIndex
|
|
169
|
-
val closedTy =
|
|
173
|
+
val closedTy = resolvedMaxDetentHeight(h)
|
|
170
174
|
sheetContainer.translationY = closedTy
|
|
171
175
|
emitPosition()
|
|
172
176
|
return
|
|
@@ -177,7 +181,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
177
181
|
targetIndex = clampedIndex
|
|
178
182
|
|
|
179
183
|
if (animateIn) {
|
|
180
|
-
val closedTy =
|
|
184
|
+
val closedTy = resolvedMaxDetentHeight(h)
|
|
181
185
|
sheetContainer.translationY = closedTy
|
|
182
186
|
emitPosition()
|
|
183
187
|
snapToIndex(targetIndex, 0f, emitIndexChange = false, emitSettle = true)
|
|
@@ -198,18 +202,25 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
198
202
|
super.dispatchDraw(canvas)
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
private fun layoutSheetChildren() {
|
|
205
|
+
private fun layoutSheetChildren(containerWidth: Int, containerHeight: Int) {
|
|
202
206
|
for (i in 0 until sheetContainer.childCount) {
|
|
203
207
|
val child = sheetContainer.getChildAt(i)
|
|
204
|
-
child
|
|
208
|
+
if (child === surfaceView) {
|
|
209
|
+
// The surface fills the full container so it always covers the visible
|
|
210
|
+
// sheet (the container is translated to the current sheet position),
|
|
211
|
+
// regardless of how short the content becomes.
|
|
212
|
+
child.layout(0, 0, containerWidth, containerHeight)
|
|
213
|
+
} else {
|
|
214
|
+
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
|
|
215
|
+
}
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
private fun layoutSheetContainer(viewWidth: Int, viewHeight: Int) {
|
|
209
|
-
val maxHeight =
|
|
220
|
+
val maxHeight = resolvedMaxDetentHeight(viewHeight)
|
|
210
221
|
val containerTop = (viewHeight - maxHeight).toInt()
|
|
211
222
|
sheetContainer.layout(0, containerTop, viewWidth, containerTop + maxHeight.toInt())
|
|
212
|
-
layoutSheetChildren()
|
|
223
|
+
layoutSheetChildren(viewWidth, maxHeight.toInt())
|
|
213
224
|
}
|
|
214
225
|
|
|
215
226
|
// MARK: - Prop setters
|
|
@@ -247,11 +258,22 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
247
258
|
invalidate()
|
|
248
259
|
}
|
|
249
260
|
|
|
261
|
+
fun setScrimOpacities(values: List<Float>) {
|
|
262
|
+
scrimOpacities = if (values.isEmpty()) listOf(1f) else values
|
|
263
|
+
updateScrim()
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
fun setMaxDetentHeight(maxDetentHeight: Double) {
|
|
251
267
|
this.maxDetentHeight = (maxDetentHeight * density).toFloat()
|
|
252
268
|
refreshDetentsFromLayout()
|
|
253
269
|
}
|
|
254
270
|
|
|
271
|
+
// Stable coordinate base for the sheet container. The container is sized to
|
|
272
|
+
// the full available height rather than the tallest detent, so it stays a
|
|
273
|
+
// fixed-size canvas: when content — and thus the `content` detent — shrinks,
|
|
274
|
+
// the container does not collapse underneath the sheet, leaving room to
|
|
275
|
+
// animate the sheet down to its new height. The surface fills this canvas, so
|
|
276
|
+
// the area below the shrunken content stays covered throughout.
|
|
255
277
|
private fun resolvedMaxDetentHeight(viewHeight: Int = height): Float {
|
|
256
278
|
val viewHeightPx = viewHeight.toFloat()
|
|
257
279
|
if (!maxDetentHeight.isFinite() || maxDetentHeight <= 0f) {
|
|
@@ -294,7 +316,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
294
316
|
return
|
|
295
317
|
}
|
|
296
318
|
|
|
297
|
-
val previousMaxHeight =
|
|
319
|
+
val previousMaxHeight = resolvedMaxDetentHeight()
|
|
298
320
|
// Whether the scrim is currently fully opaque, i.e. the sheet is settled at
|
|
299
321
|
// or above the first non-zero detent. If so, a detent resize must not dip
|
|
300
322
|
// the scrim while the sheet re-anchors to the new geometry.
|
|
@@ -308,7 +330,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
308
330
|
|
|
309
331
|
if (hasLaidOut && !isPanning) {
|
|
310
332
|
targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
|
|
311
|
-
val newMaxHeight =
|
|
333
|
+
val newMaxHeight = resolvedMaxDetentHeight()
|
|
312
334
|
val targetTy = translationY(targetIndex)
|
|
313
335
|
if (activeAnimation != null && isTargetingClosedDetent) {
|
|
314
336
|
suppressScrimForClosingTarget = true
|
|
@@ -337,20 +359,15 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
337
359
|
} else {
|
|
338
360
|
val currentVisibleHeight = previousMaxHeight - sheetContainer.translationY
|
|
339
361
|
val targetHeight = detentSpecs.getOrNull(targetIndex)?.height ?: 0f
|
|
340
|
-
|
|
341
|
-
val didShrink = targetHeight < currentVisibleHeight - 0.5f
|
|
342
|
-
if (isContentDetent && didShrink) {
|
|
343
|
-
// Content shrank: snap immediately. Animating here would expose
|
|
344
|
-
// blank space below the shrunken content.
|
|
345
|
-
sheetContainer.translationY = targetTy
|
|
346
|
-
emitPosition()
|
|
347
|
-
} else if (kotlin.math.abs(targetHeight - currentVisibleHeight) <= 0.5f) {
|
|
362
|
+
if (kotlin.math.abs(targetHeight - currentVisibleHeight) <= 0.5f) {
|
|
348
363
|
// No meaningful change.
|
|
349
364
|
sheetContainer.translationY = targetTy
|
|
350
365
|
emitPosition()
|
|
351
366
|
} else {
|
|
352
|
-
//
|
|
353
|
-
// visible height, then animate to the new target.
|
|
367
|
+
// The content detent changed (grew or shrank): re-anchor at the
|
|
368
|
+
// current visible height, then animate to the new target. The
|
|
369
|
+
// surface covers the full sheet, so a shrink no longer exposes
|
|
370
|
+
// blank space.
|
|
354
371
|
sheetContainer.translationY =
|
|
355
372
|
(newMaxHeight - currentVisibleHeight).coerceIn(0f, newMaxHeight)
|
|
356
373
|
scrimPinnedFull = scrimPinnedFull || wasScrimFull
|
|
@@ -386,6 +403,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
386
403
|
}
|
|
387
404
|
|
|
388
405
|
private fun refreshContentHeightMarker() {
|
|
406
|
+
surfaceView = findSurfaceView()
|
|
389
407
|
val marker = findContentHeightMarker()
|
|
390
408
|
if (marker === contentHeightMarker) return
|
|
391
409
|
contentHeightMarker?.removeOnLayoutChangeListener(contentHeightMarkerLayoutListener)
|
|
@@ -393,8 +411,21 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
393
411
|
contentHeightMarker?.addOnLayoutChangeListener(contentHeightMarkerLayoutListener)
|
|
394
412
|
}
|
|
395
413
|
|
|
414
|
+
private fun findSurfaceView(): View? {
|
|
415
|
+
for (i in 0 until sheetContainer.childCount) {
|
|
416
|
+
val child = sheetContainer.getChildAt(i)
|
|
417
|
+
if (child is BottomSheetSurfaceView) return child
|
|
418
|
+
}
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
|
|
396
422
|
private fun findContentHeightMarker(): View? {
|
|
397
|
-
|
|
423
|
+
// The surface is a sibling of the content wrapper; skip it so the marker is
|
|
424
|
+
// always read from the content, never from the surface.
|
|
425
|
+
val contentView =
|
|
426
|
+
(0 until sheetContainer.childCount)
|
|
427
|
+
.map { sheetContainer.getChildAt(it) }
|
|
428
|
+
.firstOrNull { it !== surfaceView } as? ViewGroup ?: return null
|
|
398
429
|
if (contentView.childCount == 0) return null
|
|
399
430
|
return contentView.getChildAt(contentView.childCount - 1)
|
|
400
431
|
}
|
|
@@ -402,7 +433,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
402
433
|
// MARK: - Snap logic
|
|
403
434
|
|
|
404
435
|
private fun translationY(index: Int): Float {
|
|
405
|
-
val maxHeight =
|
|
436
|
+
val maxHeight = resolvedMaxDetentHeight()
|
|
406
437
|
val snapHeight = detentSpecs.getOrNull(index)?.height ?: 0f
|
|
407
438
|
return maxHeight - snapHeight
|
|
408
439
|
}
|
|
@@ -447,7 +478,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
447
478
|
}
|
|
448
479
|
|
|
449
480
|
private fun emitPosition() {
|
|
450
|
-
val maxHeight =
|
|
481
|
+
val maxHeight = resolvedMaxDetentHeight()
|
|
451
482
|
val ty = sheetContainer.translationY
|
|
452
483
|
val position = maxHeight - ty
|
|
453
484
|
updateScrim(position)
|
|
@@ -464,7 +495,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
464
495
|
private var lastShadowOffsetY = Float.NaN
|
|
465
496
|
|
|
466
497
|
private fun updateShadowState(translationY: Float) {
|
|
467
|
-
val maxDetentHeight =
|
|
498
|
+
val maxDetentHeight = resolvedMaxDetentHeight()
|
|
468
499
|
val containerTop = height.toFloat() - maxDetentHeight
|
|
469
500
|
val offsetY = ((containerTop + translationY) / density).toDouble()
|
|
470
501
|
if (offsetY.toFloat() == lastShadowOffsetY) return
|
|
@@ -703,7 +734,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
703
734
|
v
|
|
704
735
|
} ?: 0f
|
|
705
736
|
velocityTracker = null
|
|
706
|
-
val maxHeight =
|
|
737
|
+
val maxHeight = resolvedMaxDetentHeight()
|
|
707
738
|
val currentHeight = maxHeight - sheetContainer.translationY
|
|
708
739
|
val index = bestSnapIndex(currentHeight, velocity, panStartingIndex)
|
|
709
740
|
panStartingIndex = null
|
|
@@ -814,6 +845,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
814
845
|
velocityTracker = null
|
|
815
846
|
contentHeightMarker?.removeOnLayoutChangeListener(contentHeightMarkerLayoutListener)
|
|
816
847
|
contentHeightMarker = null
|
|
848
|
+
surfaceView = null
|
|
817
849
|
rawDetentSpecs = emptyList()
|
|
818
850
|
detentSpecs = emptyList()
|
|
819
851
|
targetIndex = 0
|
|
@@ -854,18 +886,55 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
854
886
|
|
|
855
887
|
// While the sheet is fully open and only its content/detent geometry is
|
|
856
888
|
// resizing, the position momentarily lags the grown detent height. Keep the
|
|
857
|
-
// scrim
|
|
889
|
+
// scrim pinned to the fully-open opacity instead of dipping it until the
|
|
890
|
+
// re-anchor settles.
|
|
858
891
|
if (scrimPinnedFull) {
|
|
859
|
-
scrimProgress =
|
|
892
|
+
scrimProgress = fullyOpenScrimOpacity()
|
|
860
893
|
invalidate()
|
|
861
894
|
return
|
|
862
895
|
}
|
|
863
896
|
|
|
864
|
-
|
|
865
|
-
scrimProgress = if (threshold <= 0f) 0f else (position / threshold).coerceIn(0f, 1f)
|
|
897
|
+
scrimProgress = scrimOpacityAt(position)
|
|
866
898
|
invalidate()
|
|
867
899
|
}
|
|
868
900
|
|
|
901
|
+
/** The opacity at the tallest detent, held while the sheet re-anchors. */
|
|
902
|
+
private fun fullyOpenScrimOpacity(): Float {
|
|
903
|
+
val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: return 1f
|
|
904
|
+
return scrimOpacityAt(maxHeight)
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Interpolates the scrim opacity for a sheet height by bracketing it between adjacent detent
|
|
909
|
+
* heights and lerping each detent index's configured value.
|
|
910
|
+
*/
|
|
911
|
+
private fun scrimOpacityAt(position: Float): Float {
|
|
912
|
+
if (detentSpecs.isEmpty()) return 0f
|
|
913
|
+
val pairs =
|
|
914
|
+
detentSpecs.indices
|
|
915
|
+
.map { index ->
|
|
916
|
+
detentSpecs[index].height to
|
|
917
|
+
scrimOpacities[index.coerceAtMost(scrimOpacities.size - 1)].coerceIn(0f, 1f)
|
|
918
|
+
}
|
|
919
|
+
.sortedBy { it.first }
|
|
920
|
+
|
|
921
|
+
val first = pairs.first()
|
|
922
|
+
val last = pairs.last()
|
|
923
|
+
if (position <= first.first) return first.second
|
|
924
|
+
if (position >= last.first) return last.second
|
|
925
|
+
|
|
926
|
+
for (i in 1 until pairs.size) {
|
|
927
|
+
val upper = pairs[i]
|
|
928
|
+
if (position <= upper.first) {
|
|
929
|
+
val lower = pairs[i - 1]
|
|
930
|
+
val span = upper.first - lower.first
|
|
931
|
+
val t = if (span <= 0f) 1f else (position - lower.first) / span
|
|
932
|
+
return lower.second + (upper.second - lower.second) * t
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return last.second
|
|
936
|
+
}
|
|
937
|
+
|
|
869
938
|
private fun hideScrim() {
|
|
870
939
|
scrimProgress = 0f
|
|
871
940
|
invalidate()
|
|
@@ -881,7 +950,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
|
881
950
|
}
|
|
882
951
|
|
|
883
952
|
private fun currentSheetHeight(): Float {
|
|
884
|
-
val maxHeight =
|
|
953
|
+
val maxHeight = resolvedMaxDetentHeight()
|
|
885
954
|
return maxHeight - sheetContainer.translationY
|
|
886
955
|
}
|
|
887
956
|
|
package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt
CHANGED
|
@@ -138,6 +138,17 @@ class BottomSheetViewManager :
|
|
|
138
138
|
view.setScrimColor(scrimColor)
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
@ReactProp(name = "scrimOpacities")
|
|
142
|
+
override fun setScrimOpacities(view: BottomSheetView, value: ReadableArray?) {
|
|
143
|
+
val opacities = mutableListOf<Float>()
|
|
144
|
+
if (value != null) {
|
|
145
|
+
for (i in 0 until value.size()) {
|
|
146
|
+
opacities.add(value.getDouble(i).toFloat())
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
view.setScrimOpacities(opacities)
|
|
150
|
+
}
|
|
151
|
+
|
|
141
152
|
override fun onDropViewInstance(view: BottomSheetView) {
|
|
142
153
|
super.onDropViewInstance(view)
|
|
143
154
|
view.destroy()
|
package/common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h
CHANGED
|
@@ -19,4 +19,8 @@ class BottomSheetViewComponentDescriptor final
|
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
// The surface needs no custom initial state, so this mirrors the codegen alias.
|
|
23
|
+
using BottomSheetSurfaceViewComponentDescriptor =
|
|
24
|
+
ConcreteComponentDescriptor<BottomSheetSurfaceViewShadowNode>;
|
|
25
|
+
|
|
22
26
|
} // namespace facebook::react
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include <react/renderer/components/ReactNativeBottomSheetSpec/EventEmitters.h>
|
|
6
6
|
#include <react/renderer/components/ReactNativeBottomSheetSpec/Props.h>
|
|
7
7
|
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
8
|
+
#include <react/renderer/core/StateData.h>
|
|
8
9
|
|
|
9
10
|
namespace facebook::react {
|
|
10
11
|
|
|
@@ -22,4 +23,15 @@ class JSI_EXPORT BottomSheetViewShadowNode final
|
|
|
22
23
|
Point getContentOriginOffset(bool includeTransform) const override;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
JSI_EXPORT extern const char BottomSheetSurfaceViewComponentName[];
|
|
27
|
+
|
|
28
|
+
// The surface needs no custom shadow-node behavior, so this mirrors the codegen
|
|
29
|
+
// alias exactly (state is the default StateData). It only lives here because the
|
|
30
|
+
// custom header search path shadows the generated ShadowNodes.h.
|
|
31
|
+
using BottomSheetSurfaceViewShadowNode = ConcreteViewShadowNode<
|
|
32
|
+
BottomSheetSurfaceViewComponentName,
|
|
33
|
+
BottomSheetSurfaceViewProps,
|
|
34
|
+
BottomSheetSurfaceViewEventEmitter,
|
|
35
|
+
StateData>;
|
|
36
|
+
|
|
25
37
|
} // namespace facebook::react
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#import "BottomSheetComponentView.h"
|
|
2
2
|
#import "BottomSheetContentView.h"
|
|
3
|
+
#import "BottomSheetSurfaceComponentView.h"
|
|
3
4
|
#import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/BottomSheetStateHelper.h"
|
|
4
5
|
#import "../common/cpp/react/renderer/components/ReactNativeBottomSheetSpec/ComponentDescriptors.h"
|
|
5
6
|
|
|
@@ -85,6 +86,14 @@ using namespace facebook::react;
|
|
|
85
86
|
[_sheetView setScrimColor:RCTUIColorFromSharedColor(newViewProps.scrimColor)];
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
if (newViewProps.scrimOpacities != oldViewProps.scrimOpacities) {
|
|
90
|
+
NSMutableArray<NSNumber *> *opacities = [NSMutableArray new];
|
|
91
|
+
for (const auto &opacity : newViewProps.scrimOpacities) {
|
|
92
|
+
[opacities addObject:@(opacity)];
|
|
93
|
+
}
|
|
94
|
+
[_sheetView setScrimOpacities:opacities];
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
[super updateProps:props oldProps:oldProps];
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -95,12 +104,22 @@ using namespace facebook::react;
|
|
|
95
104
|
|
|
96
105
|
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
97
106
|
{
|
|
98
|
-
|
|
107
|
+
// Identify the visual surface by component type so the host can own its
|
|
108
|
+
// geometry. Everything else is treated as content.
|
|
109
|
+
if ([childComponentView isKindOfClass:BottomSheetSurfaceComponentView.class]) {
|
|
110
|
+
[_sheetView mountSurfaceComponentView:childComponentView atIndex:index];
|
|
111
|
+
} else {
|
|
112
|
+
[_sheetView mountChildComponentView:childComponentView atIndex:index];
|
|
113
|
+
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
|
|
102
117
|
{
|
|
103
|
-
[
|
|
118
|
+
if ([childComponentView isKindOfClass:BottomSheetSurfaceComponentView.class]) {
|
|
119
|
+
[_sheetView unmountSurfaceComponentView:childComponentView];
|
|
120
|
+
} else {
|
|
121
|
+
[_sheetView unmountChildComponentView:childComponentView];
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
#pragma mark - BottomSheetContentViewDelegate
|
|
@@ -23,9 +23,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
23
23
|
- (void)setMaxDetentHeight:(CGFloat)maxDetentHeight;
|
|
24
24
|
- (void)setDetentIndex:(NSInteger)newIndex;
|
|
25
25
|
- (void)setScrimColor:(UIColor *_Nullable)color;
|
|
26
|
+
- (void)setScrimOpacities:(NSArray<NSNumber *> *)opacities;
|
|
26
27
|
- (CGFloat)currentContentOffsetY;
|
|
27
28
|
- (void)mountChildComponentView:(UIView *)childView atIndex:(NSInteger)index;
|
|
28
29
|
- (void)unmountChildComponentView:(UIView *)childView;
|
|
30
|
+
- (void)mountSurfaceComponentView:(UIView *)surfaceView atIndex:(NSInteger)index;
|
|
31
|
+
- (void)unmountSurfaceComponentView:(UIView *)surfaceView;
|
|
29
32
|
- (void)resetSheetState;
|
|
30
33
|
|
|
31
34
|
@end
|