react-native-drum-picker 0.1.4 → 0.1.5
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 +21 -1
- package/README.md +85 -12
- package/android/src/main/java/com/drumpicker/DrumPickerAdapter.kt +1 -0
- package/android/src/main/java/com/drumpicker/DrumPickerDefaults.kt +1 -0
- package/android/src/main/java/com/drumpicker/DrumPickerView.kt +92 -16
- package/lib/module/DateDrumPicker.js +15 -1
- package/lib/module/DrumPicker.native.js +2 -5
- package/lib/module/drumPickerLayout.js +29 -0
- package/lib/typescript/src/drumPickerLayout.d.ts +7 -0
- package/package.json +1 -1
- package/src/DateDrumPicker.tsx +19 -1
- package/src/DrumPicker.native.tsx +6 -8
- package/src/drumPickerLayout.ts +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
_No changes yet._
|
|
11
11
|
|
|
12
|
+
## [0.1.5] - 2026-05-22
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Center `selectedIndex` reliably using `scrollToPositionWithOffset` with a computed center offset (after items, layout, and metric changes).
|
|
17
|
+
- Cancel pending scroll work and `stopScroll()` on detach; broader lifecycle guards for navigation transitions.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- JS `DrumPicker` applies default `minWidth` / `height` (or `minHeight` with flex) so the picker is visible without manual sizing.
|
|
22
|
+
- Android native `minimumWidth` / `minimumHeight` aligned with `itemHeight * visibleItemCount`.
|
|
23
|
+
- One-time `__DEV__` warning when layout dimensions are missing.
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
- README: tested compatibility matrix, practical examples, `onChange` / debounce guidance.
|
|
28
|
+
- Example app: basic, time, height/weight, date, controlled, debounced demos.
|
|
29
|
+
- Bug report template: Expo, react-native-screens, navigation crash, empty picker.
|
|
30
|
+
|
|
12
31
|
## [0.1.4] - 2026-05-22
|
|
13
32
|
|
|
14
33
|
### Fixed
|
|
@@ -63,7 +82,8 @@ First release on [npm](https://www.npmjs.com/package/react-native-drum-picker).
|
|
|
63
82
|
|
|
64
83
|
Initial GitHub release. See [v0.0.1](https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.0.1).
|
|
65
84
|
|
|
66
|
-
[Unreleased]: https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.1.
|
|
85
|
+
[Unreleased]: https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.1.5...HEAD
|
|
86
|
+
[0.1.5]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.5
|
|
67
87
|
[0.1.4]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.4
|
|
68
88
|
[0.1.2]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.2
|
|
69
89
|
[0.1.1]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.1
|
package/README.md
CHANGED
|
@@ -67,22 +67,27 @@ Requires **React Native 0.76+** with the **New Architecture** enabled.
|
|
|
67
67
|
| React Native New Architecture | **Required** |
|
|
68
68
|
| Fabric | **Required** |
|
|
69
69
|
| Android | Supported |
|
|
70
|
-
| iOS | Not supported yet |
|
|
71
|
-
| Expo
|
|
72
|
-
|
|
|
73
|
-
|
|
|
70
|
+
| iOS | **Not supported yet** |
|
|
71
|
+
| Expo Go | **Not supported** (native Android library) |
|
|
72
|
+
| Expo SDK 54 + dev build / prebuild | Tested |
|
|
73
|
+
| `react-native-screens` navigation | Tested (use **0.1.4+** for detach safety) |
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
### Tested with
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
| Tool | Version |
|
|
78
|
+
|------|---------|
|
|
79
|
+
| Expo SDK | 54 |
|
|
80
|
+
| React Native | 0.81.5, 0.85.0 (example app) |
|
|
81
|
+
| New Architecture | enabled |
|
|
82
|
+
| Android | emulator / device |
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
**Intended range:** `react-native >= 0.76` with New Architecture. The package is **actively tested on RN 0.81.x / 0.85.x**. Older 0.76–0.80 may work but are not CI-guaranteed.
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
This package is an **Android Fabric View** library. Use a **development build** or `expo run:android` after `expo prebuild` — not Expo Go.
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
### React Native 0.81+ event dispatch
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
If Android Kotlin compile fails with `No value passed for parameter 'uiManagerType'`, upgrade to **0.1.3+** (Fabric `UIManagerType.FABRIC`).
|
|
86
91
|
|
|
87
92
|
## Basic usage
|
|
88
93
|
|
|
@@ -105,7 +110,73 @@ export function Example() {
|
|
|
105
110
|
}
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
|
|
113
|
+
### Layout defaults (0.1.5+)
|
|
114
|
+
|
|
115
|
+
The JS wrapper applies **`minWidth: 64`** and, unless you use flex or pass `height` / `minHeight`, **`height: itemHeight * visibleItemCount`** (default **220**). Native Android also sets matching `minimumWidth` / `minimumHeight`.
|
|
116
|
+
|
|
117
|
+
For production layouts, still pass explicit dimensions:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
style={{ width: 120, height: itemHeight * visibleItemCount }}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
In `__DEV__`, a **one-time** warning is logged if neither height nor flex sizing is provided.
|
|
124
|
+
|
|
125
|
+
## Examples
|
|
126
|
+
|
|
127
|
+
### Time picker (hour · minute)
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
|
|
131
|
+
const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
|
|
132
|
+
|
|
133
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
134
|
+
<DrumPicker items={hours} style={{ width: 72, height: 220 }} />
|
|
135
|
+
<DrumPicker items={minutes} style={{ width: 72, height: 220 }} />
|
|
136
|
+
</View>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Height / weight row (onboarding style)
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
143
|
+
<DrumPicker items={heightsCm} style={{ width: 90, height: 220 }} />
|
|
144
|
+
<DrumPicker items={weightsKg} style={{ width: 96, height: 220 }} />
|
|
145
|
+
</View>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Controlled `selectedIndex`
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
const [index, setIndex] = useState(1);
|
|
152
|
+
|
|
153
|
+
<DrumPicker
|
|
154
|
+
items={items}
|
|
155
|
+
selectedIndex={index}
|
|
156
|
+
onChange={(e) => setIndex(e.nativeEvent.index)}
|
|
157
|
+
style={{ width: 120, height: 220 }}
|
|
158
|
+
/>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `onChange` and expensive side effects
|
|
162
|
+
|
|
163
|
+
Native emits `onChange` when the wheel **snaps to idle** and the **centered index changes** (duplicate indices are ignored). Use it for UI state. For AsyncStorage, APIs, or analytics, **debounce** in your app:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const save = useMemo(() => {
|
|
167
|
+
let t: ReturnType<typeof setTimeout> | undefined;
|
|
168
|
+
return (value: string) => {
|
|
169
|
+
if (t) clearTimeout(t);
|
|
170
|
+
t = setTimeout(() => {
|
|
171
|
+
// persist value
|
|
172
|
+
}, 300);
|
|
173
|
+
};
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
<DrumPicker onChange={(e) => save(e.nativeEvent.value)} ... />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
See the **example** app (`example/src/App.tsx`) for basic, time, height/weight, date, controlled, and debounced demos.
|
|
109
180
|
|
|
110
181
|
## DateDrumPicker
|
|
111
182
|
|
|
@@ -246,12 +317,14 @@ Use an **odd** `visibleItemCount` (e.g. `5`) for a symmetric wheel.
|
|
|
246
317
|
| Metro shows old code | `npx react-native start --reset-cache` |
|
|
247
318
|
| Gradle / build errors | `cd android && ./gradlew clean` (Windows: `.\gradlew clean`) |
|
|
248
319
|
| `adb` not found | Install Android SDK Platform-Tools; add to `PATH` |
|
|
249
|
-
| Empty or white picker | Enable New Architecture; set
|
|
320
|
+
| Empty or white picker | Enable New Architecture; upgrade to **0.1.5+** for layout defaults; or set `style={{ width, height: itemHeight * visibleItemCount }}` |
|
|
321
|
+
| Wrong initial row / off-center | Upgrade to **0.1.5+**; avoid `key` remount hacks unless needed for other reasons |
|
|
250
322
|
| Props not applied | Rebuild app after native changes; run `yarn build` in the library before packing |
|
|
251
323
|
| iOS | Not supported — Android only |
|
|
252
324
|
| Expo Go | Use prebuild + dev build; this library is not in Expo Go |
|
|
253
325
|
| RN 0.81 `uiManagerType` compile error | Upgrade to 0.1.3+ |
|
|
254
326
|
| Crash when leaving a screen | Upgrade to 0.1.4+ (safe `onDetachedFromWindow` with react-native-screens) |
|
|
327
|
+
| npm shows “no README” | Often a registry UI lag; run `npm view react-native-drum-picker readme` — if content appears, hard-refresh the package page |
|
|
255
328
|
|
|
256
329
|
### Android build fails in Expo / React Native 0.81+
|
|
257
330
|
|
|
@@ -76,6 +76,7 @@ internal class DrumPickerAdapter(
|
|
|
76
76
|
textView.layoutParams =
|
|
77
77
|
(textView.layoutParams as RecyclerView.LayoutParams).apply { this.height = height }
|
|
78
78
|
textView.text = items[position]
|
|
79
|
+
textView.contentDescription = items[position]
|
|
79
80
|
textView.setBackgroundColor(itemBackgroundColor)
|
|
80
81
|
holder.lastStyleBucket = Int.MIN_VALUE
|
|
81
82
|
val distance = distanceForPosition?.invoke(position) ?: 2f
|
|
@@ -8,6 +8,7 @@ import android.graphics.Color
|
|
|
8
8
|
internal object DrumPickerDefaults {
|
|
9
9
|
const val ITEM_HEIGHT_DP = 44f
|
|
10
10
|
const val VISIBLE_ITEM_COUNT = 5
|
|
11
|
+
const val MIN_WIDTH_DP = 64f
|
|
11
12
|
const val TEXT_SIZE_SP = 20f
|
|
12
13
|
const val SELECTED_TEXT_SIZE_SP = 22f
|
|
13
14
|
const val SELECTION_INDICATOR_HEIGHT_DP = 1f
|
|
@@ -51,6 +51,8 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
51
51
|
private var isAttachedToWindow = false
|
|
52
52
|
private var isDisposed = false
|
|
53
53
|
private var styleUpdatePosted = false
|
|
54
|
+
private var pendingScrollRunnable: Runnable? = null
|
|
55
|
+
private val minWidthPx = dpToPx(DrumPickerDefaults.MIN_WIDTH_DP)
|
|
54
56
|
|
|
55
57
|
private val styleUpdateRunnable =
|
|
56
58
|
Runnable {
|
|
@@ -120,11 +122,12 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
120
122
|
applyBackgroundColors()
|
|
121
123
|
applyRecyclerPadding()
|
|
122
124
|
updateIndicatorAppearance()
|
|
125
|
+
updateMinimumDimensions()
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
fun setItemsProp(value: Any?) {
|
|
126
129
|
val newItems = parseItems(value)
|
|
127
|
-
if (newItems
|
|
130
|
+
if (itemsContentEquals(newItems, items)) {
|
|
128
131
|
return
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -139,7 +142,7 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
selectedIndex = selectedIndex.coerceIn(0, items.size - 1)
|
|
142
|
-
runWhenAttached {
|
|
145
|
+
runWhenAttached { scheduleScrollToSelectedIndexCentered(animated = false, emit = false) }
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
fun setSelectedIndexProp(value: Any?) {
|
|
@@ -215,13 +218,11 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
218
|
-
|
|
221
|
+
updateMinimumDimensions()
|
|
222
|
+
val width = resolveSize(minWidthPx, widthMeasureSpec)
|
|
219
223
|
val safeVisibleCount = visibleItemCount.coerceAtLeast(1)
|
|
220
|
-
val
|
|
221
|
-
|
|
222
|
-
(itemHeightPx * safeVisibleCount).coerceAtLeast(suggestedMinimumHeight),
|
|
223
|
-
heightMeasureSpec,
|
|
224
|
-
)
|
|
224
|
+
val defaultHeight = itemHeightPx * safeVisibleCount
|
|
225
|
+
val height = resolveSize(defaultHeight, heightMeasureSpec)
|
|
225
226
|
|
|
226
227
|
val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
|
|
227
228
|
val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
|
@@ -237,7 +238,7 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
237
238
|
layoutSelectionIndicators(width, height)
|
|
238
239
|
if (changed) {
|
|
239
240
|
applyRecyclerPadding()
|
|
240
|
-
runWhenAttached {
|
|
241
|
+
runWhenAttached { scheduleScrollToSelectedIndexCentered(animated = false, emit = false) }
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
|
|
@@ -245,6 +246,7 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
245
246
|
super.onAttachedToWindow()
|
|
246
247
|
isAttachedToWindow = true
|
|
247
248
|
isDisposed = false
|
|
249
|
+
suppressChangeEvent = false
|
|
248
250
|
recyclerView.removeOnScrollListener(scrollListener)
|
|
249
251
|
recyclerView.addOnScrollListener(scrollListener)
|
|
250
252
|
}
|
|
@@ -252,7 +254,10 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
252
254
|
override fun onDetachedFromWindow() {
|
|
253
255
|
isAttachedToWindow = false
|
|
254
256
|
isDisposed = true
|
|
257
|
+
suppressChangeEvent = false
|
|
255
258
|
styleUpdatePosted = false
|
|
259
|
+
cancelPendingScroll()
|
|
260
|
+
recyclerView.stopScroll()
|
|
256
261
|
recyclerView.removeCallbacks(styleUpdateRunnable)
|
|
257
262
|
removeCallbacks(null)
|
|
258
263
|
recyclerView.removeCallbacks(null)
|
|
@@ -271,11 +276,15 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
271
276
|
}
|
|
272
277
|
val clamped = index.coerceIn(0, items.size - 1)
|
|
273
278
|
val snapIndex = findSnapCenterIndex()
|
|
274
|
-
|
|
279
|
+
val isAlreadyCentered =
|
|
280
|
+
clamped == selectedIndex &&
|
|
281
|
+
snapIndex == clamped &&
|
|
282
|
+
recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE
|
|
283
|
+
if (isAlreadyCentered) {
|
|
275
284
|
return
|
|
276
285
|
}
|
|
277
286
|
selectedIndex = clamped
|
|
278
|
-
|
|
287
|
+
scheduleScrollToSelectedIndexCentered(animated = false, emit = false)
|
|
279
288
|
}
|
|
280
289
|
|
|
281
290
|
fun setItemHeight(height: Float) {
|
|
@@ -287,6 +296,7 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
287
296
|
adapter.itemHeightPx = itemHeightPx
|
|
288
297
|
adapter.notifyRowMetricsChanged()
|
|
289
298
|
applyRecyclerPadding()
|
|
299
|
+
updateMinimumDimensions()
|
|
290
300
|
requestLayout()
|
|
291
301
|
}
|
|
292
302
|
|
|
@@ -296,9 +306,16 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
296
306
|
}
|
|
297
307
|
visibleItemCount = count
|
|
298
308
|
applyRecyclerPadding()
|
|
309
|
+
updateMinimumDimensions()
|
|
299
310
|
requestLayout()
|
|
300
311
|
}
|
|
301
312
|
|
|
313
|
+
private fun updateMinimumDimensions() {
|
|
314
|
+
val safeVisibleCount = visibleItemCount.coerceAtLeast(1)
|
|
315
|
+
minimumHeight = itemHeightPx * safeVisibleCount
|
|
316
|
+
minimumWidth = minWidthPx
|
|
317
|
+
}
|
|
318
|
+
|
|
302
319
|
fun setTextColor(color: Int) {
|
|
303
320
|
textColor = color
|
|
304
321
|
syncAdapterStyle()
|
|
@@ -391,7 +408,36 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
391
408
|
bottomIndicator.requestLayout()
|
|
392
409
|
}
|
|
393
410
|
|
|
394
|
-
private fun
|
|
411
|
+
private fun scheduleScrollToSelectedIndexCentered(animated: Boolean, emit: Boolean) {
|
|
412
|
+
if (!isLifecycleActive() || items.isEmpty()) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
cancelPendingScroll()
|
|
416
|
+
val runnable =
|
|
417
|
+
Runnable {
|
|
418
|
+
pendingScrollRunnable = null
|
|
419
|
+
if (!isLifecycleActive()) {
|
|
420
|
+
return@Runnable
|
|
421
|
+
}
|
|
422
|
+
scrollToSelectedIndexCentered(animated, emit)
|
|
423
|
+
}
|
|
424
|
+
pendingScrollRunnable = runnable
|
|
425
|
+
if (recyclerView.height > 0) {
|
|
426
|
+
recyclerView.post(runnable)
|
|
427
|
+
} else {
|
|
428
|
+
post(runnable)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private fun cancelPendingScroll() {
|
|
433
|
+
pendingScrollRunnable?.let { pending ->
|
|
434
|
+
recyclerView.removeCallbacks(pending)
|
|
435
|
+
removeCallbacks(pending)
|
|
436
|
+
}
|
|
437
|
+
pendingScrollRunnable = null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private fun scrollToSelectedIndexCentered(animated: Boolean, emit: Boolean) {
|
|
395
441
|
if (!isLifecycleActive() || items.isEmpty()) {
|
|
396
442
|
return
|
|
397
443
|
}
|
|
@@ -402,16 +448,30 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
402
448
|
return
|
|
403
449
|
}
|
|
404
450
|
|
|
405
|
-
|
|
451
|
+
if (recyclerView.height <= 0) {
|
|
452
|
+
scheduleScrollToSelectedIndexCentered(animated = false, emit = emit)
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
val centerOffset = ((recyclerView.height - itemHeightPx) / 2).coerceAtLeast(0)
|
|
457
|
+
layoutManager.scrollToPositionWithOffset(index, centerOffset)
|
|
406
458
|
recyclerView.post {
|
|
407
459
|
if (!isLifecycleActive()) {
|
|
408
460
|
return@post
|
|
409
461
|
}
|
|
462
|
+
val snappedIndex = findSnapCenterIndex()
|
|
463
|
+
if (snappedIndex != RecyclerView.NO_POSITION && snappedIndex != index) {
|
|
464
|
+
layoutManager.scrollToPositionWithOffset(snappedIndex, centerOffset)
|
|
465
|
+
selectedIndex = snappedIndex
|
|
466
|
+
}
|
|
410
467
|
updateVisibleItemStyles()
|
|
468
|
+
val resolvedIndex =
|
|
469
|
+
if (snappedIndex != RecyclerView.NO_POSITION) snappedIndex else index
|
|
411
470
|
if (emit) {
|
|
412
|
-
maybeEmitChange(
|
|
471
|
+
maybeEmitChange(resolvedIndex)
|
|
413
472
|
} else {
|
|
414
|
-
lastEmittedIndex =
|
|
473
|
+
lastEmittedIndex = resolvedIndex
|
|
474
|
+
selectedIndex = resolvedIndex
|
|
415
475
|
}
|
|
416
476
|
suppressChangeEvent = false
|
|
417
477
|
}
|
|
@@ -506,7 +566,8 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
506
566
|
if (!reactContext.hasActiveReactInstance()) {
|
|
507
567
|
return
|
|
508
568
|
}
|
|
509
|
-
// Fabric-only: RN 0.81
|
|
569
|
+
// Fabric-only: pass UIManagerType.FABRIC for RN 0.81–0.84; deprecated but still required there.
|
|
570
|
+
@Suppress("DEPRECATION")
|
|
510
571
|
val dispatcher: EventDispatcher? =
|
|
511
572
|
UIManagerHelper.getEventDispatcher(reactContext, UIManagerType.FABRIC)
|
|
512
573
|
dispatcher?.dispatchEvent(
|
|
@@ -589,4 +650,19 @@ class DrumPickerView @JvmOverloads constructor(
|
|
|
589
650
|
|
|
590
651
|
private fun dpToPx(dp: Float): Int =
|
|
591
652
|
(dp * resources.displayMetrics.density + 0.5f).toInt()
|
|
653
|
+
|
|
654
|
+
private fun itemsContentEquals(a: List<String>, b: List<String>): Boolean {
|
|
655
|
+
if (a === b) {
|
|
656
|
+
return true
|
|
657
|
+
}
|
|
658
|
+
if (a.size != b.size) {
|
|
659
|
+
return false
|
|
660
|
+
}
|
|
661
|
+
for (i in a.indices) {
|
|
662
|
+
if (a[i] != b[i]) {
|
|
663
|
+
return false
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return true
|
|
667
|
+
}
|
|
592
668
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { StyleSheet, View } from 'react-native';
|
|
5
5
|
import { buildDayItems, buildMonthItems, buildYearItems, clampDateDrumPickerValue, clampDayForMonth, clampYear, normalizeYearRange, parseMonthFromLabel } from "./dateDrumPickerLogic.js";
|
|
6
6
|
import { DrumPicker } from './DrumPicker';
|
|
@@ -75,6 +75,20 @@ export function DateDrumPicker({
|
|
|
75
75
|
}
|
|
76
76
|
onChange?.(next);
|
|
77
77
|
}, [isControlled, onChange, resolvedValue, minYear, maxYear]);
|
|
78
|
+
|
|
79
|
+
// Controlled: parent may pass day 31 + April — clamp and notify once.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isControlled || !onChange || value === undefined) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const clamped = clampDateDrumPickerValue(value, minYear, maxYear);
|
|
85
|
+
const day = value.day ?? clamped.day;
|
|
86
|
+
const month = value.month ?? clamped.month;
|
|
87
|
+
const year = value.year ?? clamped.year;
|
|
88
|
+
if (day !== clamped.day || month !== clamped.month || year !== clamped.year) {
|
|
89
|
+
onChange(clamped);
|
|
90
|
+
}
|
|
91
|
+
}, [isControlled, onChange, value, minYear, maxYear]);
|
|
78
92
|
const sharedPickerProps = {
|
|
79
93
|
itemHeight,
|
|
80
94
|
visibleItemCount,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { StyleSheet } from 'react-native';
|
|
4
3
|
import DrumPickerNative from './DrumPickerViewNativeComponent';
|
|
4
|
+
import { resolveDrumPickerStyle } from "./drumPickerLayout.js";
|
|
5
5
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
6
|
const DEFAULTS = {
|
|
7
7
|
selectedIndex: 0,
|
|
@@ -36,10 +36,7 @@ export function DrumPicker({
|
|
|
36
36
|
onChange,
|
|
37
37
|
style
|
|
38
38
|
}) {
|
|
39
|
-
const
|
|
40
|
-
const pickerStyle = StyleSheet.flatten([{
|
|
41
|
-
height: pickerHeight
|
|
42
|
-
}, style]);
|
|
39
|
+
const pickerStyle = resolveDrumPickerStyle(itemHeight, visibleItemCount, style);
|
|
43
40
|
return /*#__PURE__*/_jsx(DrumPickerNative, {
|
|
44
41
|
collapsable: false,
|
|
45
42
|
items: items,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { StyleSheet } from 'react-native';
|
|
4
|
+
let didWarnMissingLayout = false;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merges consumer style with sensible defaults so the native picker is visible
|
|
8
|
+
* without forcing dimensions when flex layout is used.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDrumPickerStyle(itemHeight, visibleItemCount, style) {
|
|
11
|
+
const flat = StyleSheet.flatten(style) ?? {};
|
|
12
|
+
const pickerHeight = itemHeight * visibleItemCount;
|
|
13
|
+
const hasExplicitHeight = flat.height != null || flat.minHeight != null || flat.maxHeight != null;
|
|
14
|
+
const usesFlex = flat.flex != null || flat.flexGrow != null || flat.flexShrink != null || flat.alignSelf === 'stretch';
|
|
15
|
+
if (__DEV__ && !didWarnMissingLayout && !hasExplicitHeight && !usesFlex) {
|
|
16
|
+
didWarnMissingLayout = true;
|
|
17
|
+
console.warn('react-native-drum-picker: DrumPicker needs a visible height. ' + `Defaulting to itemHeight * visibleItemCount (${pickerHeight}). ` + 'Pass style={{ height: ... }} or style={{ minHeight: ... }} for precise control.');
|
|
18
|
+
}
|
|
19
|
+
const sizeDefaults = hasExplicitHeight || usesFlex ? {
|
|
20
|
+
minWidth: 64,
|
|
21
|
+
minHeight: pickerHeight
|
|
22
|
+
} : {
|
|
23
|
+
minWidth: 64,
|
|
24
|
+
minHeight: pickerHeight,
|
|
25
|
+
height: pickerHeight
|
|
26
|
+
};
|
|
27
|
+
return StyleSheet.flatten([sizeDefaults, style]);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=drumPickerLayout.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
/**
|
|
3
|
+
* Merges consumer style with sensible defaults so the native picker is visible
|
|
4
|
+
* without forcing dimensions when flex layout is used.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveDrumPickerStyle(itemHeight: number, visibleItemCount: number, style?: StyleProp<ViewStyle>): ViewStyle;
|
|
7
|
+
//# sourceMappingURL=drumPickerLayout.d.ts.map
|
package/package.json
CHANGED
package/src/DateDrumPicker.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
StyleSheet,
|
|
4
4
|
View,
|
|
@@ -164,6 +164,24 @@ export function DateDrumPicker({
|
|
|
164
164
|
[isControlled, onChange, resolvedValue, minYear, maxYear]
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
+
// Controlled: parent may pass day 31 + April — clamp and notify once.
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (!isControlled || !onChange || value === undefined) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const clamped = clampDateDrumPickerValue(value, minYear, maxYear);
|
|
173
|
+
const day = value.day ?? clamped.day;
|
|
174
|
+
const month = value.month ?? clamped.month;
|
|
175
|
+
const year = value.year ?? clamped.year;
|
|
176
|
+
if (
|
|
177
|
+
day !== clamped.day ||
|
|
178
|
+
month !== clamped.month ||
|
|
179
|
+
year !== clamped.year
|
|
180
|
+
) {
|
|
181
|
+
onChange(clamped);
|
|
182
|
+
}
|
|
183
|
+
}, [isControlled, onChange, value, minYear, maxYear]);
|
|
184
|
+
|
|
167
185
|
const sharedPickerProps = {
|
|
168
186
|
itemHeight,
|
|
169
187
|
visibleItemCount,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { StyleSheet } from 'react-native';
|
|
2
1
|
import DrumPickerNative from './DrumPickerViewNativeComponent';
|
|
2
|
+
import { resolveDrumPickerStyle } from './drumPickerLayout';
|
|
3
3
|
import type { DrumPickerProps } from './types';
|
|
4
4
|
|
|
5
5
|
const DEFAULTS = {
|
|
@@ -36,13 +36,11 @@ export function DrumPicker({
|
|
|
36
36
|
onChange,
|
|
37
37
|
style,
|
|
38
38
|
}: DrumPickerProps) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
style,
|
|
45
|
-
]);
|
|
39
|
+
const pickerStyle = resolveDrumPickerStyle(
|
|
40
|
+
itemHeight,
|
|
41
|
+
visibleItemCount,
|
|
42
|
+
style
|
|
43
|
+
);
|
|
46
44
|
|
|
47
45
|
return (
|
|
48
46
|
<DrumPickerNative
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
let didWarnMissingLayout = false;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merges consumer style with sensible defaults so the native picker is visible
|
|
7
|
+
* without forcing dimensions when flex layout is used.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveDrumPickerStyle(
|
|
10
|
+
itemHeight: number,
|
|
11
|
+
visibleItemCount: number,
|
|
12
|
+
style?: StyleProp<ViewStyle>
|
|
13
|
+
): ViewStyle {
|
|
14
|
+
const flat = StyleSheet.flatten(style) ?? {};
|
|
15
|
+
const pickerHeight = itemHeight * visibleItemCount;
|
|
16
|
+
const hasExplicitHeight =
|
|
17
|
+
flat.height != null || flat.minHeight != null || flat.maxHeight != null;
|
|
18
|
+
const usesFlex =
|
|
19
|
+
flat.flex != null ||
|
|
20
|
+
flat.flexGrow != null ||
|
|
21
|
+
flat.flexShrink != null ||
|
|
22
|
+
flat.alignSelf === 'stretch';
|
|
23
|
+
|
|
24
|
+
if (__DEV__ && !didWarnMissingLayout && !hasExplicitHeight && !usesFlex) {
|
|
25
|
+
didWarnMissingLayout = true;
|
|
26
|
+
console.warn(
|
|
27
|
+
'react-native-drum-picker: DrumPicker needs a visible height. ' +
|
|
28
|
+
`Defaulting to itemHeight * visibleItemCount (${pickerHeight}). ` +
|
|
29
|
+
'Pass style={{ height: ... }} or style={{ minHeight: ... }} for precise control.'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sizeDefaults: ViewStyle =
|
|
34
|
+
hasExplicitHeight || usesFlex
|
|
35
|
+
? { minWidth: 64, minHeight: pickerHeight }
|
|
36
|
+
: { minWidth: 64, minHeight: pickerHeight, height: pickerHeight };
|
|
37
|
+
|
|
38
|
+
return StyleSheet.flatten([sizeDefaults, style]) as ViewStyle;
|
|
39
|
+
}
|