react-native-drum-picker 0.1.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/drumpicker/DrumPickerAdapter.kt +108 -0
- package/android/src/main/java/com/drumpicker/DrumPickerChangeEvent.kt +25 -0
- package/android/src/main/java/com/drumpicker/DrumPickerDefaults.kt +19 -0
- package/android/src/main/java/com/drumpicker/DrumPickerPackage.kt +17 -0
- package/android/src/main/java/com/drumpicker/DrumPickerView.kt +583 -0
- package/android/src/main/java/com/drumpicker/DrumPickerViewManager.kt +108 -0
- package/lib/module/DateDrumPicker.js +153 -0
- package/lib/module/DrumPicker.js +6 -0
- package/lib/module/DrumPicker.native.js +63 -0
- package/lib/module/DrumPickerViewNativeComponent.ts +31 -0
- package/lib/module/dateDrumPickerLogic.js +82 -0
- package/lib/module/index.js +5 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +4 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DateDrumPicker.d.ts +32 -0
- package/lib/typescript/src/DrumPicker.d.ts +3 -0
- package/lib/typescript/src/DrumPicker.native.d.ts +3 -0
- package/lib/typescript/src/DrumPickerViewNativeComponent.d.ts +25 -0
- package/lib/typescript/src/dateDrumPickerLogic.d.ts +20 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/types.d.ts +24 -0
- package/package.json +189 -0
- package/react-native.config.js +13 -0
- package/src/DateDrumPicker.tsx +267 -0
- package/src/DrumPicker.native.tsx +68 -0
- package/src/DrumPicker.tsx +7 -0
- package/src/DrumPickerViewNativeComponent.ts +31 -0
- package/src/dateDrumPickerLogic.ts +95 -0
- package/src/index.tsx +15 -0
- package/src/types.ts +25 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
package com.drumpicker
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.util.AttributeSet
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.FrameLayout
|
|
8
|
+
import androidx.recyclerview.widget.LinearLayoutManager
|
|
9
|
+
import androidx.recyclerview.widget.LinearSnapHelper
|
|
10
|
+
import androidx.recyclerview.widget.RecyclerView
|
|
11
|
+
import com.facebook.react.bridge.ColorPropConverter
|
|
12
|
+
import com.facebook.react.bridge.ReadableArray
|
|
13
|
+
import com.facebook.react.bridge.ReadableType
|
|
14
|
+
import com.facebook.react.bridge.ReactContext
|
|
15
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
16
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
17
|
+
import kotlin.math.abs
|
|
18
|
+
|
|
19
|
+
class DrumPickerView @JvmOverloads constructor(
|
|
20
|
+
context: Context,
|
|
21
|
+
attrs: AttributeSet? = null,
|
|
22
|
+
) : FrameLayout(context, attrs) {
|
|
23
|
+
|
|
24
|
+
private val recyclerView = RecyclerView(context)
|
|
25
|
+
private val topIndicator = View(context)
|
|
26
|
+
private val bottomIndicator = View(context)
|
|
27
|
+
private val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
|
28
|
+
private val snapHelper = LinearSnapHelper()
|
|
29
|
+
private val adapter = DrumPickerAdapter { dpToPx(itemHeightDp) }
|
|
30
|
+
|
|
31
|
+
private var items: List<String> = emptyList()
|
|
32
|
+
private var selectedIndex = 0
|
|
33
|
+
private var itemHeightDp = DrumPickerDefaults.ITEM_HEIGHT_DP
|
|
34
|
+
private var visibleItemCount = DrumPickerDefaults.VISIBLE_ITEM_COUNT
|
|
35
|
+
private var textColor = DrumPickerDefaults.TEXT_COLOR
|
|
36
|
+
private var selectedTextColor = DrumPickerDefaults.SELECTED_TEXT_COLOR
|
|
37
|
+
private var textSizeSp = DrumPickerDefaults.TEXT_SIZE_SP
|
|
38
|
+
private var selectedTextSizeSp = DrumPickerDefaults.SELECTED_TEXT_SIZE_SP
|
|
39
|
+
private var showSelectionIndicator = true
|
|
40
|
+
private var selectionIndicatorColor = DrumPickerDefaults.SELECTION_INDICATOR_COLOR
|
|
41
|
+
private var selectionIndicatorHeightDp = DrumPickerDefaults.SELECTION_INDICATOR_HEIGHT_DP
|
|
42
|
+
private var backgroundColor = DrumPickerDefaults.TRANSPARENT
|
|
43
|
+
private var containerBackgroundColor = DrumPickerDefaults.TRANSPARENT
|
|
44
|
+
private var itemBackgroundColor = DrumPickerDefaults.TRANSPARENT
|
|
45
|
+
|
|
46
|
+
private var itemHeightPx = dpToPx(itemHeightDp)
|
|
47
|
+
private var selectionIndicatorHeightPx = dpToPx(selectionIndicatorHeightDp)
|
|
48
|
+
private var lastEmittedIndex = -1
|
|
49
|
+
private var suppressChangeEvent = false
|
|
50
|
+
private var isDetached = false
|
|
51
|
+
private var styleUpdatePosted = false
|
|
52
|
+
|
|
53
|
+
private val styleUpdateRunnable =
|
|
54
|
+
Runnable {
|
|
55
|
+
styleUpdatePosted = false
|
|
56
|
+
if (!isDetached) {
|
|
57
|
+
updateVisibleItemStyles()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private val scrollListener =
|
|
62
|
+
object : RecyclerView.OnScrollListener() {
|
|
63
|
+
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
64
|
+
if (isDetached) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
scheduleVisibleItemStyleUpdate()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
71
|
+
if (isDetached) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
when (newState) {
|
|
75
|
+
RecyclerView.SCROLL_STATE_DRAGGING,
|
|
76
|
+
RecyclerView.SCROLL_STATE_SETTLING,
|
|
77
|
+
-> updateVisibleItemStyles()
|
|
78
|
+
RecyclerView.SCROLL_STATE_IDLE -> {
|
|
79
|
+
updateVisibleItemStyles()
|
|
80
|
+
if (suppressChangeEvent) {
|
|
81
|
+
val centerIndex = findSnapCenterIndex()
|
|
82
|
+
if (centerIndex != RecyclerView.NO_POSITION) {
|
|
83
|
+
selectedIndex = centerIndex
|
|
84
|
+
lastEmittedIndex = centerIndex
|
|
85
|
+
}
|
|
86
|
+
suppressChangeEvent = false
|
|
87
|
+
} else {
|
|
88
|
+
updateCenterFromSnap()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
init {
|
|
96
|
+
setBackgroundColor(DrumPickerDefaults.TRANSPARENT)
|
|
97
|
+
recyclerView.setBackgroundColor(DrumPickerDefaults.TRANSPARENT)
|
|
98
|
+
recyclerView.layoutManager = layoutManager
|
|
99
|
+
recyclerView.adapter = adapter
|
|
100
|
+
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
|
101
|
+
recyclerView.clipToPadding = false
|
|
102
|
+
snapHelper.attachToRecyclerView(recyclerView)
|
|
103
|
+
|
|
104
|
+
topIndicator.isClickable = false
|
|
105
|
+
topIndicator.isFocusable = false
|
|
106
|
+
bottomIndicator.isClickable = false
|
|
107
|
+
bottomIndicator.isFocusable = false
|
|
108
|
+
|
|
109
|
+
addView(
|
|
110
|
+
recyclerView,
|
|
111
|
+
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT),
|
|
112
|
+
)
|
|
113
|
+
addView(topIndicator, LayoutParams(LayoutParams.MATCH_PARENT, selectionIndicatorHeightPx))
|
|
114
|
+
addView(bottomIndicator, LayoutParams(LayoutParams.MATCH_PARENT, selectionIndicatorHeightPx))
|
|
115
|
+
|
|
116
|
+
adapter.distanceForPosition = { position -> distanceFromCenterForPosition(position) }
|
|
117
|
+
syncAdapterStyle()
|
|
118
|
+
applyBackgroundColors()
|
|
119
|
+
applyRecyclerPadding()
|
|
120
|
+
updateIndicatorAppearance()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fun setItemsProp(value: Any?) {
|
|
124
|
+
val newItems = parseItems(value)
|
|
125
|
+
if (newItems == items) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
recyclerView.stopScroll()
|
|
130
|
+
items = newItems
|
|
131
|
+
adapter.updateItems(newItems)
|
|
132
|
+
lastEmittedIndex = -1
|
|
133
|
+
|
|
134
|
+
if (items.isEmpty()) {
|
|
135
|
+
selectedIndex = selectedIndex.coerceAtLeast(0)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
selectedIndex = selectedIndex.coerceIn(0, items.size - 1)
|
|
140
|
+
runWhenAttached { scrollToSelectedIndex(animated = false, emit = false) }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fun setSelectedIndexProp(value: Any?) {
|
|
144
|
+
val index = toInt(value, selectedIndex)
|
|
145
|
+
val safeIndex =
|
|
146
|
+
if (items.isEmpty()) {
|
|
147
|
+
index.coerceAtLeast(0)
|
|
148
|
+
} else {
|
|
149
|
+
index.coerceIn(0, items.size - 1)
|
|
150
|
+
}
|
|
151
|
+
setSelectedIndex(safeIndex)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fun setItemHeightProp(value: Any?) {
|
|
155
|
+
setItemHeight(toFloat(value, itemHeightDp))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fun setVisibleItemCountProp(value: Any?) {
|
|
159
|
+
setVisibleItemCount(toInt(value, visibleItemCount))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun setTextColorProp(value: Any?) {
|
|
163
|
+
setTextColor(resolveColor(value, textColor))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fun setSelectedTextColorProp(value: Any?) {
|
|
167
|
+
setSelectedTextColor(resolveColor(value, selectedTextColor))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fun setTextSizeProp(value: Any?) {
|
|
171
|
+
setTextSize(toFloat(value, textSizeSp))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fun setSelectedTextSizeProp(value: Any?) {
|
|
175
|
+
setSelectedTextSize(toFloat(value, selectedTextSizeSp))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fun setShowSelectionIndicatorProp(value: Any?) {
|
|
179
|
+
showSelectionIndicator = toBoolean(value, true)
|
|
180
|
+
updateIndicatorAppearance()
|
|
181
|
+
requestLayout()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fun setSelectionIndicatorColorProp(value: Any?) {
|
|
185
|
+
selectionIndicatorColor = resolveColor(value, selectionIndicatorColor)
|
|
186
|
+
updateIndicatorAppearance()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fun setSelectionIndicatorHeightProp(value: Any?) {
|
|
190
|
+
val height = toFloat(value, selectionIndicatorHeightDp)
|
|
191
|
+
if (height <= 0f) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
selectionIndicatorHeightDp = height
|
|
195
|
+
selectionIndicatorHeightPx = dpToPx(selectionIndicatorHeightDp)
|
|
196
|
+
updateIndicatorAppearance()
|
|
197
|
+
requestLayout()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fun setBackgroundColorProp(value: Any?) {
|
|
201
|
+
backgroundColor = resolveBackgroundColor(value, backgroundColor)
|
|
202
|
+
applyBackgroundColors()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fun setContainerBackgroundColorProp(value: Any?) {
|
|
206
|
+
containerBackgroundColor = resolveBackgroundColor(value, containerBackgroundColor)
|
|
207
|
+
applyBackgroundColors()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun setItemBackgroundColorProp(value: Any?) {
|
|
211
|
+
itemBackgroundColor = resolveBackgroundColor(value, itemBackgroundColor)
|
|
212
|
+
applyBackgroundColors()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
216
|
+
val width = resolveSize(suggestedMinimumWidth, widthMeasureSpec)
|
|
217
|
+
val safeVisibleCount = visibleItemCount.coerceAtLeast(1)
|
|
218
|
+
val height =
|
|
219
|
+
resolveSize(
|
|
220
|
+
(itemHeightPx * safeVisibleCount).coerceAtLeast(suggestedMinimumHeight),
|
|
221
|
+
heightMeasureSpec,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
|
225
|
+
val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
226
|
+
recyclerView.measure(childWidthSpec, childHeightSpec)
|
|
227
|
+
setMeasuredDimension(width, height)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
231
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
232
|
+
val width = right - left
|
|
233
|
+
val height = bottom - top
|
|
234
|
+
recyclerView.layout(0, 0, width, height)
|
|
235
|
+
layoutSelectionIndicators(width, height)
|
|
236
|
+
if (changed) {
|
|
237
|
+
applyRecyclerPadding()
|
|
238
|
+
runWhenAttached { scrollToSelectedIndex(animated = false, emit = false) }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
override fun onAttachedToWindow() {
|
|
243
|
+
super.onAttachedToWindow()
|
|
244
|
+
isDetached = false
|
|
245
|
+
if (recyclerView.adapter == null) {
|
|
246
|
+
recyclerView.adapter = adapter
|
|
247
|
+
}
|
|
248
|
+
snapHelper.attachToRecyclerView(recyclerView)
|
|
249
|
+
recyclerView.addOnScrollListener(scrollListener)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
override fun onDetachedFromWindow() {
|
|
253
|
+
isDetached = true
|
|
254
|
+
recyclerView.removeCallbacks(styleUpdateRunnable)
|
|
255
|
+
styleUpdatePosted = false
|
|
256
|
+
removeCallbacks(null)
|
|
257
|
+
recyclerView.clearOnScrollListeners()
|
|
258
|
+
snapHelper.attachToRecyclerView(null)
|
|
259
|
+
recyclerView.adapter = null
|
|
260
|
+
super.onDetachedFromWindow()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fun setSelectedIndex(index: Int) {
|
|
264
|
+
if (items.isEmpty()) {
|
|
265
|
+
selectedIndex = index.coerceAtLeast(0)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
val clamped = index.coerceIn(0, items.size - 1)
|
|
269
|
+
val snapIndex = findSnapCenterIndex()
|
|
270
|
+
if (clamped == selectedIndex && snapIndex == clamped) {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
selectedIndex = clamped
|
|
274
|
+
scrollToSelectedIndex(animated = false, emit = false)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fun setItemHeight(height: Float) {
|
|
278
|
+
if (height <= 0f || height == itemHeightDp) {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
itemHeightDp = height
|
|
282
|
+
itemHeightPx = dpToPx(itemHeightDp)
|
|
283
|
+
adapter.itemHeightPx = itemHeightPx
|
|
284
|
+
adapter.notifyRowMetricsChanged()
|
|
285
|
+
applyRecyclerPadding()
|
|
286
|
+
requestLayout()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fun setVisibleItemCount(count: Int) {
|
|
290
|
+
if (count <= 0 || count == visibleItemCount) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
visibleItemCount = count
|
|
294
|
+
applyRecyclerPadding()
|
|
295
|
+
requestLayout()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fun setTextColor(color: Int) {
|
|
299
|
+
textColor = color
|
|
300
|
+
syncAdapterStyle()
|
|
301
|
+
updateVisibleItemStyles()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fun setSelectedTextColor(color: Int) {
|
|
305
|
+
selectedTextColor = color
|
|
306
|
+
syncAdapterStyle()
|
|
307
|
+
updateVisibleItemStyles()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fun setTextSize(size: Float) {
|
|
311
|
+
if (size <= 0f) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
textSizeSp = size
|
|
315
|
+
syncAdapterStyle()
|
|
316
|
+
updateVisibleItemStyles()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fun setSelectedTextSize(size: Float) {
|
|
320
|
+
if (size <= 0f) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
selectedTextSizeSp = size
|
|
324
|
+
syncAdapterStyle()
|
|
325
|
+
updateVisibleItemStyles()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private fun syncAdapterStyle() {
|
|
329
|
+
adapter.textColor = textColor
|
|
330
|
+
adapter.selectedTextColor = selectedTextColor
|
|
331
|
+
adapter.textSizeSp = textSizeSp
|
|
332
|
+
adapter.selectedTextSizeSp = selectedTextSizeSp
|
|
333
|
+
adapter.itemHeightPx = itemHeightPx
|
|
334
|
+
adapter.itemBackgroundColor = itemBackgroundColor
|
|
335
|
+
resetVisibleStyleBuckets()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun applyBackgroundColors() {
|
|
339
|
+
setBackgroundColor(backgroundColor)
|
|
340
|
+
recyclerView.setBackgroundColor(containerBackgroundColor)
|
|
341
|
+
adapter.itemBackgroundColor = itemBackgroundColor
|
|
342
|
+
if (adapter.itemCount > 0) {
|
|
343
|
+
adapter.notifyRowMetricsChanged()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private fun resetVisibleStyleBuckets() {
|
|
348
|
+
for (i in 0 until recyclerView.childCount) {
|
|
349
|
+
val holder =
|
|
350
|
+
recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
|
|
351
|
+
as? DrumPickerAdapter.ItemViewHolder ?: continue
|
|
352
|
+
holder.lastStyleBucket = Int.MIN_VALUE
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun applyRecyclerPadding() {
|
|
357
|
+
val verticalPadding = (itemHeightPx * (visibleItemCount - 1)) / 2
|
|
358
|
+
recyclerView.setPadding(0, verticalPadding, 0, verticalPadding)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private fun layoutSelectionIndicators(width: Int, height: Int) {
|
|
362
|
+
if (!showSelectionIndicator || width == 0 || height == 0) {
|
|
363
|
+
topIndicator.visibility = GONE
|
|
364
|
+
bottomIndicator.visibility = GONE
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
topIndicator.visibility = VISIBLE
|
|
369
|
+
bottomIndicator.visibility = VISIBLE
|
|
370
|
+
|
|
371
|
+
val bandTop = (height - itemHeightPx) / 2
|
|
372
|
+
val bandBottom = bandTop + itemHeightPx
|
|
373
|
+
topIndicator.layout(0, bandTop, width, bandTop + selectionIndicatorHeightPx)
|
|
374
|
+
bottomIndicator.layout(0, bandBottom - selectionIndicatorHeightPx, width, bandBottom)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private fun updateIndicatorAppearance() {
|
|
378
|
+
val visibility = if (showSelectionIndicator) VISIBLE else GONE
|
|
379
|
+
topIndicator.visibility = visibility
|
|
380
|
+
bottomIndicator.visibility = visibility
|
|
381
|
+
topIndicator.setBackgroundColor(selectionIndicatorColor)
|
|
382
|
+
bottomIndicator.setBackgroundColor(selectionIndicatorColor)
|
|
383
|
+
|
|
384
|
+
(topIndicator.layoutParams as LayoutParams).height = selectionIndicatorHeightPx
|
|
385
|
+
(bottomIndicator.layoutParams as LayoutParams).height = selectionIndicatorHeightPx
|
|
386
|
+
topIndicator.requestLayout()
|
|
387
|
+
bottomIndicator.requestLayout()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private fun scrollToSelectedIndex(animated: Boolean, emit: Boolean) {
|
|
391
|
+
if (isDetached || items.isEmpty()) {
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
suppressChangeEvent = !emit
|
|
395
|
+
val index = selectedIndex.coerceIn(0, items.size - 1)
|
|
396
|
+
if (animated) {
|
|
397
|
+
recyclerView.smoothScrollToPosition(index)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
layoutManager.scrollToPositionWithOffset(index, 0)
|
|
402
|
+
recyclerView.post {
|
|
403
|
+
if (isDetached) {
|
|
404
|
+
return@post
|
|
405
|
+
}
|
|
406
|
+
updateVisibleItemStyles()
|
|
407
|
+
if (emit) {
|
|
408
|
+
maybeEmitChange(index)
|
|
409
|
+
} else {
|
|
410
|
+
lastEmittedIndex = index
|
|
411
|
+
}
|
|
412
|
+
suppressChangeEvent = false
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private fun runWhenAttached(block: () -> Unit) {
|
|
417
|
+
if (isDetached) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
post {
|
|
421
|
+
if (isDetached) {
|
|
422
|
+
return@post
|
|
423
|
+
}
|
|
424
|
+
block()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private fun updateCenterFromSnap() {
|
|
429
|
+
if (isDetached || items.isEmpty()) {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
val centerIndex = findSnapCenterIndex()
|
|
433
|
+
if (centerIndex == RecyclerView.NO_POSITION) {
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
selectedIndex = centerIndex
|
|
438
|
+
updateVisibleItemStyles()
|
|
439
|
+
maybeEmitChange(centerIndex)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private fun findSnapCenterIndex(): Int {
|
|
443
|
+
if (items.isEmpty()) {
|
|
444
|
+
return RecyclerView.NO_POSITION
|
|
445
|
+
}
|
|
446
|
+
val centerView = snapHelper.findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
|
|
447
|
+
return layoutManager.getPosition(centerView)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun scheduleVisibleItemStyleUpdate() {
|
|
451
|
+
if (styleUpdatePosted || isDetached) {
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
styleUpdatePosted = true
|
|
455
|
+
recyclerView.postOnAnimation(styleUpdateRunnable)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private fun distanceFromCenterForPosition(position: Int): Float {
|
|
459
|
+
if (itemHeightPx <= 0 || recyclerView.height == 0) {
|
|
460
|
+
return 2f
|
|
461
|
+
}
|
|
462
|
+
val child = layoutManager.findViewByPosition(position) ?: return 2f
|
|
463
|
+
val pickerCenterY = recyclerView.height / 2f
|
|
464
|
+
val childCenterY = child.top + child.height / 2f
|
|
465
|
+
return abs(childCenterY - pickerCenterY) / itemHeightPx.toFloat()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private fun updateVisibleItemStyles() {
|
|
469
|
+
if (isDetached || recyclerView.height == 0 || itemHeightPx <= 0) {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
val pickerCenterY = recyclerView.height / 2f
|
|
474
|
+
val rowHeight = itemHeightPx.toFloat()
|
|
475
|
+
for (i in 0 until recyclerView.childCount) {
|
|
476
|
+
val child = recyclerView.getChildAt(i)
|
|
477
|
+
val holder = recyclerView.getChildViewHolder(child) as? DrumPickerAdapter.ItemViewHolder
|
|
478
|
+
?: continue
|
|
479
|
+
val childCenterY = child.top + child.height / 2f
|
|
480
|
+
val distance = abs(childCenterY - pickerCenterY) / rowHeight
|
|
481
|
+
adapter.applyItemStyle(holder, distance)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private fun maybeEmitChange(index: Int) {
|
|
486
|
+
if (isDetached || suppressChangeEvent || items.isEmpty()) {
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
if (index < 0 || index >= items.size) {
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
val clamped = index.coerceIn(0, items.size - 1)
|
|
493
|
+
if (clamped == lastEmittedIndex) {
|
|
494
|
+
selectedIndex = clamped
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
lastEmittedIndex = clamped
|
|
499
|
+
selectedIndex = clamped
|
|
500
|
+
|
|
501
|
+
val reactContext = context as? ReactContext ?: return
|
|
502
|
+
val dispatcher: EventDispatcher? = UIManagerHelper.getEventDispatcher(reactContext)
|
|
503
|
+
dispatcher?.dispatchEvent(
|
|
504
|
+
DrumPickerChangeEvent(
|
|
505
|
+
UIManagerHelper.getSurfaceId(reactContext),
|
|
506
|
+
id,
|
|
507
|
+
clamped,
|
|
508
|
+
items[clamped],
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private fun parseItems(value: Any?): List<String> {
|
|
514
|
+
when (value) {
|
|
515
|
+
null -> return emptyList()
|
|
516
|
+
is ReadableArray -> {
|
|
517
|
+
val parsed = ArrayList<String>(value.size())
|
|
518
|
+
for (i in 0 until value.size()) {
|
|
519
|
+
parsed.add(readArrayString(value, i))
|
|
520
|
+
}
|
|
521
|
+
return parsed
|
|
522
|
+
}
|
|
523
|
+
is List<*> -> return value.mapNotNull { it?.toString() }
|
|
524
|
+
is Array<*> -> return value.mapNotNull { it?.toString() }
|
|
525
|
+
else -> return emptyList()
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private fun readArrayString(array: ReadableArray, index: Int): String =
|
|
530
|
+
when (array.getType(index)) {
|
|
531
|
+
ReadableType.String -> array.getString(index) ?: ""
|
|
532
|
+
ReadableType.Number -> array.getDouble(index).toInt().toString()
|
|
533
|
+
else -> array.getDynamic(index).asString() ?: ""
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private fun resolveColor(value: Any?, fallback: Int): Int =
|
|
537
|
+
when (value) {
|
|
538
|
+
is Int -> value
|
|
539
|
+
null -> fallback
|
|
540
|
+
else -> ColorPropConverter.getColor(value, context) ?: fallback
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private fun resolveBackgroundColor(value: Any?, fallback: Int): Int {
|
|
544
|
+
if (value == null) {
|
|
545
|
+
return fallback
|
|
546
|
+
}
|
|
547
|
+
if (value is String && value.equals("transparent", ignoreCase = true)) {
|
|
548
|
+
return Color.TRANSPARENT
|
|
549
|
+
}
|
|
550
|
+
return resolveColor(value, fallback)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private fun toBoolean(value: Any?, fallback: Boolean): Boolean =
|
|
554
|
+
when (value) {
|
|
555
|
+
null -> fallback
|
|
556
|
+
is Boolean -> value
|
|
557
|
+
is Number -> value.toInt() != 0
|
|
558
|
+
else -> fallback
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private fun toInt(value: Any?, fallback: Int): Int =
|
|
562
|
+
when (value) {
|
|
563
|
+
null -> fallback
|
|
564
|
+
is Int -> value
|
|
565
|
+
is Double -> value.toInt()
|
|
566
|
+
is Float -> value.toInt()
|
|
567
|
+
is Number -> value.toInt()
|
|
568
|
+
else -> fallback
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private fun toFloat(value: Any?, fallback: Float): Float =
|
|
572
|
+
when (value) {
|
|
573
|
+
null -> fallback
|
|
574
|
+
is Float -> value
|
|
575
|
+
is Double -> value.toFloat()
|
|
576
|
+
is Int -> value.toFloat()
|
|
577
|
+
is Number -> value.toFloat()
|
|
578
|
+
else -> fallback
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private fun dpToPx(dp: Float): Int =
|
|
582
|
+
(dp * resources.displayMetrics.density + 0.5f).toInt()
|
|
583
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
package com.drumpicker
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReadableArray
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.ReactStylesDiffMap
|
|
6
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
7
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
8
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
9
|
+
import com.facebook.react.viewmanagers.DrumPickerViewManagerDelegate
|
|
10
|
+
import com.facebook.react.viewmanagers.DrumPickerViewManagerInterface
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = DrumPickerViewManager.NAME)
|
|
13
|
+
class DrumPickerViewManager :
|
|
14
|
+
SimpleViewManager<DrumPickerView>(),
|
|
15
|
+
DrumPickerViewManagerInterface<DrumPickerView> {
|
|
16
|
+
private val delegate: ViewManagerDelegate<DrumPickerView> =
|
|
17
|
+
DrumPickerViewManagerDelegate(this)
|
|
18
|
+
|
|
19
|
+
override fun getDelegate(): ViewManagerDelegate<DrumPickerView>? = delegate
|
|
20
|
+
|
|
21
|
+
override fun getName(): String = NAME
|
|
22
|
+
|
|
23
|
+
public override fun createViewInstance(context: ThemedReactContext): DrumPickerView =
|
|
24
|
+
DrumPickerView(context)
|
|
25
|
+
|
|
26
|
+
override fun updateProperties(view: DrumPickerView, props: ReactStylesDiffMap) {
|
|
27
|
+
for ((key, value) in props.toMap()) {
|
|
28
|
+
when (key) {
|
|
29
|
+
"items" -> view.setItemsProp(value)
|
|
30
|
+
"selectedIndex" -> view.setSelectedIndexProp(value)
|
|
31
|
+
"itemHeight" -> view.setItemHeightProp(value)
|
|
32
|
+
"visibleItemCount" -> view.setVisibleItemCountProp(value)
|
|
33
|
+
"textColor" -> view.setTextColorProp(value)
|
|
34
|
+
"selectedTextColor" -> view.setSelectedTextColorProp(value)
|
|
35
|
+
"textSize" -> view.setTextSizeProp(value)
|
|
36
|
+
"selectedTextSize" -> view.setSelectedTextSizeProp(value)
|
|
37
|
+
"showSelectionIndicator" -> view.setShowSelectionIndicatorProp(value)
|
|
38
|
+
"selectionIndicatorColor" -> view.setSelectionIndicatorColorProp(value)
|
|
39
|
+
"selectionIndicatorHeight" -> view.setSelectionIndicatorHeightProp(value)
|
|
40
|
+
"backgroundColor" -> view.setBackgroundColorProp(value)
|
|
41
|
+
"containerBackgroundColor" -> view.setContainerBackgroundColorProp(value)
|
|
42
|
+
"itemBackgroundColor" -> view.setItemBackgroundColorProp(value)
|
|
43
|
+
else -> delegate.setProperty(view, key, value)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onAfterUpdateTransaction(view)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun setItems(view: DrumPickerView?, value: ReadableArray?) {
|
|
50
|
+
view?.setItemsProp(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun setSelectedIndex(view: DrumPickerView?, value: Int) {
|
|
54
|
+
view?.setSelectedIndexProp(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun setItemHeight(view: DrumPickerView?, value: Float) {
|
|
58
|
+
view?.setItemHeightProp(value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun setVisibleItemCount(view: DrumPickerView?, value: Int) {
|
|
62
|
+
view?.setVisibleItemCountProp(value)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun setTextColor(view: DrumPickerView?, value: Int?) {
|
|
66
|
+
view?.setTextColorProp(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun setSelectedTextColor(view: DrumPickerView?, value: Int?) {
|
|
70
|
+
view?.setSelectedTextColorProp(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun setTextSize(view: DrumPickerView?, value: Float) {
|
|
74
|
+
view?.setTextSizeProp(value)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun setSelectedTextSize(view: DrumPickerView?, value: Float) {
|
|
78
|
+
view?.setSelectedTextSizeProp(value)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override fun setShowSelectionIndicator(view: DrumPickerView?, value: Boolean) {
|
|
82
|
+
view?.setShowSelectionIndicatorProp(value)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
override fun setSelectionIndicatorColor(view: DrumPickerView?, value: Int?) {
|
|
86
|
+
view?.setSelectionIndicatorColorProp(value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun setSelectionIndicatorHeight(view: DrumPickerView?, value: Float) {
|
|
90
|
+
view?.setSelectionIndicatorHeightProp(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun setBackgroundColor(view: DrumPickerView?, value: Int?) {
|
|
94
|
+
view?.setBackgroundColorProp(value)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun setContainerBackgroundColor(view: DrumPickerView?, value: Int?) {
|
|
98
|
+
view?.setContainerBackgroundColorProp(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun setItemBackgroundColor(view: DrumPickerView?, value: Int?) {
|
|
102
|
+
view?.setItemBackgroundColorProp(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
companion object {
|
|
106
|
+
const val NAME = "DrumPickerView"
|
|
107
|
+
}
|
|
108
|
+
}
|