@onekeyfe/react-native-pager-view 6.7.0-alpha.354 → 6.7.0-alpha.359
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/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +111 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +4 -0
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +72 -7
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +14 -0
- package/docs/SPEC.md +34 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +56 -5
- package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +91 -0
- package/ios/tests/RNCCollapsiblePagerPressCancellationTests.mm +65 -0
- package/lib/module/CollapsiblePagerView.js +4 -0
- package/lib/module/CollapsiblePagerViewNativeComponent.ts +2 -0
- package/lib/typescript/src/CollapsiblePagerView.d.ts +3 -1
- package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +2 -0
- package/package.json +2 -2
- package/src/CollapsiblePagerView.tsx +18 -0
- package/src/CollapsiblePagerViewNativeComponent.ts +2 -0
package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt
CHANGED
|
@@ -20,6 +20,117 @@ import kotlin.math.roundToInt
|
|
|
20
20
|
class CollapsiblePagerHitTestActivity : Activity()
|
|
21
21
|
|
|
22
22
|
class CollapsiblePagerTabReservationTest : InstrumentationTestCase() {
|
|
23
|
+
fun testSegmentedNativeTabsTrackFractionalProgressAndReset() {
|
|
24
|
+
val context = instrumentation.targetContext
|
|
25
|
+
assertTrue(context.applicationInfo.flags and ApplicationInfo.FLAG_SUPPORTS_RTL != 0)
|
|
26
|
+
val activity = instrumentation.startActivitySync(
|
|
27
|
+
Intent(context, CollapsiblePagerHitTestActivity::class.java)
|
|
28
|
+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
29
|
+
) as CollapsiblePagerHitTestActivity
|
|
30
|
+
val density = context.resources.displayMetrics.density
|
|
31
|
+
fun dp(value: Int) = (value * density).roundToInt()
|
|
32
|
+
lateinit var tab: CollapsiblePagerNativeTabBarView
|
|
33
|
+
lateinit var row: HorizontalScrollView
|
|
34
|
+
lateinit var content: ViewGroup
|
|
35
|
+
lateinit var track: View
|
|
36
|
+
lateinit var thumb: View
|
|
37
|
+
lateinit var buttons: List<TextView>
|
|
38
|
+
fun layout(width: Int = 320, height: Int = 56) {
|
|
39
|
+
tab.measure(View.MeasureSpec.makeMeasureSpec(dp(width), View.MeasureSpec.EXACTLY),
|
|
40
|
+
View.MeasureSpec.makeMeasureSpec(dp(height), View.MeasureSpec.EXACTLY))
|
|
41
|
+
tab.layout(0, 0, dp(width), dp(height))
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
instrumentation.runOnMainSync {
|
|
45
|
+
tab = CollapsiblePagerNativeTabBarView(context)
|
|
46
|
+
activity.setContentView(tab)
|
|
47
|
+
val presses = mutableListOf<String>()
|
|
48
|
+
tab.onItemPress = { _, key -> presses.add(key) }
|
|
49
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents"},""" +
|
|
50
|
+
"""{"key":"account","title":"Accounts"},""" +
|
|
51
|
+
"""{"key":"book","title":"Address Book"}]""")
|
|
52
|
+
tab.updateStyle(56.0, "segmented", 20.0, 12.0, 0.0, 14.0, null, 2.0, 0.0)
|
|
53
|
+
tab.updateColors(Color.LTGRAY, Color.BLACK, Color.GRAY, Color.WHITE)
|
|
54
|
+
layout()
|
|
55
|
+
row = tab.getChildAt(0) as HorizontalScrollView
|
|
56
|
+
content = row.getChildAt(0) as ViewGroup
|
|
57
|
+
track = content.getChildAt(0)
|
|
58
|
+
thumb = content.getChildAt(1)
|
|
59
|
+
buttons = (0 until content.childCount).map(content::getChildAt).filterIsInstance<TextView>()
|
|
60
|
+
assertEquals(3, buttons.size)
|
|
61
|
+
assertEquals(dp(20), track.left)
|
|
62
|
+
assertTrue(kotlin.math.abs(dp(32) - track.height) <= 1)
|
|
63
|
+
assertEquals(0, row.scrollX)
|
|
64
|
+
assertTrue(kotlin.math.abs(buttons[0].width - buttons[1].width) <= 1)
|
|
65
|
+
tab.setProgress(0f)
|
|
66
|
+
val firstX = thumb.left
|
|
67
|
+
tab.setProgress(1f)
|
|
68
|
+
val secondX = thumb.left
|
|
69
|
+
tab.setProgress(0.25f)
|
|
70
|
+
assertTrue(kotlin.math.abs(thumb.left - (firstX + (secondX - firstX) * 0.25f)) <= 1)
|
|
71
|
+
tab.setProgress(0.5f)
|
|
72
|
+
assertTrue(kotlin.math.abs(thumb.left - (firstX + secondX) / 2f) <= 1)
|
|
73
|
+
tab.setProgress(0f)
|
|
74
|
+
assertEquals(firstX, thumb.left)
|
|
75
|
+
buttons[1].performClick()
|
|
76
|
+
assertEquals(listOf("account"), presses)
|
|
77
|
+
|
|
78
|
+
tab.layoutDirection = View.LAYOUT_DIRECTION_RTL
|
|
79
|
+
}
|
|
80
|
+
instrumentation.waitForIdleSync()
|
|
81
|
+
instrumentation.runOnMainSync {
|
|
82
|
+
layout()
|
|
83
|
+
assertEquals(View.LAYOUT_DIRECTION_RTL, row.layoutDirection)
|
|
84
|
+
val rtlFirst = buttons[0].layoutParams as android.widget.FrameLayout.LayoutParams
|
|
85
|
+
val rtlSecond = buttons[1].layoutParams as android.widget.FrameLayout.LayoutParams
|
|
86
|
+
assertTrue(rtlFirst.leftMargin > rtlSecond.leftMargin)
|
|
87
|
+
val rtlGeometry = "rowDir=${row.layoutDirection} contentDir=${content.layoutDirection} " +
|
|
88
|
+
buttons.mapIndexed { index, button ->
|
|
89
|
+
val params = button.layoutParams as android.widget.FrameLayout.LayoutParams
|
|
90
|
+
"$index:${button.left}-${button.right} margin=${params.leftMargin}/${params.rightMargin} gravity=${params.gravity}"
|
|
91
|
+
}.joinToString(" ")
|
|
92
|
+
assertTrue(rtlGeometry, buttons[0].left >= buttons[1].right)
|
|
93
|
+
assertEquals(dp(20), track.left)
|
|
94
|
+
tab.setProgress(0f)
|
|
95
|
+
val rtlFirstX = thumb.left
|
|
96
|
+
tab.setProgress(1f)
|
|
97
|
+
val rtlSecondX = thumb.left
|
|
98
|
+
assertTrue(rtlFirstX > rtlSecondX)
|
|
99
|
+
tab.setProgress(0.5f)
|
|
100
|
+
assertTrue(kotlin.math.abs(thumb.left - (rtlFirstX + rtlSecondX) / 2f) <= 1)
|
|
101
|
+
tab.layoutDirection = View.LAYOUT_DIRECTION_LTR
|
|
102
|
+
}
|
|
103
|
+
instrumentation.waitForIdleSync()
|
|
104
|
+
instrumentation.runOnMainSync {
|
|
105
|
+
layout()
|
|
106
|
+
assertTrue(buttons[0].right <= buttons[1].left)
|
|
107
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents (8)"},""" +
|
|
108
|
+
"""{"key":"account","title":"Accounts"}]""")
|
|
109
|
+
layout()
|
|
110
|
+
val twoButtons = (0 until content.childCount).map(content::getChildAt).filterIsInstance<TextView>()
|
|
111
|
+
assertEquals(2, twoButtons.size)
|
|
112
|
+
assertTrue(kotlin.math.abs(twoButtons[0].width - track.width / 2) <= 1)
|
|
113
|
+
layout(width = 30)
|
|
114
|
+
assertEquals(dp(15), track.left)
|
|
115
|
+
assertEquals(0, track.width)
|
|
116
|
+
tab.updateStyle(56.0, "segmented", 20.0, 40.0, 0.0, 14.0, null, 2.0, 0.0)
|
|
117
|
+
layout(width = 30)
|
|
118
|
+
assertTrue(kotlin.math.abs(dp(28) - track.top) <= 1)
|
|
119
|
+
assertEquals(0, track.height)
|
|
120
|
+
assertEquals(View.GONE, thumb.visibility)
|
|
121
|
+
tab.updateItemsJSON("[]")
|
|
122
|
+
assertEquals(View.GONE, tab.visibility)
|
|
123
|
+
tab.updateItemsJSON("""[{"key":"recent","title":"Recents"}]""")
|
|
124
|
+
tab.updateStyle(44.0, "underline", 20.0, 0.0, 8.0, 16.0, null, 2.0, 0.0)
|
|
125
|
+
layout(height = 44)
|
|
126
|
+
assertEquals(View.GONE, track.visibility)
|
|
127
|
+
assertEquals(dp(2), thumb.height)
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
instrumentation.runOnMainSync { activity.finish() }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
23
134
|
fun testTabViewportToolbarHitAndResetInBothDirections() {
|
|
24
135
|
val context = instrumentation.targetContext
|
|
25
136
|
assertTrue(context.applicationInfo.flags and ApplicationInfo.FLAG_SUPPORTS_RTL != 0)
|
|
@@ -293,6 +293,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
293
293
|
var onNativeSubHeaderPress: ((Int, String) -> Unit)? = null
|
|
294
294
|
|
|
295
295
|
var nativeTabBarHeight = 44.0
|
|
296
|
+
var nativeTabBarVariant = "underline"
|
|
296
297
|
var nativeTabBarTrailingReservedWidth = 0.0
|
|
297
298
|
set(value) {
|
|
298
299
|
field = if (value.isFinite() && value >= 0.0) value else 0.0
|
|
@@ -308,6 +309,7 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
308
309
|
} else 0
|
|
309
310
|
}
|
|
310
311
|
var nativeTabBarContentPaddingHorizontal = 20.0
|
|
312
|
+
var nativeTabBarContentPaddingVertical = 0.0
|
|
311
313
|
var nativeTabBarItemSpacing = 8.0
|
|
312
314
|
var nativeTabBarFontSize = 16.0
|
|
313
315
|
var nativeTabBarFontFamily: String? = null
|
|
@@ -430,7 +432,9 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
|
|
|
430
432
|
fun applyNativeHeaderStyle() {
|
|
431
433
|
nativeTabBarView.updateStyle(
|
|
432
434
|
nativeTabBarHeight,
|
|
435
|
+
nativeTabBarVariant,
|
|
433
436
|
nativeTabBarContentPaddingHorizontal,
|
|
437
|
+
nativeTabBarContentPaddingVertical,
|
|
434
438
|
nativeTabBarItemSpacing,
|
|
435
439
|
nativeTabBarFontSize,
|
|
436
440
|
nativeTabBarFontFamily,
|
|
@@ -65,7 +65,11 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
65
65
|
) : HorizontalScrollView(context) {
|
|
66
66
|
private val density = resources.displayMetrics.density
|
|
67
67
|
private val content = FrameLayout(context)
|
|
68
|
+
private val track = View(context)
|
|
68
69
|
private val indicator = View(context)
|
|
70
|
+
private val trackBackground = GradientDrawable().apply {
|
|
71
|
+
shape = GradientDrawable.RECTANGLE
|
|
72
|
+
}
|
|
69
73
|
private val indicatorBackground = GradientDrawable().apply {
|
|
70
74
|
shape = GradientDrawable.RECTANGLE
|
|
71
75
|
}
|
|
@@ -75,6 +79,8 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
75
79
|
private var selectedKey = ""
|
|
76
80
|
private var rowHeightPx = dp(if (showsProgressIndicator) 44.0 else 42.0)
|
|
77
81
|
private var contentPaddingPx = dp(20.0)
|
|
82
|
+
private var contentPaddingVerticalPx = 0
|
|
83
|
+
private var segmented = false
|
|
78
84
|
private var itemSpacingPx = dp(8.0)
|
|
79
85
|
private var fontSize = if (showsProgressIndicator) 16.0 else 14.0
|
|
80
86
|
private var fontFamily: String? = null
|
|
@@ -96,7 +102,9 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
96
102
|
isFillViewport = true
|
|
97
103
|
overScrollMode = OVER_SCROLL_NEVER
|
|
98
104
|
clipToPadding = false
|
|
105
|
+
track.background = trackBackground
|
|
99
106
|
indicator.background = indicatorBackground
|
|
107
|
+
content.addView(track, FrameLayout.LayoutParams(0, 0))
|
|
100
108
|
content.addView(indicator, FrameLayout.LayoutParams(0, 0))
|
|
101
109
|
addView(
|
|
102
110
|
content,
|
|
@@ -149,17 +157,24 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
149
157
|
fontFamily: String?,
|
|
150
158
|
indicatorHeight: Double = 0.0,
|
|
151
159
|
indicatorBottom: Double = 0.0,
|
|
160
|
+
variant: String = "underline",
|
|
161
|
+
contentPaddingVertical: Double = 0.0,
|
|
152
162
|
) {
|
|
153
163
|
rowHeightPx = max(1, dp(rowHeight))
|
|
154
164
|
contentPaddingPx = max(0, dp(contentPadding))
|
|
165
|
+
contentPaddingVerticalPx = max(0, dp(contentPaddingVertical))
|
|
166
|
+
val wasSegmented = segmented
|
|
167
|
+
segmented = showsProgressIndicator && variant == "segmented"
|
|
155
168
|
itemSpacingPx = max(0, dp(itemSpacing))
|
|
156
169
|
this.fontSize = max(1.0, fontSize)
|
|
157
170
|
this.fontFamily = fontFamily
|
|
158
171
|
if (showsProgressIndicator) {
|
|
159
172
|
indicatorHeightPx = max(0, dp(indicatorHeight))
|
|
160
173
|
indicatorBottomPx = max(0, dp(indicatorBottom))
|
|
161
|
-
indicatorBackground.cornerRadius = indicatorHeightPx / 2f
|
|
174
|
+
indicatorBackground.cornerRadius = if (segmented) dp(14.0).toFloat() else indicatorHeightPx / 2f
|
|
162
175
|
}
|
|
176
|
+
track.visibility = if (segmented) VISIBLE else GONE
|
|
177
|
+
if (segmented && !wasSegmented) scrollTo(0, 0)
|
|
163
178
|
applyTypeface()
|
|
164
179
|
requestLayout()
|
|
165
180
|
}
|
|
@@ -171,8 +186,9 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
171
186
|
selectedBackgroundColor: Int,
|
|
172
187
|
indicatorColor: Int,
|
|
173
188
|
) {
|
|
174
|
-
setBackgroundColor(backgroundColor)
|
|
175
|
-
content.setBackgroundColor(backgroundColor)
|
|
189
|
+
setBackgroundColor(if (segmented) Color.TRANSPARENT else backgroundColor)
|
|
190
|
+
content.setBackgroundColor(if (segmented) Color.TRANSPARENT else backgroundColor)
|
|
191
|
+
trackBackground.setColor(backgroundColor)
|
|
176
192
|
this.activeTextColor = activeTextColor
|
|
177
193
|
this.inactiveTextColor = inactiveTextColor
|
|
178
194
|
this.selectedBackgroundColor = selectedBackgroundColor
|
|
@@ -213,6 +229,46 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
213
229
|
}
|
|
214
230
|
applyTypeface()
|
|
215
231
|
itemFrames.clear()
|
|
232
|
+
if (segmented) {
|
|
233
|
+
val actualPaddingHorizontal = min(contentPaddingPx, width / 2)
|
|
234
|
+
val actualPaddingVertical = min(contentPaddingVerticalPx, height / 2)
|
|
235
|
+
val trackLeft = actualPaddingHorizontal
|
|
236
|
+
val trackRight = max(trackLeft, width - contentPaddingPx)
|
|
237
|
+
val trackTop = actualPaddingVertical
|
|
238
|
+
val trackBottom = max(trackTop, height - contentPaddingVerticalPx)
|
|
239
|
+
val trackWidth = trackRight - trackLeft
|
|
240
|
+
val trackHeight = trackBottom - trackTop
|
|
241
|
+
trackBackground.cornerRadius = trackHeight / 2f
|
|
242
|
+
indicatorBackground.cornerRadius = max(0, trackHeight - dp(4.0)) / 2f
|
|
243
|
+
track.layoutParams = FrameLayout.LayoutParams(trackWidth, trackHeight).apply {
|
|
244
|
+
gravity = Gravity.TOP or Gravity.LEFT
|
|
245
|
+
leftMargin = trackLeft
|
|
246
|
+
topMargin = trackTop
|
|
247
|
+
}
|
|
248
|
+
buttons.forEachIndexed { index, button ->
|
|
249
|
+
val count = buttons.size
|
|
250
|
+
val first = trackLeft + trackWidth * index / count
|
|
251
|
+
val last = trackLeft + trackWidth * (index + 1) / count
|
|
252
|
+
val start = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) trackRight - (last - trackLeft) else first
|
|
253
|
+
val end = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) trackRight - (first - trackLeft) else last
|
|
254
|
+
val buttonWidth = max(0, end - start)
|
|
255
|
+
val thumbInset = dp(2.0)
|
|
256
|
+
itemFrames.add(intArrayOf(start, buttonWidth, start + thumbInset, max(0, buttonWidth - 2 * thumbInset)))
|
|
257
|
+
button.layoutParams = FrameLayout.LayoutParams(buttonWidth, trackHeight).apply {
|
|
258
|
+
gravity = Gravity.TOP or Gravity.LEFT
|
|
259
|
+
leftMargin = start
|
|
260
|
+
topMargin = trackTop
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
content.layoutParams = LayoutParams(width, height)
|
|
264
|
+
content.measure(
|
|
265
|
+
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
|
266
|
+
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
|
|
267
|
+
)
|
|
268
|
+
setMeasuredDimension(width, height)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
track.layoutParams = FrameLayout.LayoutParams(0, 0)
|
|
216
272
|
var x = contentPaddingPx
|
|
217
273
|
buttons.forEachIndexed { index, button ->
|
|
218
274
|
val textWidth = ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt()
|
|
@@ -260,6 +316,9 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
260
316
|
return super.onTouchEvent(event)
|
|
261
317
|
}
|
|
262
318
|
|
|
319
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean =
|
|
320
|
+
if (segmented) false else super.onInterceptTouchEvent(event)
|
|
321
|
+
|
|
263
322
|
private fun updatePresentation() {
|
|
264
323
|
if (buttons.isEmpty() || itemFrames.size != buttons.size) return
|
|
265
324
|
val selectedIndex: Int
|
|
@@ -275,14 +334,16 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
275
334
|
val interpolatedIndicatorWidth = from[3] + (to[3] - from[3]) * fraction
|
|
276
335
|
val indicatorX = interpolatedIndicatorX.roundToInt()
|
|
277
336
|
val indicatorWidth = interpolatedIndicatorWidth.roundToInt()
|
|
278
|
-
val
|
|
337
|
+
val actualPaddingVertical = min(contentPaddingVerticalPx, measuredHeight / 2)
|
|
338
|
+
val indicatorHeight = if (segmented) max(0, measuredHeight - 2 * actualPaddingVertical - dp(4.0)) else indicatorHeightPx
|
|
339
|
+
val indicatorTop = if (segmented) actualPaddingVertical + dp(2.0) else max(0, measuredHeight - indicatorBottomPx - indicatorHeightPx)
|
|
279
340
|
indicator.layout(
|
|
280
341
|
indicatorX,
|
|
281
342
|
indicatorTop,
|
|
282
343
|
indicatorX + indicatorWidth,
|
|
283
|
-
indicatorTop +
|
|
344
|
+
indicatorTop + indicatorHeight,
|
|
284
345
|
)
|
|
285
|
-
indicator.visibility = if (
|
|
346
|
+
indicator.visibility = if (indicatorHeight > 0) VISIBLE else GONE
|
|
286
347
|
scrollTargetCenterX = interpolatedIndicatorX + interpolatedIndicatorWidth / 2f
|
|
287
348
|
selectedIndex = clamped.roundToInt()
|
|
288
349
|
buttons.forEachIndexed { index, button ->
|
|
@@ -304,7 +365,7 @@ private class CollapsiblePagerHorizontalItemsView(
|
|
|
304
365
|
}
|
|
305
366
|
}
|
|
306
367
|
|
|
307
|
-
if (centerSelectionWhenIdle && !isTouchDragging && width > 0 && selectedIndex in itemFrames.indices) {
|
|
368
|
+
if (centerSelectionWhenIdle && !segmented && !isTouchDragging && width > 0 && selectedIndex in itemFrames.indices) {
|
|
308
369
|
val desired = (scrollTargetCenterX - width / 2f).roundToInt()
|
|
309
370
|
.coerceIn(0, max(0, content.measuredWidth - width))
|
|
310
371
|
scrollTo(desired, 0)
|
|
@@ -335,7 +396,9 @@ internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(
|
|
|
335
396
|
|
|
336
397
|
fun updateStyle(
|
|
337
398
|
height: Double,
|
|
399
|
+
variant: String,
|
|
338
400
|
contentPadding: Double,
|
|
401
|
+
contentPaddingVertical: Double,
|
|
339
402
|
itemSpacing: Double,
|
|
340
403
|
fontSize: Double,
|
|
341
404
|
fontFamily: String?,
|
|
@@ -351,6 +414,8 @@ internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(
|
|
|
351
414
|
fontFamily,
|
|
352
415
|
indicatorHeight,
|
|
353
416
|
indicatorBottom,
|
|
417
|
+
variant,
|
|
418
|
+
contentPaddingVertical,
|
|
354
419
|
)
|
|
355
420
|
requestLayout()
|
|
356
421
|
}
|
|
@@ -239,6 +239,13 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
239
239
|
view.applyNativeHeaderStyle()
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
@ReactProp(name = "nativeTabBarVariant")
|
|
243
|
+
override fun setNativeTabBarVariant(view: CollapsiblePagerHost?, value: String?) {
|
|
244
|
+
view ?: return
|
|
245
|
+
view.nativeTabBarVariant = if (value == "segmented") "segmented" else "underline"
|
|
246
|
+
view.applyNativeHeaderStyle()
|
|
247
|
+
}
|
|
248
|
+
|
|
242
249
|
@ReactProp(name = "nativeTabBarTrailingReservedWidth", defaultDouble = 0.0)
|
|
243
250
|
override fun setNativeTabBarTrailingReservedWidth(view: CollapsiblePagerHost?, value: Double) {
|
|
244
251
|
view?.nativeTabBarTrailingReservedWidth = value
|
|
@@ -254,6 +261,13 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
|
|
|
254
261
|
view.applyNativeHeaderStyle()
|
|
255
262
|
}
|
|
256
263
|
|
|
264
|
+
@ReactProp(name = "nativeTabBarContentPaddingVertical")
|
|
265
|
+
override fun setNativeTabBarContentPaddingVertical(view: CollapsiblePagerHost?, value: Double) {
|
|
266
|
+
view ?: return
|
|
267
|
+
view.nativeTabBarContentPaddingVertical = if (value.isFinite()) value.coerceAtLeast(0.0) else 0.0
|
|
268
|
+
view.applyNativeHeaderStyle()
|
|
269
|
+
}
|
|
270
|
+
|
|
257
271
|
@ReactProp(name = "nativeTabBarItemSpacing")
|
|
258
272
|
override fun setNativeTabBarItemSpacing(view: CollapsiblePagerHost?, value: Double) {
|
|
259
273
|
view ?: return
|
package/docs/SPEC.md
CHANGED
|
@@ -496,6 +496,40 @@ The attached UIKit regression intentionally constrains the real pager pan to two
|
|
|
496
496
|
required touches to prove the no-pan path; it is distinct from natural Home OS
|
|
497
497
|
input, coalesced-event delivery, and physical-device acceptance.
|
|
498
498
|
|
|
499
|
+
## Native segmented tab bar
|
|
500
|
+
|
|
501
|
+
Status: implemented locally. Focused production-class iOS XCTest and Android
|
|
502
|
+
instrumentation checks pass; consuming App interaction acceptance is separate.
|
|
503
|
+
`nativeTabBar.style.variant` accepts `"underline"` (the default) or
|
|
504
|
+
`"segmented"` on iOS and Android. The optional
|
|
505
|
+
`nativeTabBar.style.contentPaddingVertical` is a nonnegative logical inset,
|
|
506
|
+
defaulting to zero. Omitting both fields preserves the existing content-width,
|
|
507
|
+
horizontally scrollable underline bar. Web ignores `nativeTabBar` and retains
|
|
508
|
+
its caller-rendered sticky header.
|
|
509
|
+
|
|
510
|
+
In segmented mode, the existing `height` is the entire tab slot. The track
|
|
511
|
+
fills the space inside `contentPaddingHorizontal` and
|
|
512
|
+
`contentPaddingVertical`, uses `backgroundColor`, and has a fully rounded
|
|
513
|
+
outline. Its visible items divide the track into equal-width segments in their
|
|
514
|
+
current order, including after item insertion, removal, relabeling, or resize.
|
|
515
|
+
The thumb uses `indicatorColor`, is inset two logical points from its segment,
|
|
516
|
+
and is fully rounded. Segment titles use the existing font and active/inactive
|
|
517
|
+
text colors, remain single-line with truncation, and retain their existing
|
|
518
|
+
accessibility labels, test IDs, and native press event keys. The segmented bar
|
|
519
|
+
does not scroll horizontally; `itemSpacing`, `indicatorHeight`, and
|
|
520
|
+
`indicatorBottom` apply only to underline mode. The pager's native fractional
|
|
521
|
+
page progress moves the thumb continuously between equal-width segments and
|
|
522
|
+
interpolates text emphasis without per-frame JavaScript updates. It settles at
|
|
523
|
+
the actual selected page after a completed or cancelled gesture. RTL reverses
|
|
524
|
+
physical segment order without changing logical page keys or indices.
|
|
525
|
+
|
|
526
|
+
Switching variants or removing the native tab bar clears the previous visual
|
|
527
|
+
state. The existing native press command, page callbacks, retention, vertical
|
|
528
|
+
header geometry, and Web behavior remain unchanged. Acceptance requires
|
|
529
|
+
focused native geometry/progress/press checks on iOS and Android, including
|
|
530
|
+
two or three segments, dynamic labels/items, RTL, narrow widths, and variant
|
|
531
|
+
switching. Type forwarding alone does not prove rendered behavior.
|
|
532
|
+
|
|
499
533
|
## Native tab-press animation default
|
|
500
534
|
|
|
501
535
|
Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
|
|
@@ -369,7 +369,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
369
369
|
@property (nonatomic, copy) RNCCollapsiblePagerNativeTabPressHandler onTabPress;
|
|
370
370
|
- (void)updateItemsJSON:(NSString *)itemsJSON;
|
|
371
371
|
- (void)updateStyleWithHeight:(CGFloat)height
|
|
372
|
+
variant:(NSString *)variant
|
|
372
373
|
contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
|
|
374
|
+
contentPaddingVertical:(CGFloat)contentPaddingVertical
|
|
373
375
|
itemSpacing:(CGFloat)itemSpacing
|
|
374
376
|
fontSize:(CGFloat)fontSize
|
|
375
377
|
fontFamily:(NSString *)fontFamily
|
|
@@ -386,12 +388,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
386
388
|
|
|
387
389
|
@implementation RNCCollapsiblePagerNativeTabBarView {
|
|
388
390
|
RNCCollapsiblePagerHorizontalScrollView *_scrollView;
|
|
391
|
+
UIView *_trackView;
|
|
389
392
|
UIView *_indicatorView;
|
|
390
393
|
NSArray<NSDictionary *> *_items;
|
|
391
394
|
NSMutableArray<UIButton *> *_buttons;
|
|
392
395
|
NSMutableArray<NSValue *> *_indicatorFrames;
|
|
393
396
|
CGFloat _barHeight;
|
|
394
397
|
CGFloat _contentPaddingHorizontal;
|
|
398
|
+
CGFloat _contentPaddingVertical;
|
|
399
|
+
BOOL _segmented;
|
|
395
400
|
CGFloat _itemSpacing;
|
|
396
401
|
CGFloat _fontSize;
|
|
397
402
|
NSString *_fontFamily;
|
|
@@ -414,6 +419,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
414
419
|
_indicatorFrames = [NSMutableArray new];
|
|
415
420
|
_barHeight = 44;
|
|
416
421
|
_contentPaddingHorizontal = 20;
|
|
422
|
+
_contentPaddingVertical = 0;
|
|
423
|
+
_segmented = NO;
|
|
417
424
|
_itemSpacing = 8;
|
|
418
425
|
_fontSize = 16;
|
|
419
426
|
_activeTextColor = UIColor.labelColor;
|
|
@@ -435,6 +442,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
435
442
|
_scrollView.delegate = self;
|
|
436
443
|
[self addSubview:_scrollView];
|
|
437
444
|
|
|
445
|
+
_trackView = [UIView new];
|
|
446
|
+
_trackView.userInteractionEnabled = NO;
|
|
447
|
+
[_scrollView addSubview:_trackView];
|
|
448
|
+
|
|
438
449
|
_indicatorView = [UIView new];
|
|
439
450
|
_indicatorView.userInteractionEnabled = NO;
|
|
440
451
|
[_scrollView addSubview:_indicatorView];
|
|
@@ -489,7 +500,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
489
500
|
}
|
|
490
501
|
|
|
491
502
|
- (void)updateStyleWithHeight:(CGFloat)height
|
|
503
|
+
variant:(NSString *)variant
|
|
492
504
|
contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
|
|
505
|
+
contentPaddingVertical:(CGFloat)contentPaddingVertical
|
|
493
506
|
itemSpacing:(CGFloat)itemSpacing
|
|
494
507
|
fontSize:(CGFloat)fontSize
|
|
495
508
|
fontFamily:(NSString *)fontFamily
|
|
@@ -501,19 +514,26 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
501
514
|
indicatorBottom:(CGFloat)indicatorBottom
|
|
502
515
|
{
|
|
503
516
|
_barHeight = MAX(1, height);
|
|
517
|
+
BOOL wasSegmented = _segmented;
|
|
518
|
+
_segmented = [variant isEqualToString:@"segmented"];
|
|
504
519
|
_contentPaddingHorizontal = MAX(0, contentPaddingHorizontal);
|
|
520
|
+
_contentPaddingVertical = MAX(0, contentPaddingVertical);
|
|
505
521
|
_itemSpacing = MAX(0, itemSpacing);
|
|
506
522
|
_fontSize = MAX(1, fontSize);
|
|
507
523
|
_fontFamily = [fontFamily copy];
|
|
508
|
-
self.backgroundColor = backgroundColor ?: UIColor.clearColor;
|
|
524
|
+
self.backgroundColor = _segmented ? UIColor.clearColor : (backgroundColor ?: UIColor.clearColor);
|
|
509
525
|
_scrollView.backgroundColor = self.backgroundColor;
|
|
526
|
+
_scrollView.scrollEnabled = !_segmented;
|
|
527
|
+
if (_segmented && !wasSegmented) [_scrollView setContentOffset:CGPointZero animated:NO];
|
|
528
|
+
_trackView.hidden = !_segmented;
|
|
529
|
+
_trackView.backgroundColor = backgroundColor ?: UIColor.clearColor;
|
|
510
530
|
_activeTextColor = activeTextColor ?: UIColor.labelColor;
|
|
511
531
|
_inactiveTextColor = inactiveTextColor ?: UIColor.secondaryLabelColor;
|
|
512
532
|
_indicatorColor = indicatorColor ?: _activeTextColor;
|
|
513
533
|
_indicatorHeight = MAX(0, indicatorHeight);
|
|
514
534
|
_indicatorBottom = MAX(0, indicatorBottom);
|
|
515
535
|
_indicatorView.backgroundColor = _indicatorColor;
|
|
516
|
-
_indicatorView.layer.cornerRadius = _indicatorHeight / 2;
|
|
536
|
+
_indicatorView.layer.cornerRadius = _segmented ? 0 : _indicatorHeight / 2;
|
|
517
537
|
for (UIButton *button in _buttons) button.titleLabel.font = self.tabFont;
|
|
518
538
|
[self setNeedsLayout];
|
|
519
539
|
}
|
|
@@ -572,6 +592,33 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
572
592
|
{
|
|
573
593
|
[super layoutSubviews];
|
|
574
594
|
_scrollView.frame = self.bounds;
|
|
595
|
+
if (_segmented) {
|
|
596
|
+
CGFloat width = CGRectGetWidth(self.bounds);
|
|
597
|
+
CGFloat height = CGRectGetHeight(self.bounds);
|
|
598
|
+
CGFloat insetX = MIN(_contentPaddingHorizontal, width / 2);
|
|
599
|
+
CGFloat insetY = MIN(_contentPaddingVertical, height / 2);
|
|
600
|
+
CGRect trackFrame = CGRectMake(insetX, insetY,
|
|
601
|
+
MAX(0, width - 2 * insetX), MAX(0, height - 2 * insetY));
|
|
602
|
+
_trackView.frame = trackFrame;
|
|
603
|
+
_trackView.layer.cornerRadius = CGRectGetHeight(trackFrame) / 2;
|
|
604
|
+
_scrollView.contentSize = self.bounds.size;
|
|
605
|
+
[_indicatorFrames removeAllObjects];
|
|
606
|
+
NSUInteger count = _buttons.count;
|
|
607
|
+
[_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
|
|
608
|
+
CGFloat first = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * index / count;
|
|
609
|
+
CGFloat last = CGRectGetMinX(trackFrame) + CGRectGetWidth(trackFrame) * (index + 1) / count;
|
|
610
|
+
CGFloat x = self->_leftToRight ? first : CGRectGetMaxX(trackFrame) - (last - CGRectGetMinX(trackFrame));
|
|
611
|
+
CGFloat end = self->_leftToRight ? last : CGRectGetMaxX(trackFrame) - (first - CGRectGetMinX(trackFrame));
|
|
612
|
+
button.frame = CGRectMake(x, CGRectGetMinY(trackFrame), MAX(0, end - x), CGRectGetHeight(trackFrame));
|
|
613
|
+
CGRect thumbFrame = CGRectMake(x + 2, CGRectGetMinY(trackFrame) + 2,
|
|
614
|
+
MAX(0, CGRectGetWidth(button.frame) - 4), MAX(0, CGRectGetHeight(trackFrame) - 4));
|
|
615
|
+
[self->_indicatorFrames addObject:[NSValue valueWithCGRect:thumbFrame]];
|
|
616
|
+
}];
|
|
617
|
+
_indicatorView.layer.cornerRadius = MAX(0, CGRectGetHeight(trackFrame) - 4) / 2;
|
|
618
|
+
[self updatePresentation];
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
_trackView.frame = CGRectZero;
|
|
575
622
|
UIFont *font = self.tabFont;
|
|
576
623
|
NSMutableArray<NSNumber *> *widths = [NSMutableArray arrayWithCapacity:_buttons.count];
|
|
577
624
|
CGFloat itemsWidth = 0;
|
|
@@ -635,10 +682,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
635
682
|
CGRectGetMinX(fromFrame) + (CGRectGetMinX(toFrame) - CGRectGetMinX(fromFrame)) * fraction,
|
|
636
683
|
CGRectGetMinY(fromFrame),
|
|
637
684
|
CGRectGetWidth(fromFrame) + (CGRectGetWidth(toFrame) - CGRectGetWidth(fromFrame)) * fraction,
|
|
638
|
-
_indicatorHeight
|
|
685
|
+
_segmented ? CGRectGetHeight(fromFrame) : _indicatorHeight
|
|
639
686
|
);
|
|
640
687
|
_indicatorView.frame = indicatorFrame;
|
|
641
|
-
_indicatorView.hidden =
|
|
688
|
+
_indicatorView.hidden = CGRectGetHeight(indicatorFrame) <= 0;
|
|
642
689
|
|
|
643
690
|
NSInteger selectedIndex = (NSInteger)round(progress);
|
|
644
691
|
[_buttons enumerateObjectsUsingBlock:^(UIButton *button, NSUInteger index, BOOL *stop) {
|
|
@@ -655,7 +702,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
655
702
|
: UIAccessibilityTraitButton;
|
|
656
703
|
}];
|
|
657
704
|
|
|
658
|
-
if (_shouldCenterSelectedItem && !_scrollView.tracking && !_scrollView.dragging &&
|
|
705
|
+
if (_shouldCenterSelectedItem && !_segmented && !_scrollView.tracking && !_scrollView.dragging &&
|
|
659
706
|
!_scrollView.decelerating && CGRectGetWidth(_scrollView.bounds) > 0) {
|
|
660
707
|
CGFloat maximumOffset = MAX(0, _scrollView.contentSize.width - CGRectGetWidth(_scrollView.bounds));
|
|
661
708
|
CGFloat desiredOffset = RNCClamp(
|
|
@@ -3457,7 +3504,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3457
3504
|
}
|
|
3458
3505
|
if (_needsPropsReapply ||
|
|
3459
3506
|
oldViewProps.nativeTabBarHeight != newViewProps.nativeTabBarHeight ||
|
|
3507
|
+
oldViewProps.nativeTabBarVariant != newViewProps.nativeTabBarVariant ||
|
|
3460
3508
|
oldViewProps.nativeTabBarContentPaddingHorizontal != newViewProps.nativeTabBarContentPaddingHorizontal ||
|
|
3509
|
+
oldViewProps.nativeTabBarContentPaddingVertical != newViewProps.nativeTabBarContentPaddingVertical ||
|
|
3461
3510
|
oldViewProps.nativeTabBarItemSpacing != newViewProps.nativeTabBarItemSpacing ||
|
|
3462
3511
|
oldViewProps.nativeTabBarFontSize != newViewProps.nativeTabBarFontSize ||
|
|
3463
3512
|
oldViewProps.nativeTabBarFontFamily != newViewProps.nativeTabBarFontFamily ||
|
|
@@ -3474,7 +3523,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3474
3523
|
NSString *fontFamily = RCTNSStringFromString(newViewProps.nativeTabBarFontFamily);
|
|
3475
3524
|
[_nativeTabBarView
|
|
3476
3525
|
updateStyleWithHeight:_nativeTabBarHeight
|
|
3526
|
+
variant:RCTNSStringFromString(newViewProps.nativeTabBarVariant)
|
|
3477
3527
|
contentPaddingHorizontal:MAX(0, newViewProps.nativeTabBarContentPaddingHorizontal)
|
|
3528
|
+
contentPaddingVertical:MAX(0, newViewProps.nativeTabBarContentPaddingVertical)
|
|
3478
3529
|
itemSpacing:MAX(0, newViewProps.nativeTabBarItemSpacing)
|
|
3479
3530
|
fontSize:newViewProps.nativeTabBarFontSize > 0 ? newViewProps.nativeTabBarFontSize : 16
|
|
3480
3531
|
fontFamily:fontFamily
|
|
@@ -16,6 +16,19 @@ using namespace facebook::react;
|
|
|
16
16
|
|
|
17
17
|
@interface UIView (NativeTabReservationTesting)
|
|
18
18
|
- (void)updateItemsJSON:(NSString *)itemsJSON;
|
|
19
|
+
- (void)updateStyleWithHeight:(CGFloat)height
|
|
20
|
+
variant:(NSString *)variant
|
|
21
|
+
contentPaddingHorizontal:(CGFloat)contentPaddingHorizontal
|
|
22
|
+
contentPaddingVertical:(CGFloat)contentPaddingVertical
|
|
23
|
+
itemSpacing:(CGFloat)itemSpacing
|
|
24
|
+
fontSize:(CGFloat)fontSize
|
|
25
|
+
fontFamily:(NSString *)fontFamily
|
|
26
|
+
backgroundColor:(UIColor *)backgroundColor
|
|
27
|
+
activeTextColor:(UIColor *)activeTextColor
|
|
28
|
+
inactiveTextColor:(UIColor *)inactiveTextColor
|
|
29
|
+
indicatorColor:(UIColor *)indicatorColor
|
|
30
|
+
indicatorHeight:(CGFloat)indicatorHeight
|
|
31
|
+
indicatorBottom:(CGFloat)indicatorBottom;
|
|
19
32
|
- (void)setProgress:(CGFloat)progress;
|
|
20
33
|
- (void)setLeftToRight:(BOOL)leftToRight;
|
|
21
34
|
@end
|
|
@@ -143,6 +156,84 @@ using namespace facebook::react;
|
|
|
143
156
|
[[self.pager valueForKey:@"nativeTabBarTrailingReservedWidth"] doubleValue], 0, 0.001);
|
|
144
157
|
}
|
|
145
158
|
|
|
159
|
+
- (void)testSegmentedNativeTabsFollowFractionalProgressAndResetToUnderline
|
|
160
|
+
{
|
|
161
|
+
UIView *tabBar = [self.pager valueForKey:@"nativeTabBarView"];
|
|
162
|
+
[tabBar updateItemsJSON:@"[{\"key\":\"recent\",\"title\":\"Recents\"},"
|
|
163
|
+
@"{\"key\":\"account\",\"title\":\"Accounts\"},"
|
|
164
|
+
@"{\"key\":\"book\",\"title\":\"Address Book\"}]"];
|
|
165
|
+
[tabBar updateStyleWithHeight:56 variant:@"segmented"
|
|
166
|
+
contentPaddingHorizontal:20 contentPaddingVertical:12 itemSpacing:0
|
|
167
|
+
fontSize:14 fontFamily:nil backgroundColor:UIColor.lightGrayColor
|
|
168
|
+
activeTextColor:UIColor.blackColor inactiveTextColor:UIColor.grayColor
|
|
169
|
+
indicatorColor:UIColor.whiteColor indicatorHeight:2 indicatorBottom:0];
|
|
170
|
+
tabBar.frame = CGRectMake(0, 0, 320, 56);
|
|
171
|
+
[tabBar layoutIfNeeded];
|
|
172
|
+
UIScrollView *scroll = (UIScrollView *)tabBar.subviews.firstObject;
|
|
173
|
+
UIView *track = scroll.subviews.firstObject;
|
|
174
|
+
UIView *thumb = [tabBar valueForKey:@"indicatorView"];
|
|
175
|
+
NSArray<UIButton *> *buttons = [scroll.subviews filteredArrayUsingPredicate:
|
|
176
|
+
[NSPredicate predicateWithBlock:^BOOL(UIView *view, NSDictionary *bindings) {
|
|
177
|
+
return [view isKindOfClass:UIButton.class];
|
|
178
|
+
}]];
|
|
179
|
+
XCTAssertEqual(buttons.count, 3);
|
|
180
|
+
XCTAssertFalse(scroll.scrollEnabled);
|
|
181
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(track.frame), 20, 0.01);
|
|
182
|
+
XCTAssertEqualWithAccuracy(CGRectGetHeight(track.frame), 32, 0.01);
|
|
183
|
+
XCTAssertEqualWithAccuracy(CGRectGetWidth(buttons[0].frame),
|
|
184
|
+
CGRectGetWidth(buttons[1].frame), 0.01);
|
|
185
|
+
[tabBar setProgress:0];
|
|
186
|
+
CGFloat firstX = CGRectGetMinX(thumb.frame);
|
|
187
|
+
[tabBar setProgress:1];
|
|
188
|
+
CGFloat secondX = CGRectGetMinX(thumb.frame);
|
|
189
|
+
[tabBar setProgress:0.25];
|
|
190
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame), firstX + (secondX - firstX) * 0.25, 0.01);
|
|
191
|
+
[tabBar setProgress:0.5];
|
|
192
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame), (firstX + secondX) / 2, 0.01);
|
|
193
|
+
[tabBar setProgress:0];
|
|
194
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame), firstX, 0.01);
|
|
195
|
+
|
|
196
|
+
[tabBar setLeftToRight:NO];
|
|
197
|
+
[tabBar layoutIfNeeded];
|
|
198
|
+
XCTAssertGreaterThan(CGRectGetMinX(buttons[0].frame), CGRectGetMinX(buttons[1].frame));
|
|
199
|
+
[tabBar updateItemsJSON:@"[{\"key\":\"recent\",\"title\":\"Recents (8)\"},"
|
|
200
|
+
@"{\"key\":\"account\",\"title\":\"Accounts\"}]"];
|
|
201
|
+
[tabBar layoutIfNeeded];
|
|
202
|
+
NSArray<UIButton *> *twoButtons = [scroll.subviews filteredArrayUsingPredicate:
|
|
203
|
+
[NSPredicate predicateWithBlock:^BOOL(UIView *view, NSDictionary *bindings) {
|
|
204
|
+
return [view isKindOfClass:UIButton.class];
|
|
205
|
+
}]];
|
|
206
|
+
XCTAssertEqual(twoButtons.count, 2);
|
|
207
|
+
XCTAssertEqualWithAccuracy(CGRectGetWidth(twoButtons[0].frame),
|
|
208
|
+
CGRectGetWidth(track.frame) / 2, 0.01);
|
|
209
|
+
[tabBar updateStyleWithHeight:56 variant:@"segmented"
|
|
210
|
+
contentPaddingHorizontal:20 contentPaddingVertical:40 itemSpacing:0
|
|
211
|
+
fontSize:14 fontFamily:nil backgroundColor:UIColor.lightGrayColor
|
|
212
|
+
activeTextColor:UIColor.blackColor inactiveTextColor:UIColor.grayColor
|
|
213
|
+
indicatorColor:UIColor.whiteColor indicatorHeight:2 indicatorBottom:0];
|
|
214
|
+
tabBar.frame = CGRectMake(0, 0, 30, 56);
|
|
215
|
+
[tabBar layoutIfNeeded];
|
|
216
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(track.frame), 15, 0.01);
|
|
217
|
+
XCTAssertEqualWithAccuracy(CGRectGetWidth(track.frame), 0, 0.01);
|
|
218
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinY(track.frame), 28, 0.01);
|
|
219
|
+
XCTAssertEqualWithAccuracy(CGRectGetHeight(track.frame), 0, 0.01);
|
|
220
|
+
XCTAssertTrue(thumb.hidden);
|
|
221
|
+
[tabBar updateItemsJSON:@"[]"];
|
|
222
|
+
XCTAssertTrue(tabBar.hidden);
|
|
223
|
+
|
|
224
|
+
[tabBar updateItemsJSON:@"[{\"key\":\"recent\",\"title\":\"Recents\"}]"];
|
|
225
|
+
[tabBar updateStyleWithHeight:44 variant:@"underline"
|
|
226
|
+
contentPaddingHorizontal:20 contentPaddingVertical:0 itemSpacing:8
|
|
227
|
+
fontSize:16 fontFamily:nil backgroundColor:UIColor.clearColor
|
|
228
|
+
activeTextColor:UIColor.blackColor inactiveTextColor:UIColor.grayColor
|
|
229
|
+
indicatorColor:UIColor.blackColor indicatorHeight:2 indicatorBottom:0];
|
|
230
|
+
tabBar.frame = CGRectMake(0, 0, 320, 44);
|
|
231
|
+
[tabBar layoutIfNeeded];
|
|
232
|
+
XCTAssertTrue(scroll.scrollEnabled);
|
|
233
|
+
XCTAssertTrue(track.hidden);
|
|
234
|
+
XCTAssertEqualWithAccuracy(CGRectGetHeight(thumb.frame), 2, 0.01);
|
|
235
|
+
}
|
|
236
|
+
|
|
146
237
|
- (void)testPinnedChildOutsideHeaderHostStillReceivesViewportTouches
|
|
147
238
|
{
|
|
148
239
|
XCTAssertEqual([self.pager hitTest:CGPointMake(20, 90) withEvent:nil], self.sticky);
|
|
@@ -763,6 +763,26 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
763
763
|
XCTAssertTrue([pager isKindOfClass:NSClassFromString(@"RNCCollapsiblePagerDirectScrollView")]);
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
- (void)configureSegmentedTabsForTwoPages
|
|
767
|
+
{
|
|
768
|
+
auto baseline = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
769
|
+
baseline->nativeSmoothHeaderScrollEnabled = true;
|
|
770
|
+
baseline->headerHeight = 120;
|
|
771
|
+
baseline->stickyHeaderHeight = 44;
|
|
772
|
+
auto segmented = std::make_shared<RNCCollapsiblePagerViewProps>();
|
|
773
|
+
segmented->nativeSmoothHeaderScrollEnabled = true;
|
|
774
|
+
segmented->headerHeight = 120;
|
|
775
|
+
segmented->stickyHeaderHeight = 56;
|
|
776
|
+
segmented->nativeTabBarItems =
|
|
777
|
+
R"([{"key":"recent","title":"Recents"},{"key":"account","title":"Accounts"}])";
|
|
778
|
+
segmented->nativeTabBarVariant = "segmented";
|
|
779
|
+
segmented->nativeTabBarHeight = 56;
|
|
780
|
+
segmented->nativeTabBarContentPaddingHorizontal = 20;
|
|
781
|
+
segmented->nativeTabBarContentPaddingVertical = 12;
|
|
782
|
+
[self.pager updateProps:segmented oldProps:baseline];
|
|
783
|
+
[self.window layoutIfNeeded];
|
|
784
|
+
}
|
|
785
|
+
|
|
766
786
|
- (void)recordPagerPan:(UIPanGestureRecognizer *)pan
|
|
767
787
|
{
|
|
768
788
|
if (pan.state == UIGestureRecognizerStateBegan) self.pagerPanBegins += 1;
|
|
@@ -904,6 +924,51 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
|
|
|
904
924
|
XCTAssertEqual(self.handler.events.count, eventsBeforeLift);
|
|
905
925
|
}
|
|
906
926
|
|
|
927
|
+
- (void)testSegmentedThumbTracksDeliveredPagerDragAndReverseBeforeLift
|
|
928
|
+
{
|
|
929
|
+
[self assertFixtureIsReadyForDelivery];
|
|
930
|
+
[self assertDirectPagerForRegression];
|
|
931
|
+
[self configureSegmentedTabsForTwoPages];
|
|
932
|
+
|
|
933
|
+
UIScrollView *pagerScrollView = [self.pager valueForKey:@"pagerScrollView"];
|
|
934
|
+
UIView *tabBar = [self.pager valueForKey:@"nativeTabBarView"];
|
|
935
|
+
UIView *thumb = [tabBar valueForKey:@"indicatorView"];
|
|
936
|
+
NSArray<UIButton *> *buttons = [tabBar valueForKey:@"buttons"];
|
|
937
|
+
XCTAssertEqual(buttons.count, 2);
|
|
938
|
+
XCTAssertFalse(tabBar.hidden);
|
|
939
|
+
XCTAssertEqualWithAccuracy(CGRectGetHeight(tabBar.bounds), 56, 0.01);
|
|
940
|
+
CGFloat firstThumbX = CGRectGetMinX(buttons[0].frame) + 2;
|
|
941
|
+
CGFloat secondThumbX = CGRectGetMinX(buttons[1].frame) + 2;
|
|
942
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame), firstThumbX, 1);
|
|
943
|
+
|
|
944
|
+
CGPoint point = CGPointMake(350, 600);
|
|
945
|
+
UITouch *touch = [self.driver beginAt:point];
|
|
946
|
+
CGFloat largestOffset = 0;
|
|
947
|
+
NSUInteger intermediateFrames = 0;
|
|
948
|
+
for (NSInteger step = 1; step <= 18; ++step) {
|
|
949
|
+
[self.driver move:touch to:CGPointMake(point.x - 14 * step, point.y)];
|
|
950
|
+
CGFloat offset = pagerScrollView.contentOffset.x;
|
|
951
|
+
largestOffset = MAX(largestOffset, offset);
|
|
952
|
+
if (offset > 1 && offset < CGRectGetWidth(pagerScrollView.bounds) - 1) {
|
|
953
|
+
CGFloat progress = offset / CGRectGetWidth(pagerScrollView.bounds);
|
|
954
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame),
|
|
955
|
+
firstThumbX + (secondThumbX - firstThumbX) * progress, 1);
|
|
956
|
+
intermediateFrames += 1;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
XCTAssertTrue(pagerScrollView.isDragging);
|
|
960
|
+
XCTAssertGreaterThan(largestOffset, 80);
|
|
961
|
+
XCTAssertGreaterThan(intermediateFrames, 2);
|
|
962
|
+
|
|
963
|
+
for (NSInteger step = 17; step >= 0; --step) {
|
|
964
|
+
[self.driver move:touch to:CGPointMake(point.x - 14 * step, point.y)];
|
|
965
|
+
}
|
|
966
|
+
// UIKit can briefly bounce past the leading edge before the finger lifts.
|
|
967
|
+
XCTAssertLessThan(fabs(pagerScrollView.contentOffset.x), 10);
|
|
968
|
+
XCTAssertEqualWithAccuracy(CGRectGetMinX(thumb.frame), firstThumbX, 1);
|
|
969
|
+
[self.driver end:touch];
|
|
970
|
+
}
|
|
971
|
+
|
|
907
972
|
- (void)testStationaryTapKeepsReactPress
|
|
908
973
|
{
|
|
909
974
|
[self assertFixtureIsReadyForDelivery];
|
|
@@ -242,6 +242,8 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
242
242
|
const deducedLayoutDirection = !layoutDirection || layoutDirection === "locale" ? I18nManager.isRTL ? "rtl" : "ltr" : layoutDirection;
|
|
243
243
|
const nativeTabBarEnabled = (Platform.OS === "ios" || Platform.OS === "android") && !!nativeTabBar?.items.length;
|
|
244
244
|
const nativeTabBarStyle = nativeTabBar?.style;
|
|
245
|
+
const requestedContentPaddingVertical = nativeTabBarStyle?.contentPaddingVertical;
|
|
246
|
+
const nativeTabBarContentPaddingVertical = typeof requestedContentPaddingVertical === "number" && Number.isFinite(requestedContentPaddingVertical) && requestedContentPaddingVertical >= 0 ? requestedContentPaddingVertical : 0;
|
|
245
247
|
const requestedTrailingReservedWidth = nativeTabBarStyle?.trailingReservedWidth;
|
|
246
248
|
const nativeTabBarTrailingReservedWidth = typeof requestedTrailingReservedWidth === "number" && Number.isFinite(requestedTrailingReservedWidth) && requestedTrailingReservedWidth >= 0 ? requestedTrailingReservedWidth : 0;
|
|
247
249
|
const nativeSubHeaderEnabled = (Platform.OS === "ios" || Platform.OS === "android") && !!nativeSubHeader?.items.length;
|
|
@@ -274,8 +276,10 @@ export class CollapsiblePagerView extends React.PureComponent {
|
|
|
274
276
|
onPageScrollStateChanged: this.onPageScrollStateChanged,
|
|
275
277
|
nativeTabBarItems: nativeTabBarEnabled ? JSON.stringify(nativeTabBar.items) : undefined,
|
|
276
278
|
nativeTabBarHeight: nativeTabBarEnabled ? nativeTabBarStyle?.height ?? 44 : undefined,
|
|
279
|
+
nativeTabBarVariant: nativeTabBarEnabled ? nativeTabBarStyle?.variant ?? "underline" : undefined,
|
|
277
280
|
nativeTabBarTrailingReservedWidth: nativeTabBarEnabled ? nativeTabBarTrailingReservedWidth : 0,
|
|
278
281
|
nativeTabBarContentPaddingHorizontal: nativeTabBarEnabled ? nativeTabBarStyle?.contentPaddingHorizontal ?? 20 : undefined,
|
|
282
|
+
nativeTabBarContentPaddingVertical: nativeTabBarEnabled ? nativeTabBarContentPaddingVertical : undefined,
|
|
279
283
|
nativeTabBarItemSpacing: nativeTabBarEnabled ? nativeTabBarStyle?.itemSpacing ?? 8 : undefined,
|
|
280
284
|
nativeTabBarFontSize: nativeTabBarEnabled ? nativeTabBarStyle?.fontSize ?? 16 : undefined,
|
|
281
285
|
nativeTabBarFontFamily: nativeTabBarEnabled ? nativeTabBarStyle?.fontFamily : undefined,
|
|
@@ -67,8 +67,10 @@ export interface NativeProps extends ViewProps {
|
|
|
67
67
|
retainedPages?: string;
|
|
68
68
|
nativeTabBarItems?: string;
|
|
69
69
|
nativeTabBarHeight?: Double;
|
|
70
|
+
nativeTabBarVariant?: string;
|
|
70
71
|
nativeTabBarTrailingReservedWidth?: WithDefault<Double, 0>;
|
|
71
72
|
nativeTabBarContentPaddingHorizontal?: Double;
|
|
73
|
+
nativeTabBarContentPaddingVertical?: Double;
|
|
72
74
|
nativeTabBarItemSpacing?: Double;
|
|
73
75
|
nativeTabBarFontSize?: Double;
|
|
74
76
|
nativeTabBarFontFamily?: string;
|
|
@@ -16,9 +16,11 @@ export type CollapsiblePagerNativeTabBarItem = Readonly<{
|
|
|
16
16
|
testID?: string;
|
|
17
17
|
}>;
|
|
18
18
|
export type CollapsiblePagerNativeTabBarStyle = Readonly<{
|
|
19
|
+
variant?: "underline" | "segmented";
|
|
19
20
|
height?: number;
|
|
20
21
|
trailingReservedWidth?: number;
|
|
21
22
|
contentPaddingHorizontal?: number;
|
|
23
|
+
contentPaddingVertical?: number;
|
|
22
24
|
itemSpacing?: number;
|
|
23
25
|
fontSize?: number;
|
|
24
26
|
fontFamily?: string;
|
|
@@ -57,7 +59,7 @@ export type CollapsiblePagerNativeSubHeaderConfig = Readonly<{
|
|
|
57
59
|
}>;
|
|
58
60
|
export type CollapsiblePagerViewOnNativeTabPressEvent = ReactNative.NativeSyntheticEvent<OnNativeTabPressEventData>;
|
|
59
61
|
export type CollapsiblePagerViewOnNativeSubHeaderPressEvent = ReactNative.NativeSyntheticEvent<OnNativeTabPressEventData>;
|
|
60
|
-
export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children" | "headerHeight" | "stickyHeaderHeight" | "topScrollEdgeEffect" | "topOverlayHeight" | "externalTopOverlayTag" | "pageKeys" | "retainedPages" | "nativeTabBarItems" | "nativeTabBarHeight" | "nativeTabBarTrailingReservedWidth" | "nativeTabBarContentPaddingHorizontal" | "nativeTabBarItemSpacing" | "nativeTabBarFontSize" | "nativeTabBarFontFamily" | "nativeTabBarBackgroundColor" | "nativeTabBarActiveTextColor" | "nativeTabBarInactiveTextColor" | "nativeTabBarIndicatorColor" | "nativeTabBarIndicatorHeight" | "nativeTabBarIndicatorBottom" | "nativeSubHeaderConfig" | "nativeSubHeaderSelectedBackgroundColor" | "onNativeTabPress" | "onNativeSubHeaderPress" | "layoutDirection"> {
|
|
62
|
+
export interface CollapsiblePagerViewProps extends Omit<NativeProps, "children" | "headerHeight" | "stickyHeaderHeight" | "topScrollEdgeEffect" | "topOverlayHeight" | "externalTopOverlayTag" | "pageKeys" | "retainedPages" | "nativeTabBarItems" | "nativeTabBarHeight" | "nativeTabBarVariant" | "nativeTabBarTrailingReservedWidth" | "nativeTabBarContentPaddingHorizontal" | "nativeTabBarContentPaddingVertical" | "nativeTabBarItemSpacing" | "nativeTabBarFontSize" | "nativeTabBarFontFamily" | "nativeTabBarBackgroundColor" | "nativeTabBarActiveTextColor" | "nativeTabBarInactiveTextColor" | "nativeTabBarIndicatorColor" | "nativeTabBarIndicatorHeight" | "nativeTabBarIndicatorBottom" | "nativeSubHeaderConfig" | "nativeSubHeaderSelectedBackgroundColor" | "onNativeTabPress" | "onNativeSubHeaderPress" | "layoutDirection"> {
|
|
61
63
|
/** Collapsible content rendered above the sticky bar. */
|
|
62
64
|
header: React.ReactNode;
|
|
63
65
|
/** Bar which remains pinned after the header has collapsed. */
|
|
@@ -50,8 +50,10 @@ export interface NativeProps extends ViewProps {
|
|
|
50
50
|
retainedPages?: string;
|
|
51
51
|
nativeTabBarItems?: string;
|
|
52
52
|
nativeTabBarHeight?: Double;
|
|
53
|
+
nativeTabBarVariant?: string;
|
|
53
54
|
nativeTabBarTrailingReservedWidth?: WithDefault<Double, 0>;
|
|
54
55
|
nativeTabBarContentPaddingHorizontal?: Double;
|
|
56
|
+
nativeTabBarContentPaddingVertical?: Double;
|
|
55
57
|
nativeTabBarItemSpacing?: Double;
|
|
56
58
|
nativeTabBarFontSize?: Double;
|
|
57
59
|
nativeTabBarFontFamily?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.359",
|
|
4
4
|
"description": "React Native wrapper for Android and iOS ViewPager",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"typescript": "^5.9.2"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
76
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.359",
|
|
77
77
|
"react": "*",
|
|
78
78
|
"react-native": "*"
|
|
79
79
|
},
|
|
@@ -32,9 +32,11 @@ export type CollapsiblePagerNativeTabBarItem = Readonly<{
|
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
export type CollapsiblePagerNativeTabBarStyle = Readonly<{
|
|
35
|
+
variant?: "underline" | "segmented";
|
|
35
36
|
height?: number;
|
|
36
37
|
trailingReservedWidth?: number;
|
|
37
38
|
contentPaddingHorizontal?: number;
|
|
39
|
+
contentPaddingVertical?: number;
|
|
38
40
|
itemSpacing?: number;
|
|
39
41
|
fontSize?: number;
|
|
40
42
|
fontFamily?: string;
|
|
@@ -95,8 +97,10 @@ export interface CollapsiblePagerViewProps
|
|
|
95
97
|
| "retainedPages"
|
|
96
98
|
| "nativeTabBarItems"
|
|
97
99
|
| "nativeTabBarHeight"
|
|
100
|
+
| "nativeTabBarVariant"
|
|
98
101
|
| "nativeTabBarTrailingReservedWidth"
|
|
99
102
|
| "nativeTabBarContentPaddingHorizontal"
|
|
103
|
+
| "nativeTabBarContentPaddingVertical"
|
|
100
104
|
| "nativeTabBarItemSpacing"
|
|
101
105
|
| "nativeTabBarFontSize"
|
|
102
106
|
| "nativeTabBarFontFamily"
|
|
@@ -568,6 +572,14 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
568
572
|
(Platform.OS === "ios" || Platform.OS === "android") &&
|
|
569
573
|
!!nativeTabBar?.items.length;
|
|
570
574
|
const nativeTabBarStyle = nativeTabBar?.style;
|
|
575
|
+
const requestedContentPaddingVertical =
|
|
576
|
+
nativeTabBarStyle?.contentPaddingVertical;
|
|
577
|
+
const nativeTabBarContentPaddingVertical =
|
|
578
|
+
typeof requestedContentPaddingVertical === "number" &&
|
|
579
|
+
Number.isFinite(requestedContentPaddingVertical) &&
|
|
580
|
+
requestedContentPaddingVertical >= 0
|
|
581
|
+
? requestedContentPaddingVertical
|
|
582
|
+
: 0;
|
|
571
583
|
const requestedTrailingReservedWidth =
|
|
572
584
|
nativeTabBarStyle?.trailingReservedWidth;
|
|
573
585
|
const nativeTabBarTrailingReservedWidth =
|
|
@@ -629,6 +641,9 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
629
641
|
nativeTabBarHeight={
|
|
630
642
|
nativeTabBarEnabled ? nativeTabBarStyle?.height ?? 44 : undefined
|
|
631
643
|
}
|
|
644
|
+
nativeTabBarVariant={
|
|
645
|
+
nativeTabBarEnabled ? nativeTabBarStyle?.variant ?? "underline" : undefined
|
|
646
|
+
}
|
|
632
647
|
nativeTabBarTrailingReservedWidth={
|
|
633
648
|
nativeTabBarEnabled ? nativeTabBarTrailingReservedWidth : 0
|
|
634
649
|
}
|
|
@@ -637,6 +652,9 @@ export class CollapsiblePagerView extends React.PureComponent<
|
|
|
637
652
|
? nativeTabBarStyle?.contentPaddingHorizontal ?? 20
|
|
638
653
|
: undefined
|
|
639
654
|
}
|
|
655
|
+
nativeTabBarContentPaddingVertical={
|
|
656
|
+
nativeTabBarEnabled ? nativeTabBarContentPaddingVertical : undefined
|
|
657
|
+
}
|
|
640
658
|
nativeTabBarItemSpacing={
|
|
641
659
|
nativeTabBarEnabled ? nativeTabBarStyle?.itemSpacing ?? 8 : undefined
|
|
642
660
|
}
|
|
@@ -67,8 +67,10 @@ export interface NativeProps extends ViewProps {
|
|
|
67
67
|
retainedPages?: string;
|
|
68
68
|
nativeTabBarItems?: string;
|
|
69
69
|
nativeTabBarHeight?: Double;
|
|
70
|
+
nativeTabBarVariant?: string;
|
|
70
71
|
nativeTabBarTrailingReservedWidth?: WithDefault<Double, 0>;
|
|
71
72
|
nativeTabBarContentPaddingHorizontal?: Double;
|
|
73
|
+
nativeTabBarContentPaddingVertical?: Double;
|
|
72
74
|
nativeTabBarItemSpacing?: Double;
|
|
73
75
|
nativeTabBarFontSize?: Double;
|
|
74
76
|
nativeTabBarFontFamily?: string;
|