@lodev09/react-native-true-sheet 0.9.7 → 0.9.8

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 CHANGED
@@ -31,7 +31,7 @@ npm i @lodev09/react-native-true-sheet
31
31
 
32
32
  ## Usage
33
33
 
34
- ```ts
34
+ ```tsx
35
35
  import { TrueSheet } from "@lodev09/react-native-true-sheet"
36
36
 
37
37
  // ...
@@ -78,7 +78,7 @@ Extends `ViewProps`
78
78
 
79
79
  ## Methods
80
80
 
81
- ```ts
81
+ ```tsx
82
82
  const sheet = useRef<TrueSheet>(null)
83
83
 
84
84
  const resize = () => {
@@ -108,7 +108,7 @@ return (
108
108
 
109
109
  ## Events
110
110
 
111
- ```ts
111
+ ```tsx
112
112
  const handleSizeChange = (info: SizeInfo) => {
113
113
  console.log(info)
114
114
  }
@@ -130,7 +130,7 @@ return (
130
130
 
131
131
  ### `SheetSize`
132
132
 
133
- ```ts
133
+ ```tsx
134
134
  <TrueSheet sizes={['auto', '80%', 'large']}>
135
135
  // ...
136
136
  </TrueSheet>
@@ -166,7 +166,7 @@ Grabber props to be used for android grabber or handle.
166
166
 
167
167
  Blur tint that is mapped into native values in iOS.
168
168
 
169
- ```ts
169
+ ```tsx
170
170
  <TrueSheet blurTint="dark">
171
171
  // ...
172
172
  </TrueSheet>
@@ -200,7 +200,7 @@ Blur tint that is mapped into native values in iOS.
200
200
 
201
201
  `Object` that comes with some events.
202
202
 
203
- ```ts
203
+ ```tsx
204
204
  {
205
205
  index: 1,
206
206
  value: 69
@@ -212,21 +212,41 @@ Blur tint that is mapped into native values in iOS.
212
212
  | index | `number` | The size index from the provided sizes. See `sizes` prop. |
213
213
  | value | `number` | The actual height value of the size. |
214
214
 
215
+ ## Testing
216
+
217
+ When using `jest`, simply mock the entire package.
218
+
219
+ ```tsx
220
+ jest.mock('@lodev09/react-native-true-sheet')
221
+ ```
222
+
215
223
  ## Troubleshooting
216
224
 
225
+ ### Handling `ScrollView` on **Android**
226
+
227
+ On Android, `nestedScrollEnabled` needs to be enabled so that scrolling works when the sheet is expanded to its `maxHeight`.
228
+
229
+ ```tsx
230
+ <TrueSheet ref={sheet}>
231
+ <ScrollView nestedScrollEnabled>
232
+ // ...
233
+ </ScrollView>
234
+ </TrueSheet>
235
+ ```
236
+
217
237
  ### Using `react-native-gesture-handler` on **Android**
218
238
 
219
239
  On Android, RNGH does not work by default because modals are not located under React Native Root view in native hierarchy. To fix that, components need to be wrapped with `GestureHandlerRootView`.
220
240
 
221
241
  Example:
222
- ```ts
242
+ ```tsx
223
243
  import { GestureHandlerRootView } from 'react-native-gesture-handler'
224
244
  ```
225
- ```ts
245
+ ```tsx
226
246
  return (
227
247
  <TrueSheet ref={sheet}>
228
248
  <GestureHandlerRootView>
229
- <MyComponent />
249
+ // ...
230
250
  </GestureHandlerRootView>
231
251
  </TrueSheet>
232
252
  )
@@ -237,7 +257,7 @@ return (
237
257
  On iOS, navigating to a [React Navigation](https://reactnavigation.org) screen from within the Sheet can cause issues. To resolve this, dismiss the sheet before navigating!
238
258
 
239
259
  Example:
240
- ```ts
260
+ ```tsx
241
261
  const sheet = useRef<TrueSheet>(null)
242
262
 
243
263
  const navigate = async () => {
@@ -3,38 +3,34 @@ package com.lodev09.truesheet
3
3
  import android.graphics.Color
4
4
  import android.view.ViewGroup
5
5
  import android.view.WindowManager
6
- import android.widget.LinearLayout
7
- import androidx.coordinatorlayout.widget.CoordinatorLayout
8
6
  import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
9
8
  import com.google.android.material.bottomsheet.BottomSheetDialog
10
9
  import com.lodev09.truesheet.core.KeyboardManager
11
- import com.lodev09.truesheet.core.RootViewGroup
10
+ import com.lodev09.truesheet.core.RootSheetView
12
11
  import com.lodev09.truesheet.core.Utils
13
12
 
14
- class TrueSheetDialog(
15
- private val reactContext: ThemedReactContext,
16
- private val behavior: TrueSheetBehavior,
17
- private val rootViewGroup: RootViewGroup
18
- ) : BottomSheetDialog(reactContext) {
13
+ data class SizeInfo(val index: Int, val value: Float)
14
+
15
+ class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: RootSheetView) :
16
+ BottomSheetDialog(reactContext) {
19
17
 
20
18
  private var keyboardManager = KeyboardManager(reactContext)
21
19
 
22
- var sheetView: ViewGroup
20
+ var maxScreenHeight: Int = 0
21
+ var maxSheetHeight: Int? = null
23
22
 
24
- init {
25
- LinearLayout(reactContext).apply {
26
- addView(rootViewGroup)
27
- setContentView(this)
23
+ var contentView: ViewGroup? = null
24
+ var footerView: ViewGroup? = null
28
25
 
29
- sheetView = parent as ViewGroup
26
+ var sizes: Array<Any> = arrayOf("medium", "large")
30
27
 
31
- // Set to transparent background to support corner radius
32
- sheetView.setBackgroundColor(Color.TRANSPARENT)
28
+ var sheetView: ViewGroup
33
29
 
34
- // Assign our main BottomSheetBehavior
35
- val sheetViewParams = sheetView.layoutParams as CoordinatorLayout.LayoutParams
36
- sheetViewParams.behavior = behavior
37
- }
30
+ init {
31
+ setContentView(rootSheetView)
32
+ sheetView = rootSheetView.parent as ViewGroup
33
+ sheetView.setBackgroundColor(Color.TRANSPARENT)
38
34
 
39
35
  // Setup window params to adjust layout based on Keyboard state.
40
36
  window?.apply {
@@ -47,15 +43,95 @@ class TrueSheetDialog(
47
43
 
48
44
  fun show(sizeIndex: Int) {
49
45
  if (isShowing) {
50
- behavior.setStateForSizeIndex(sizeIndex)
46
+ setStateForSizeIndex(sizeIndex)
51
47
  } else {
52
- behavior.configure()
53
- behavior.setStateForSizeIndex(sizeIndex)
48
+ configure()
49
+ setStateForSizeIndex(sizeIndex)
54
50
 
55
51
  this.show()
56
52
  }
57
53
  }
58
54
 
55
+ /**
56
+ * Set the state based on the given size index.
57
+ */
58
+ private fun setStateForSizeIndex(index: Int) {
59
+ behavior.state = getStateForSizeIndex(index)
60
+ }
61
+
62
+ /**
63
+ * Get the height value based on the size config value.
64
+ */
65
+ private fun getSizeHeight(size: Any, contentHeight: Int): Int {
66
+ val height =
67
+ when (size) {
68
+ is Double -> Utils.toPixel(size)
69
+
70
+ is Int -> Utils.toPixel(size.toDouble())
71
+
72
+ is String -> {
73
+ when (size) {
74
+ "auto" -> contentHeight
75
+
76
+ "large" -> maxScreenHeight
77
+
78
+ "medium" -> (maxScreenHeight * 0.50).toInt()
79
+
80
+ "small" -> (maxScreenHeight * 0.25).toInt()
81
+
82
+ else -> {
83
+ if (size.endsWith('%')) {
84
+ val percent = size.trim('%').toDoubleOrNull()
85
+ if (percent == null) {
86
+ 0
87
+ } else {
88
+ ((percent / 100) * maxScreenHeight).toInt()
89
+ }
90
+ } else {
91
+ val fixedHeight = size.toDoubleOrNull()
92
+ if (fixedHeight == null) {
93
+ 0
94
+ } else {
95
+ Utils.toPixel(fixedHeight)
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ else -> (maxScreenHeight * 0.5).toInt()
103
+ }
104
+
105
+ return minOf(height, maxSheetHeight ?: maxScreenHeight)
106
+ }
107
+
108
+ /**
109
+ * Determines the state based on the given size index.
110
+ */
111
+ private fun getStateForSizeIndex(index: Int) =
112
+ when (sizes.size) {
113
+ 1 -> BottomSheetBehavior.STATE_EXPANDED
114
+
115
+ 2 -> {
116
+ when (index) {
117
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
118
+ 1 -> BottomSheetBehavior.STATE_EXPANDED
119
+ else -> BottomSheetBehavior.STATE_HIDDEN
120
+ }
121
+ }
122
+
123
+ 3 -> {
124
+ when (index) {
125
+ 0 -> BottomSheetBehavior.STATE_COLLAPSED
126
+ 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
127
+ 2 -> BottomSheetBehavior.STATE_EXPANDED
128
+ else -> BottomSheetBehavior.STATE_HIDDEN
129
+ }
130
+ }
131
+
132
+ else -> BottomSheetBehavior.STATE_HIDDEN
133
+ }
134
+
59
135
  /**
60
136
  * Handle keyboard state changes and adjust maxScreenHeight (sheet max height) accordingly.
61
137
  * Also update footer's Y position.
@@ -63,13 +139,13 @@ class TrueSheetDialog(
63
139
  fun registerKeyboardManager() {
64
140
  keyboardManager.registerKeyboardListener(object : KeyboardManager.OnKeyboardListener {
65
141
  override fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?) {
66
- when (isVisible) {
67
- true -> behavior.maxScreenHeight = visibleHeight ?: 0
68
- else -> behavior.maxScreenHeight = Utils.screenHeight(reactContext)
142
+ maxScreenHeight = when (isVisible) {
143
+ true -> visibleHeight ?: 0
144
+ else -> Utils.screenHeight(reactContext)
69
145
  }
70
146
 
71
- behavior.footerView?.apply {
72
- y = (behavior.maxScreenHeight - (sheetView.top ?: 0) - height).toFloat()
147
+ footerView?.apply {
148
+ y = (maxScreenHeight - (sheetView.top ?: 0) - height).toFloat()
73
149
  }
74
150
  }
75
151
  })
@@ -82,6 +158,93 @@ class TrueSheetDialog(
82
158
  keyboardManager.unregisterKeyboardListener()
83
159
  }
84
160
 
161
+ /**
162
+ * Configure the sheet based on size preferences.
163
+ */
164
+ fun configure() {
165
+ // Update the usable sheet height
166
+ maxScreenHeight = Utils.screenHeight(reactContext)
167
+
168
+ var contentHeight = 0
169
+
170
+ contentView?.let { contentHeight = it.height }
171
+ footerView?.let { contentHeight += it.height }
172
+
173
+ // Configure sheet sizes
174
+ behavior.apply {
175
+ skipCollapsed = false
176
+ isFitToContents = true
177
+
178
+ // m3 max width 640dp
179
+ maxWidth = Utils.toPixel(640.0)
180
+
181
+ when (sizes.size) {
182
+ 1 -> {
183
+ maxHeight = getSizeHeight(sizes[0], contentHeight)
184
+ peekHeight = maxHeight
185
+ skipCollapsed = true
186
+ }
187
+
188
+ 2 -> {
189
+ peekHeight = getSizeHeight(sizes[0], contentHeight)
190
+ maxHeight = getSizeHeight(sizes[1], contentHeight)
191
+ }
192
+
193
+ 3 -> {
194
+ // Enables half expanded
195
+ isFitToContents = false
196
+
197
+ peekHeight = getSizeHeight(sizes[0], contentHeight)
198
+ halfExpandedRatio = getSizeHeight(sizes[1], contentHeight).toFloat() / maxScreenHeight.toFloat()
199
+ maxHeight = getSizeHeight(sizes[2], contentHeight)
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Get the SizeInfo data by state.
207
+ */
208
+ fun getSizeInfoForState(state: Int): SizeInfo? =
209
+ when (sizes.size) {
210
+ 1 -> {
211
+ when (state) {
212
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(behavior.maxHeight))
213
+ else -> null
214
+ }
215
+ }
216
+
217
+ 2 -> {
218
+ when (state) {
219
+ BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
220
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(behavior.maxHeight))
221
+ else -> null
222
+ }
223
+ }
224
+
225
+ 3 -> {
226
+ when (state) {
227
+ BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
228
+
229
+ BottomSheetBehavior.STATE_HALF_EXPANDED -> {
230
+ val height = behavior.halfExpandedRatio * maxScreenHeight
231
+ SizeInfo(1, Utils.toDIP(height.toInt()))
232
+ }
233
+
234
+ BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(behavior.maxHeight))
235
+
236
+ else -> null
237
+ }
238
+ }
239
+
240
+ else -> null
241
+ }
242
+
243
+ /**
244
+ * Get SizeInfo data for given size index.
245
+ */
246
+ fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f)
247
+
85
248
  companion object {
86
249
  const val TAG = "TrueSheetView"
87
250
  }
@@ -1,6 +1,7 @@
1
1
  package com.lodev09.truesheet
2
2
 
3
3
  import android.content.Context
4
+ import android.util.Log
4
5
  import android.view.View
5
6
  import android.view.ViewGroup
6
7
  import android.view.ViewStructure
@@ -13,7 +14,7 @@ import com.facebook.react.uimanager.events.EventDispatcher
13
14
  import com.google.android.material.bottomsheet.BottomSheetBehavior
14
15
  import com.lodev09.truesheet.core.DismissEvent
15
16
  import com.lodev09.truesheet.core.PresentEvent
16
- import com.lodev09.truesheet.core.RootViewGroup
17
+ import com.lodev09.truesheet.core.RootSheetView
17
18
  import com.lodev09.truesheet.core.SizeChangeEvent
18
19
 
19
20
  class TrueSheetView(context: Context) :
@@ -47,15 +48,10 @@ class TrueSheetView(context: Context) :
47
48
  */
48
49
  private val sheetDialog: TrueSheetDialog
49
50
 
50
- /**
51
- * The custom BottomSheetDialogBehavior instance.
52
- */
53
- private val sheetBehavior: TrueSheetBehavior
54
-
55
51
  /**
56
52
  * React root view placeholder.
57
53
  */
58
- private val sheetRootView: RootViewGroup
54
+ private val rootSheetView: RootSheetView
59
55
 
60
56
  /**
61
57
  * 1st child of the container view.
@@ -71,11 +67,10 @@ class TrueSheetView(context: Context) :
71
67
  reactContext.addLifecycleEventListener(this)
72
68
  eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
73
69
 
74
- sheetRootView = RootViewGroup(context)
75
- sheetRootView.eventDispatcher = eventDispatcher
70
+ rootSheetView = RootSheetView(context)
71
+ rootSheetView.eventDispatcher = eventDispatcher
76
72
 
77
- sheetBehavior = TrueSheetBehavior(reactContext)
78
- sheetDialog = TrueSheetDialog(reactContext, sheetBehavior, sheetRootView)
73
+ sheetDialog = TrueSheetDialog(reactContext, rootSheetView)
79
74
 
80
75
  // Configure Sheet Dialog
81
76
  sheetDialog.apply {
@@ -86,7 +81,7 @@ class TrueSheetView(context: Context) :
86
81
  // Initialize footer y
87
82
  footerView?.apply {
88
83
  UiThreadUtil.runOnUiThread {
89
- y = (sheetBehavior.maxScreenHeight - sheetView.top - height).toFloat()
84
+ y = (sheetDialog.maxScreenHeight - sheetView.top - height).toFloat()
90
85
  }
91
86
  }
92
87
 
@@ -96,11 +91,12 @@ class TrueSheetView(context: Context) :
96
91
  }
97
92
 
98
93
  // dispatch onPresent event
99
- eventDispatcher?.dispatchEvent(PresentEvent(surfaceId, id, sheetBehavior.getSizeInfoForIndex(activeIndex)))
94
+ eventDispatcher?.dispatchEvent(PresentEvent(surfaceId, id, sheetDialog.getSizeInfoForIndex(activeIndex)))
100
95
  }
101
96
 
102
97
  // Setup listener when the dialog has been dismissed.
103
98
  setOnDismissListener {
99
+ Log.d(TAG, "dismissed")
104
100
  unregisterKeyboardManager()
105
101
  dismissPromise?.let { promise ->
106
102
  promise()
@@ -113,8 +109,8 @@ class TrueSheetView(context: Context) :
113
109
  }
114
110
 
115
111
  // Configure sheet behavior events
116
- sheetBehavior.apply {
117
- addBottomSheetCallback(
112
+ sheetDialog.apply {
113
+ behavior.addBottomSheetCallback(
118
114
  object : BottomSheetBehavior.BottomSheetCallback() {
119
115
  override fun onSlide(sheetView: View, slideOffset: Float) {
120
116
  footerView?.let {
@@ -128,24 +124,18 @@ class TrueSheetView(context: Context) :
128
124
  }
129
125
 
130
126
  override fun onStateChanged(view: View, newState: Int) {
131
- when (newState) {
132
- BottomSheetBehavior.STATE_HIDDEN -> sheetDialog.dismiss()
133
-
134
- else -> {
135
- val sizeInfo = getSizeInfoForState(newState)
136
- if (sizeInfo != null && sizeInfo.index != activeIndex) {
137
- // Invoke promise when sheet resized programmatically
138
- presentPromise?.let { promise ->
139
- promise()
140
- presentPromise = null
141
- }
142
-
143
- activeIndex = sizeInfo.index
144
-
145
- // dispatch onSizeChange event
146
- eventDispatcher?.dispatchEvent(SizeChangeEvent(surfaceId, id, sizeInfo))
147
- }
127
+ val sizeInfo = getSizeInfoForState(newState)
128
+ if (sizeInfo != null && sizeInfo.index != activeIndex) {
129
+ // Invoke promise when sheet resized programmatically
130
+ presentPromise?.let { promise ->
131
+ promise()
132
+ presentPromise = null
148
133
  }
134
+
135
+ activeIndex = sizeInfo.index
136
+
137
+ // dispatch onSizeChange event
138
+ eventDispatcher?.dispatchEvent(SizeChangeEvent(surfaceId, id, sizeInfo))
149
139
  }
150
140
  }
151
141
  }
@@ -154,7 +144,7 @@ class TrueSheetView(context: Context) :
154
144
  }
155
145
 
156
146
  override fun dispatchProvideStructure(structure: ViewStructure) {
157
- sheetRootView.dispatchProvideStructure(structure)
147
+ rootSheetView.dispatchProvideStructure(structure)
158
148
  }
159
149
 
160
150
  override fun onLayout(
@@ -181,29 +171,29 @@ class TrueSheetView(context: Context) :
181
171
  contentView = it.getChildAt(0) as ViewGroup
182
172
  footerView = it.getChildAt(1) as ViewGroup
183
173
 
184
- sheetBehavior.contentView = contentView
185
- sheetBehavior.footerView = footerView
174
+ sheetDialog.contentView = contentView
175
+ sheetDialog.footerView = footerView
186
176
 
187
177
  // rootView's first child is the Container View
188
- sheetRootView.addView(it, index)
178
+ rootSheetView.addView(it, index)
189
179
  }
190
180
  }
191
181
 
192
182
  override fun getChildCount(): Int {
193
183
  // This method may be called by the parent constructor
194
184
  // before rootView is initialized.
195
- return sheetRootView.childCount
185
+ return rootSheetView.childCount
196
186
  }
197
187
 
198
- override fun getChildAt(index: Int): View = sheetRootView.getChildAt(index)
188
+ override fun getChildAt(index: Int): View = rootSheetView.getChildAt(index)
199
189
 
200
190
  override fun removeView(child: View) {
201
- sheetRootView.removeView(child)
191
+ rootSheetView.removeView(child)
202
192
  }
203
193
 
204
194
  override fun removeViewAt(index: Int) {
205
195
  val child = getChildAt(index)
206
- sheetRootView.removeView(child)
196
+ rootSheetView.removeView(child)
207
197
  }
208
198
 
209
199
  override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
@@ -232,18 +222,18 @@ class TrueSheetView(context: Context) :
232
222
  }
233
223
 
234
224
  fun setMaxHeight(height: Int) {
235
- sheetBehavior.maxSheetHeight = height
236
- sheetBehavior.configure()
225
+ sheetDialog.maxSheetHeight = height
226
+ sheetDialog.configure()
237
227
  }
238
228
 
239
229
  fun setDismissible(dismissible: Boolean) {
240
- sheetBehavior.isHideable = dismissible
230
+ sheetDialog.behavior.isHideable = dismissible
241
231
  sheetDialog.setCancelable(dismissible)
242
232
  }
243
233
 
244
234
  fun setSizes(newSizes: Array<Any>) {
245
- sheetBehavior.sizes = newSizes
246
- sheetBehavior.configure()
235
+ sheetDialog.sizes = newSizes
236
+ sheetDialog.configure()
247
237
  }
248
238
 
249
239
  /**
@@ -15,8 +15,8 @@ import com.facebook.react.uimanager.events.EventDispatcher
15
15
  import com.facebook.react.views.view.ReactViewGroup
16
16
 
17
17
  /**
18
- * RootViewGroup is the ViewGroup which contains all the children of a Modal. It gets all
19
- * child information forwarded from ReactModalHostView and uses that to create children. It is
18
+ * RootSheetView is the ViewGroup which contains all the children of a Modal. It gets all
19
+ * child information forwarded from TrueSheetView and uses that to create children. It is
20
20
  * also responsible for acting as a RootView and handling touch events. It does this the same way
21
21
  * as ReactRootView.
22
22
  *
@@ -26,7 +26,7 @@ import com.facebook.react.views.view.ReactViewGroup
26
26
  * styleHeight on the LayoutShadowNode to be the window size. This is done through the
27
27
  * UIManagerModule, and will then cause the children to layout as if they can fill the window.
28
28
  */
29
- class RootViewGroup(context: Context?) :
29
+ class RootSheetView(context: Context?) :
30
30
  ReactViewGroup(context),
31
31
  RootView {
32
32
  private var hasAdjustedSize = false
@@ -34,7 +34,6 @@ class RootViewGroup(context: Context?) :
34
34
  private var viewHeight = 0
35
35
 
36
36
  private val mJSTouchDispatcher = JSTouchDispatcher(this)
37
-
38
37
  private var mJSPointerDispatcher: JSPointerDispatcher? = null
39
38
 
40
39
  var eventDispatcher: EventDispatcher? = null
@@ -134,8 +133,4 @@ class RootViewGroup(context: Context?) :
134
133
  // No-op - override in order to still receive events to onInterceptTouchEvent
135
134
  // even when some other view disallow that
136
135
  }
137
-
138
- companion object {
139
- const val TAG = "TrueSheetView"
140
- }
141
136
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lodev09/react-native-true-sheet",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "The true native bottom sheet. 💩",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -1,230 +0,0 @@
1
- package com.lodev09.truesheet
2
-
3
- import android.view.MotionEvent
4
- import android.view.ViewGroup
5
- import android.widget.ScrollView
6
- import androidx.coordinatorlayout.widget.CoordinatorLayout
7
- import com.facebook.react.bridge.ReactContext
8
- import com.google.android.material.bottomsheet.BottomSheetBehavior
9
- import com.lodev09.truesheet.core.Utils
10
-
11
- data class SizeInfo(val index: Int, val value: Float)
12
-
13
- class TrueSheetBehavior(private val reactContext: ReactContext) : BottomSheetBehavior<ViewGroup>() {
14
- var maxScreenHeight: Int = 0
15
- var maxSheetHeight: Int? = null
16
-
17
- var contentView: ViewGroup? = null
18
- var footerView: ViewGroup? = null
19
-
20
- var sizes: Array<Any> = arrayOf("medium", "large")
21
-
22
- override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: ViewGroup, event: MotionEvent): Boolean {
23
- contentView?.let {
24
- val isDownEvent = (event.actionMasked == MotionEvent.ACTION_DOWN)
25
- val expanded = state == STATE_EXPANDED
26
-
27
- if (isDownEvent && expanded) {
28
- for (i in 0 until it.childCount) {
29
- val contentChild = it.getChildAt(i)
30
- val scrolled = (contentChild is ScrollView && contentChild.scrollY > 0)
31
-
32
- if (!scrolled) continue
33
- if (isInsideSheet(contentChild as ScrollView, event)) {
34
- return false
35
- }
36
- }
37
- }
38
- }
39
-
40
- return super.onInterceptTouchEvent(parent, child, event)
41
- }
42
-
43
- private fun isInsideSheet(scrollView: ScrollView, event: MotionEvent): Boolean {
44
- val x = event.x
45
- val y = event.y
46
-
47
- val position = IntArray(2)
48
- scrollView.getLocationOnScreen(position)
49
-
50
- val nestedX = position[0]
51
- val nestedY = position[1]
52
-
53
- val boundRight = nestedX + scrollView.width
54
- val boundBottom = nestedY + scrollView.height
55
-
56
- return (x > nestedX && x < boundRight && y > nestedY && y < boundBottom) ||
57
- event.action == MotionEvent.ACTION_CANCEL
58
- }
59
-
60
- /**
61
- * Get the height value based on the size config value.
62
- */
63
- private fun getSizeHeight(size: Any, contentHeight: Int): Int {
64
- val height =
65
- when (size) {
66
- is Double -> Utils.toPixel(size)
67
-
68
- is Int -> Utils.toPixel(size.toDouble())
69
-
70
- is String -> {
71
- when (size) {
72
- "auto" -> contentHeight
73
-
74
- "large" -> maxScreenHeight
75
-
76
- "medium" -> (maxScreenHeight * 0.50).toInt()
77
-
78
- "small" -> (maxScreenHeight * 0.25).toInt()
79
-
80
- else -> {
81
- if (size.endsWith('%')) {
82
- val percent = size.trim('%').toDoubleOrNull()
83
- if (percent == null) {
84
- 0
85
- } else {
86
- ((percent / 100) * maxScreenHeight).toInt()
87
- }
88
- } else {
89
- val fixedHeight = size.toDoubleOrNull()
90
- if (fixedHeight == null) {
91
- 0
92
- } else {
93
- Utils.toPixel(fixedHeight)
94
- }
95
- }
96
- }
97
- }
98
- }
99
-
100
- else -> (maxScreenHeight * 0.5).toInt()
101
- }
102
-
103
- return minOf(height, maxSheetHeight ?: maxScreenHeight)
104
- }
105
-
106
- /**
107
- * Determines the state based on the given size index.
108
- */
109
- fun getStateForSizeIndex(index: Int) =
110
- when (sizes.size) {
111
- 1 -> STATE_EXPANDED
112
-
113
- 2 -> {
114
- when (index) {
115
- 0 -> STATE_COLLAPSED
116
- 1 -> STATE_EXPANDED
117
- else -> STATE_HIDDEN
118
- }
119
- }
120
-
121
- 3 -> {
122
- when (index) {
123
- 0 -> STATE_COLLAPSED
124
- 1 -> STATE_HALF_EXPANDED
125
- 2 -> STATE_EXPANDED
126
- else -> STATE_HIDDEN
127
- }
128
- }
129
-
130
- else -> STATE_HIDDEN
131
- }
132
-
133
- /**
134
- * Configure the sheet based on size preferences.
135
- */
136
- fun configure() {
137
- // Update the usable sheet height
138
- maxScreenHeight = Utils.screenHeight(reactContext)
139
-
140
- var contentHeight = 0
141
-
142
- contentView?.let { contentHeight = it.height }
143
- footerView?.let { contentHeight += it.height }
144
-
145
- // Configure sheet sizes
146
- apply {
147
- skipCollapsed = false
148
- isFitToContents = true
149
-
150
- // m3 max width 640dp
151
- maxWidth = Utils.toPixel(640.0)
152
-
153
- when (sizes.size) {
154
- 1 -> {
155
- maxHeight = getSizeHeight(sizes[0], contentHeight)
156
- peekHeight = maxHeight
157
- skipCollapsed = true
158
- }
159
-
160
- 2 -> {
161
- peekHeight = getSizeHeight(sizes[0], contentHeight)
162
- maxHeight = getSizeHeight(sizes[1], contentHeight)
163
- }
164
-
165
- 3 -> {
166
- // Enables half expanded
167
- isFitToContents = false
168
-
169
- peekHeight = getSizeHeight(sizes[0], contentHeight)
170
- halfExpandedRatio = getSizeHeight(sizes[1], contentHeight).toFloat() / maxScreenHeight.toFloat()
171
- maxHeight = getSizeHeight(sizes[2], contentHeight)
172
- }
173
- }
174
- }
175
- }
176
-
177
- /**
178
- * Get the SizeInfo data by state.
179
- */
180
- fun getSizeInfoForState(state: Int): SizeInfo? =
181
- when (sizes.size) {
182
- 1 -> {
183
- when (state) {
184
- STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(maxHeight))
185
- else -> null
186
- }
187
- }
188
-
189
- 2 -> {
190
- when (state) {
191
- STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(peekHeight))
192
- STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(maxHeight))
193
- else -> null
194
- }
195
- }
196
-
197
- 3 -> {
198
- when (state) {
199
- STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(peekHeight))
200
-
201
- STATE_HALF_EXPANDED -> {
202
- val height = halfExpandedRatio * maxScreenHeight
203
- SizeInfo(1, Utils.toDIP(height.toInt()))
204
- }
205
-
206
- STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(maxHeight))
207
-
208
- else -> null
209
- }
210
- }
211
-
212
- else -> null
213
- }
214
-
215
- /**
216
- * Get SizeInfo data for given size index.
217
- */
218
- fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f)
219
-
220
- /**
221
- * Set the state based on the given size index.
222
- */
223
- fun setStateForSizeIndex(index: Int) {
224
- state = getStateForSizeIndex(index)
225
- }
226
-
227
- companion object {
228
- const val TAG = "TrueSheetView"
229
- }
230
- }