@swmansion/react-native-bottom-sheet 0.6.3 → 0.7.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ReactNativeBottomSheet.podspec +24 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetPackage.kt +14 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +412 -0
- package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetViewManager.kt +105 -0
- package/ios/BottomSheetComponentView.h +10 -0
- package/ios/BottomSheetComponentView.mm +100 -0
- package/ios/BottomSheetContentView.h +25 -0
- package/ios/BottomSheetContentView.mm +73 -0
- package/ios/RNSBottomSheetHostingView.swift +294 -0
- package/lib/module/BottomSheet.js +86 -5
- package/lib/module/BottomSheet.js.map +1 -1
- package/lib/module/BottomSheetNativeComponent.ts +24 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/BottomSheet.d.ts +16 -3
- package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts +19 -0
- package/lib/typescript/src/BottomSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +14 -1
- package/react-native.config.js +1 -0
- package/src/BottomSheet.tsx +100 -6
- package/src/BottomSheetNativeComponent.ts +24 -0
- package/src/index.tsx +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ReactNativeBottomSheet"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/software-mansion-labs/react-native-bottom-sheet.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
s.swift_version = "5.0"
|
|
19
|
+
s.pod_target_xcconfig = {
|
|
20
|
+
"DEFINES_MODULE" => "YES"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
install_modules_dependencies(s)
|
|
24
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.ReactNativeBottomSheet = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return ReactNativeBottomSheet[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
apply plugin: "com.android.library"
|
|
31
|
+
apply plugin: "kotlin-android"
|
|
32
|
+
|
|
33
|
+
apply plugin: "com.facebook.react"
|
|
34
|
+
|
|
35
|
+
android {
|
|
36
|
+
namespace "com.swmansion.reactnativebottomsheet"
|
|
37
|
+
|
|
38
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
39
|
+
|
|
40
|
+
defaultConfig {
|
|
41
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
42
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildFeatures {
|
|
46
|
+
buildConfig true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildTypes {
|
|
50
|
+
release {
|
|
51
|
+
minifyEnabled false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lint {
|
|
56
|
+
disable "GradleCompatible"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
compileOptions {
|
|
60
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
61
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dependencies {
|
|
66
|
+
implementation "com.facebook.react:react-android"
|
|
67
|
+
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.swmansion.reactnativebottomsheet
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class BottomSheetPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
emptyList()
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
|
13
|
+
listOf(BottomSheetViewManager())
|
|
14
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
package com.swmansion.reactnativebottomsheet
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.Choreographer
|
|
5
|
+
import android.view.MotionEvent
|
|
6
|
+
import android.view.VelocityTracker
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.view.ViewConfiguration
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import androidx.dynamicanimation.animation.DynamicAnimation
|
|
12
|
+
import androidx.dynamicanimation.animation.SpringAnimation
|
|
13
|
+
import androidx.dynamicanimation.animation.SpringForce
|
|
14
|
+
import com.facebook.react.uimanager.PointerEvents
|
|
15
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
16
|
+
import kotlin.math.abs
|
|
17
|
+
|
|
18
|
+
private data class DetentSpec(val height: Float, val programmatic: Boolean)
|
|
19
|
+
|
|
20
|
+
interface BottomSheetViewListener {
|
|
21
|
+
fun onIndexChange(index: Int)
|
|
22
|
+
fun onPositionChange(position: Double)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class BottomSheetView(context: Context) : ReactViewGroup(context) {
|
|
26
|
+
|
|
27
|
+
// MARK: - Listener
|
|
28
|
+
|
|
29
|
+
var listener: BottomSheetViewListener? = null
|
|
30
|
+
|
|
31
|
+
// MARK: - State
|
|
32
|
+
|
|
33
|
+
private var detentSpecs: List<DetentSpec> = emptyList()
|
|
34
|
+
private var targetIndex: Int = 0
|
|
35
|
+
var animateIn: Boolean = true
|
|
36
|
+
private var pendingIndex: Int? = null
|
|
37
|
+
private var hasLaidOut = false
|
|
38
|
+
private var isPanning = false
|
|
39
|
+
|
|
40
|
+
// MARK: - Internal
|
|
41
|
+
|
|
42
|
+
private val sheetContainer = FrameLayout(context)
|
|
43
|
+
private var activeAnimation: SpringAnimation? = null
|
|
44
|
+
private var velocityTracker: VelocityTracker? = null
|
|
45
|
+
private var choreographerCallback: Choreographer.FrameCallback? = null
|
|
46
|
+
private val density = context.resources.displayMetrics.density
|
|
47
|
+
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
48
|
+
|
|
49
|
+
// Touch tracking
|
|
50
|
+
private var initialTouchY = 0f
|
|
51
|
+
private var lastTouchY = 0f
|
|
52
|
+
private var activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
53
|
+
|
|
54
|
+
init {
|
|
55
|
+
clipChildren = false
|
|
56
|
+
clipToPadding = false
|
|
57
|
+
pointerEvents = PointerEvents.BOX_NONE
|
|
58
|
+
sheetContainer.clipChildren = false
|
|
59
|
+
sheetContainer.clipToPadding = false
|
|
60
|
+
super.addView(
|
|
61
|
+
sheetContainer,
|
|
62
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
val sheetChildCount: Int
|
|
67
|
+
get() = sheetContainer.childCount
|
|
68
|
+
|
|
69
|
+
fun getSheetChildAt(index: Int): View? = sheetContainer.getChildAt(index)
|
|
70
|
+
|
|
71
|
+
fun addSheetChild(child: View, index: Int) {
|
|
72
|
+
sheetContainer.addView(child, index)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fun removeSheetChildAt(index: Int) {
|
|
76
|
+
sheetContainer.removeViewAt(index)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Child view management
|
|
80
|
+
|
|
81
|
+
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) {
|
|
82
|
+
if (child === sheetContainer) {
|
|
83
|
+
super.addView(child, index, params)
|
|
84
|
+
} else {
|
|
85
|
+
sheetContainer.addView(child, index, params)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun removeView(view: View) {
|
|
90
|
+
if (view === sheetContainer) {
|
|
91
|
+
super.removeView(view)
|
|
92
|
+
} else {
|
|
93
|
+
sheetContainer.removeView(view)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun removeViewAt(index: Int) {
|
|
98
|
+
sheetContainer.removeViewAt(index)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - Layout
|
|
102
|
+
|
|
103
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
104
|
+
super.onLayout(changed, l, t, r, b)
|
|
105
|
+
val w = r - l
|
|
106
|
+
val h = b - t
|
|
107
|
+
if (w <= 0 || h <= 0) return
|
|
108
|
+
|
|
109
|
+
layoutSheetContainer(w, h)
|
|
110
|
+
|
|
111
|
+
if (!hasLaidOut && detentSpecs.isNotEmpty()) {
|
|
112
|
+
hasLaidOut = true
|
|
113
|
+
val indexToApply = pendingIndex ?: targetIndex
|
|
114
|
+
pendingIndex = null
|
|
115
|
+
targetIndex = indexToApply.coerceIn(0, detentSpecs.size - 1)
|
|
116
|
+
|
|
117
|
+
if (animateIn) {
|
|
118
|
+
val closedTy = detentSpecs.lastOrNull()?.height ?: h.toFloat()
|
|
119
|
+
sheetContainer.translationY = closedTy
|
|
120
|
+
emitPosition()
|
|
121
|
+
snapToIndex(targetIndex, 0f)
|
|
122
|
+
} else {
|
|
123
|
+
sheetContainer.translationY = translationY(targetIndex)
|
|
124
|
+
emitPosition()
|
|
125
|
+
listener?.onIndexChange(targetIndex)
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (activeAnimation != null || isPanning) return
|
|
131
|
+
sheetContainer.translationY = translationY(targetIndex)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun layoutSheetChildren() {
|
|
135
|
+
for (i in 0 until sheetContainer.childCount) {
|
|
136
|
+
val child = sheetContainer.getChildAt(i)
|
|
137
|
+
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private fun layoutSheetContainer(viewWidth: Int, viewHeight: Int) {
|
|
142
|
+
val maxHeight = detentSpecs.lastOrNull()?.height ?: viewHeight.toFloat()
|
|
143
|
+
val containerTop = (viewHeight - maxHeight).toInt()
|
|
144
|
+
sheetContainer.layout(0, containerTop, viewWidth, containerTop + maxHeight.toInt())
|
|
145
|
+
layoutSheetChildren()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// MARK: - Prop setters
|
|
149
|
+
|
|
150
|
+
fun setDetents(raw: List<Map<String, Any>>) {
|
|
151
|
+
detentSpecs = raw.mapNotNull { dict ->
|
|
152
|
+
val height = (dict["height"] as? Number)?.toDouble() ?: return@mapNotNull null
|
|
153
|
+
val programmatic = dict["programmatic"] as? Boolean ?: false
|
|
154
|
+
DetentSpec(height = (height * density).toFloat(), programmatic = programmatic)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (width > 0 && height > 0 && detentSpecs.isNotEmpty()) {
|
|
158
|
+
layoutSheetContainer(width, height)
|
|
159
|
+
|
|
160
|
+
if (hasLaidOut && activeAnimation == null && !isPanning) {
|
|
161
|
+
targetIndex = targetIndex.coerceIn(0, detentSpecs.size - 1)
|
|
162
|
+
sheetContainer.translationY = translationY(targetIndex)
|
|
163
|
+
emitPosition()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
requestLayout()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fun setIndex(newIndex: Int) {
|
|
171
|
+
if (newIndex < 0) return
|
|
172
|
+
|
|
173
|
+
if (!hasLaidOut) {
|
|
174
|
+
pendingIndex = newIndex
|
|
175
|
+
targetIndex = newIndex
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (newIndex >= detentSpecs.size || newIndex == targetIndex) return
|
|
180
|
+
snapToIndex(newIndex, 0f)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// MARK: - Snap logic
|
|
184
|
+
|
|
185
|
+
private fun translationY(index: Int): Float {
|
|
186
|
+
val maxHeight = detentSpecs.lastOrNull()?.height ?: height.toFloat()
|
|
187
|
+
val snapHeight = detentSpecs.getOrNull(index)?.height ?: 0f
|
|
188
|
+
return maxHeight - snapHeight
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private val draggableMinTy: Float
|
|
192
|
+
get() {
|
|
193
|
+
val highestIndex = detentSpecs.indices.lastOrNull { !detentSpecs[it].programmatic } ?: 0
|
|
194
|
+
return translationY(highestIndex)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private val draggableMaxTy: Float
|
|
198
|
+
get() {
|
|
199
|
+
val lowestIndex = detentSpecs.indices.firstOrNull { !detentSpecs[it].programmatic } ?: 0
|
|
200
|
+
return translationY(lowestIndex)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private val isAtMaxDraggable: Boolean
|
|
204
|
+
get() = sheetContainer.translationY <= draggableMinTy + 1f
|
|
205
|
+
|
|
206
|
+
private fun emitPosition() {
|
|
207
|
+
val maxHeight = detentSpecs.lastOrNull()?.height ?: height.toFloat()
|
|
208
|
+
val ty = sheetContainer.translationY
|
|
209
|
+
listener?.onPositionChange(((maxHeight - ty) / density).toDouble())
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// MARK: - Choreographer (position tracking during animation)
|
|
213
|
+
|
|
214
|
+
private fun startChoreographer() {
|
|
215
|
+
if (choreographerCallback != null) return
|
|
216
|
+
val callback = object : Choreographer.FrameCallback {
|
|
217
|
+
override fun doFrame(frameTimeNanos: Long) {
|
|
218
|
+
emitPosition()
|
|
219
|
+
choreographerCallback?.let { Choreographer.getInstance().postFrameCallback(it) }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
choreographerCallback = callback
|
|
223
|
+
Choreographer.getInstance().postFrameCallback(callback)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private fun stopChoreographer() {
|
|
227
|
+
choreographerCallback?.let { Choreographer.getInstance().removeFrameCallback(it) }
|
|
228
|
+
choreographerCallback = null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// MARK: - Spring animation
|
|
232
|
+
|
|
233
|
+
private fun snapToIndex(index: Int, velocity: Float) {
|
|
234
|
+
if (index < 0 || index >= detentSpecs.size) return
|
|
235
|
+
targetIndex = index
|
|
236
|
+
|
|
237
|
+
val targetTy = translationY(index)
|
|
238
|
+
|
|
239
|
+
activeAnimation?.cancel()
|
|
240
|
+
|
|
241
|
+
val spring = SpringAnimation(sheetContainer, DynamicAnimation.TRANSLATION_Y, targetTy).apply {
|
|
242
|
+
spring = SpringForce(targetTy).apply {
|
|
243
|
+
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
|
244
|
+
stiffness = SpringForce.STIFFNESS_MEDIUM
|
|
245
|
+
}
|
|
246
|
+
setStartVelocity(velocity)
|
|
247
|
+
addEndListener { _, _, _, _ ->
|
|
248
|
+
stopChoreographer()
|
|
249
|
+
emitPosition()
|
|
250
|
+
activeAnimation = null
|
|
251
|
+
listener?.onIndexChange(index)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
activeAnimation = spring
|
|
256
|
+
startChoreographer()
|
|
257
|
+
spring.start()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private fun bestSnapIndex(currentHeight: Float, velocity: Float): Int {
|
|
261
|
+
val draggable = detentSpecs.withIndex().filter { !it.value.programmatic }
|
|
262
|
+
if (draggable.isEmpty()) return targetIndex
|
|
263
|
+
|
|
264
|
+
val flickThreshold = 600f * density
|
|
265
|
+
|
|
266
|
+
if (velocity < -flickThreshold) {
|
|
267
|
+
return draggable.firstOrNull { it.value.height > currentHeight }?.index
|
|
268
|
+
?: draggable.lastOrNull()?.index ?: targetIndex
|
|
269
|
+
}
|
|
270
|
+
if (velocity > flickThreshold) {
|
|
271
|
+
return draggable.lastOrNull { it.value.height < currentHeight }?.index
|
|
272
|
+
?: draggable.firstOrNull()?.index ?: targetIndex
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return draggable.minByOrNull { abs(it.value.height - currentHeight) }?.index ?: targetIndex
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// MARK: - Touch handling
|
|
279
|
+
|
|
280
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
281
|
+
val sheetTop = sheetContainer.top + sheetContainer.translationY
|
|
282
|
+
if (ev.y < sheetTop) {
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
when (ev.actionMasked) {
|
|
287
|
+
MotionEvent.ACTION_DOWN -> {
|
|
288
|
+
initialTouchY = ev.y
|
|
289
|
+
lastTouchY = ev.y
|
|
290
|
+
activePointerId = ev.getPointerId(0)
|
|
291
|
+
}
|
|
292
|
+
MotionEvent.ACTION_MOVE -> {
|
|
293
|
+
if (activePointerId == MotionEvent.INVALID_POINTER_ID) return false
|
|
294
|
+
val pointerIndex = ev.findPointerIndex(activePointerId)
|
|
295
|
+
if (pointerIndex < 0) return false
|
|
296
|
+
val y = ev.getY(pointerIndex)
|
|
297
|
+
val dy = y - initialTouchY
|
|
298
|
+
|
|
299
|
+
if (abs(dy) > touchSlop) {
|
|
300
|
+
if (!isAtMaxDraggable) {
|
|
301
|
+
lastTouchY = y
|
|
302
|
+
requestDisallowInterceptTouchEvent(false)
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
if (dy > 0 && isScrollViewAtTop()) {
|
|
306
|
+
lastTouchY = y
|
|
307
|
+
requestDisallowInterceptTouchEvent(false)
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
313
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
320
|
+
val sheetTop = sheetContainer.top + sheetContainer.translationY
|
|
321
|
+
if (event.y < sheetTop) {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
when (event.actionMasked) {
|
|
326
|
+
MotionEvent.ACTION_DOWN -> {
|
|
327
|
+
beginPan(event)
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
MotionEvent.ACTION_MOVE -> {
|
|
331
|
+
if (!isPanning) beginPan(event)
|
|
332
|
+
val pointerIndex = event.findPointerIndex(activePointerId)
|
|
333
|
+
if (pointerIndex < 0) return true
|
|
334
|
+
val y = event.getY(pointerIndex)
|
|
335
|
+
velocityTracker?.addMovement(event)
|
|
336
|
+
val dy = y - lastTouchY
|
|
337
|
+
lastTouchY = y
|
|
338
|
+
|
|
339
|
+
val newTy = (sheetContainer.translationY + dy).coerceIn(draggableMinTy, draggableMaxTy)
|
|
340
|
+
sheetContainer.translationY = newTy
|
|
341
|
+
emitPosition()
|
|
342
|
+
return true
|
|
343
|
+
}
|
|
344
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
345
|
+
isPanning = false
|
|
346
|
+
activePointerId = MotionEvent.INVALID_POINTER_ID
|
|
347
|
+
val velocity = velocityTracker?.let { tracker ->
|
|
348
|
+
tracker.computeCurrentVelocity(1000)
|
|
349
|
+
val v = tracker.yVelocity
|
|
350
|
+
tracker.recycle()
|
|
351
|
+
v
|
|
352
|
+
} ?: 0f
|
|
353
|
+
velocityTracker = null
|
|
354
|
+
val maxHeight = detentSpecs.lastOrNull()?.height ?: height.toFloat()
|
|
355
|
+
val currentHeight = maxHeight - sheetContainer.translationY
|
|
356
|
+
val index = bestSnapIndex(currentHeight, velocity)
|
|
357
|
+
snapToIndex(index, velocity)
|
|
358
|
+
return true
|
|
359
|
+
}
|
|
360
|
+
MotionEvent.ACTION_POINTER_UP -> {
|
|
361
|
+
val actionIndex = event.actionIndex
|
|
362
|
+
if (event.getPointerId(actionIndex) == activePointerId) {
|
|
363
|
+
val newIndex = if (actionIndex == 0) 1 else 0
|
|
364
|
+
activePointerId = event.getPointerId(newIndex)
|
|
365
|
+
lastTouchY = event.getY(newIndex)
|
|
366
|
+
}
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return super.onTouchEvent(event)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun beginPan(event: MotionEvent) {
|
|
374
|
+
isPanning = true
|
|
375
|
+
activePointerId = event.getPointerId(0)
|
|
376
|
+
lastTouchY = event.y
|
|
377
|
+
velocityTracker?.recycle()
|
|
378
|
+
velocityTracker = VelocityTracker.obtain()
|
|
379
|
+
velocityTracker?.addMovement(event)
|
|
380
|
+
activeAnimation?.let {
|
|
381
|
+
it.cancel()
|
|
382
|
+
activeAnimation = null
|
|
383
|
+
stopChoreographer()
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// MARK: - Scroll view helpers
|
|
388
|
+
|
|
389
|
+
private fun isScrollViewAtTop(): Boolean {
|
|
390
|
+
val scrollView = findScrollView(sheetContainer) ?: return true
|
|
391
|
+
return !scrollView.canScrollVertically(-1)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private fun findScrollView(view: View): View? {
|
|
395
|
+
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) return view
|
|
396
|
+
if (view is ViewGroup) {
|
|
397
|
+
for (i in 0 until view.childCount) {
|
|
398
|
+
findScrollView(view.getChildAt(i))?.let { return it }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// MARK: - Cleanup
|
|
405
|
+
|
|
406
|
+
fun destroy() {
|
|
407
|
+
activeAnimation?.cancel()
|
|
408
|
+
stopChoreographer()
|
|
409
|
+
velocityTracker?.recycle()
|
|
410
|
+
velocityTracker = null
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
package com.swmansion.reactnativebottomsheet
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.bridge.ReadableArray
|
|
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.ViewManagerDelegate
|
|
9
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
10
|
+
import com.facebook.react.viewmanagers.BottomSheetViewManagerDelegate
|
|
11
|
+
import com.facebook.react.viewmanagers.BottomSheetViewManagerInterface
|
|
12
|
+
|
|
13
|
+
@ReactModule(name = BottomSheetViewManager.NAME)
|
|
14
|
+
class BottomSheetViewManager :
|
|
15
|
+
ViewGroupManager<BottomSheetView>(),
|
|
16
|
+
BottomSheetViewManagerInterface<BottomSheetView> {
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
const val NAME = "BottomSheetView"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private val delegate = BottomSheetViewManagerDelegate(this)
|
|
23
|
+
|
|
24
|
+
override fun getDelegate(): ViewManagerDelegate<BottomSheetView> = delegate
|
|
25
|
+
|
|
26
|
+
override fun getName(): String = NAME
|
|
27
|
+
|
|
28
|
+
override fun createViewInstance(context: ThemedReactContext): BottomSheetView {
|
|
29
|
+
val view = BottomSheetView(context)
|
|
30
|
+
view.listener = object : BottomSheetViewListener {
|
|
31
|
+
override fun onIndexChange(index: Int) {
|
|
32
|
+
val event = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
33
|
+
putInt("index", index)
|
|
34
|
+
}
|
|
35
|
+
val reactContext = view.context as? ThemedReactContext ?: return
|
|
36
|
+
reactContext
|
|
37
|
+
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
38
|
+
.receiveEvent(view.id, "topIndexChange", event)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onPositionChange(position: Double) {
|
|
42
|
+
val event = com.facebook.react.bridge.Arguments.createMap().apply {
|
|
43
|
+
putDouble("position", position)
|
|
44
|
+
}
|
|
45
|
+
val reactContext = view.context as? ThemedReactContext ?: return
|
|
46
|
+
reactContext
|
|
47
|
+
.getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
|
|
48
|
+
.receiveEvent(view.id, "topPositionChange", event)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return view
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun addView(parent: BottomSheetView, child: View, index: Int) {
|
|
55
|
+
parent.addSheetChild(child, index)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun getChildCount(parent: BottomSheetView): Int = parent.sheetChildCount
|
|
59
|
+
|
|
60
|
+
override fun getChildAt(parent: BottomSheetView, index: Int): View? = parent.getSheetChildAt(index)
|
|
61
|
+
|
|
62
|
+
override fun removeViewAt(parent: BottomSheetView, index: Int) {
|
|
63
|
+
parent.removeSheetChildAt(index)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun needsCustomLayoutForChildren(): Boolean = true
|
|
67
|
+
|
|
68
|
+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> {
|
|
69
|
+
return mapOf(
|
|
70
|
+
"topIndexChange" to mapOf("registrationName" to "onIndexChange"),
|
|
71
|
+
"topPositionChange" to mapOf("registrationName" to "onPositionChange"),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactProp(name = "detents")
|
|
76
|
+
override fun setDetents(view: BottomSheetView, detents: ReadableArray?) {
|
|
77
|
+
if (detents == null) return
|
|
78
|
+
val list = mutableListOf<Map<String, Any>>()
|
|
79
|
+
for (i in 0 until detents.size()) {
|
|
80
|
+
val map = detents.getMap(i) ?: continue
|
|
81
|
+
list.add(
|
|
82
|
+
mapOf(
|
|
83
|
+
"height" to map.getDouble("height"),
|
|
84
|
+
"programmatic" to map.getBoolean("programmatic"),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
view.setDetents(list)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@ReactProp(name = "index")
|
|
92
|
+
override fun setIndex(view: BottomSheetView, index: Int) {
|
|
93
|
+
view.setIndex(index)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@ReactProp(name = "animateIn")
|
|
97
|
+
override fun setAnimateIn(view: BottomSheetView, animateIn: Boolean) {
|
|
98
|
+
view.animateIn = animateIn
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onDropViewInstance(view: BottomSheetView) {
|
|
102
|
+
super.onDropViewInstance(view)
|
|
103
|
+
view.destroy()
|
|
104
|
+
}
|
|
105
|
+
}
|