@onekeyfe/react-native-pager-view 6.7.0-alpha.435 → 6.7.0-alpha.445

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.
Files changed (25) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +1 -0
  3. package/android/src/androidTest/java/com/reactnativepagerview/CollapsiblePagerTabReservationTest.kt +135 -0
  4. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +88 -12
  5. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerNativeHeaders.kt +442 -15
  6. package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerViewManager.kt +11 -1
  7. package/android/src/main/java/com/reactnativepagerview/event/NativeHeaderPressEvent.kt +8 -0
  8. package/docs/SPEC.md +298 -3
  9. package/ios/RNCCollapsiblePagerViewComponentView.mm +834 -20
  10. package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +61 -0
  11. package/ios/tests/RNCCollapsiblePagerHitTestTests.mm +22 -0
  12. package/lib/module/CollapsiblePagerView.js +37 -5
  13. package/lib/module/CollapsiblePagerView.web.js +519 -6
  14. package/lib/module/CollapsiblePagerViewNativeComponent.ts +6 -0
  15. package/lib/module/NativeScrollerNativeComponent.ts +0 -1
  16. package/lib/typescript/src/CollapsiblePagerView.d.ts +31 -2
  17. package/lib/typescript/src/CollapsiblePagerView.web.d.ts +44 -1
  18. package/lib/typescript/src/CollapsiblePagerViewNativeComponent.d.ts +11 -0
  19. package/lib/typescript/src/NativeScroller.d.ts +2 -2
  20. package/package.json +3 -2
  21. package/src/CollapsiblePagerView.tsx +68 -5
  22. package/src/CollapsiblePagerView.web.tsx +464 -5
  23. package/src/CollapsiblePagerViewNativeComponent.ts +6 -0
  24. package/src/NativeScroller.tsx +2 -3
  25. package/src/NativeScrollerNativeComponent.ts +0 -1
@@ -2,8 +2,12 @@ package com.reactnativepagerview
2
2
 
3
3
  import android.content.Context
4
4
  import android.graphics.Color
5
+ import android.graphics.Bitmap
5
6
  import android.graphics.Typeface
6
7
  import android.graphics.drawable.GradientDrawable
8
+ import android.graphics.drawable.StateListDrawable
9
+ import android.widget.ImageView
10
+ import com.margelo.nitro.onekeyimage.OneKeyNamedSVG
7
11
  import android.text.TextUtils
8
12
  import android.util.TypedValue
9
13
  import android.view.Gravity
@@ -81,6 +85,7 @@ private class CollapsiblePagerHorizontalItemsView(
81
85
  private var contentPaddingPx = dp(20.0)
82
86
  private var contentPaddingVerticalPx = 0
83
87
  private var segmented = false
88
+ private var textVariant = false
84
89
  private var itemSpacingPx = dp(8.0)
85
90
  private var fontSize = if (showsProgressIndicator) 16.0 else 14.0
86
91
  private var fontFamily: String? = null
@@ -91,6 +96,12 @@ private class CollapsiblePagerHorizontalItemsView(
91
96
  private var indicatorHeightPx = dp(2.0)
92
97
  private var indicatorBottomPx = 0
93
98
  private var progress = 0f
99
+ private var mappedSource: Int? = null
100
+ private var mappedDestination: Int? = null
101
+ private var mappedFraction = 0f
102
+ private var activeFontFamily: String? = null
103
+ private var controlledSelectedKey: String? = null
104
+ private var validControlledPageMapping = false
94
105
  private var centerSelectionWhenIdle = true
95
106
  private var isTouchDragging = false
96
107
 
@@ -165,6 +176,7 @@ private class CollapsiblePagerHorizontalItemsView(
165
176
  contentPaddingVerticalPx = max(0, dp(contentPaddingVertical))
166
177
  val wasSegmented = segmented
167
178
  segmented = showsProgressIndicator && variant == "segmented"
179
+ textVariant = showsProgressIndicator && variant == "text"
168
180
  itemSpacingPx = max(0, dp(itemSpacing))
169
181
  this.fontSize = max(1.0, fontSize)
170
182
  this.fontFamily = fontFamily
@@ -197,7 +209,20 @@ private class CollapsiblePagerHorizontalItemsView(
197
209
  updatePresentation()
198
210
  }
199
211
 
200
- fun setProgress(value: Float) {
212
+ fun setControlledSelectedKey(value: String?, validPageMapping: Boolean) {
213
+ controlledSelectedKey = value
214
+ validControlledPageMapping = validPageMapping
215
+ updatePresentation()
216
+ }
217
+
218
+ fun usesTextVariant(): Boolean = textVariant
219
+
220
+ fun setActiveFontFamily(value: String?) { activeFontFamily = value; requestLayout() }
221
+
222
+ fun setProgress(value: Float, source: Int? = null, destination: Int? = null, fraction: Float = 0f) {
223
+ mappedSource = source
224
+ mappedDestination = destination
225
+ mappedFraction = fraction
201
226
  if (!showsProgressIndicator || items.isEmpty()) return
202
227
  val next = value.coerceIn(0f, (items.size - 1).toFloat())
203
228
  if (kotlin.math.abs(progress - next) >= 0.001f) centerSelectionWhenIdle = true
@@ -205,10 +230,25 @@ private class CollapsiblePagerHorizontalItemsView(
205
230
  updatePresentation()
206
231
  }
207
232
 
233
+ private fun selectedTypeface(): Typeface = activeFontFamily?.takeIf(String::isNotEmpty)?.let { resolveHeaderTypeface(context, it) }
234
+ ?: if (android.os.Build.VERSION.SDK_INT >= 28) Typeface.create(Typeface.create("sans-serif", Typeface.NORMAL), 600, false)
235
+ else Typeface.create("sans-serif", Typeface.BOLD)
236
+
237
+ private fun visualFontOwnerIndex(semanticIndex: Int): Int {
238
+ val source = mappedSource
239
+ val destination = mappedDestination
240
+ if (textVariant && controlledSelectedKey != null && validControlledPageMapping && source != null && destination != null) {
241
+ return if (mappedFraction < 0.5f) source else destination
242
+ }
243
+ return semanticIndex
244
+ }
245
+
208
246
  private fun applyTypeface() {
209
247
  val typeface = resolveHeaderTypeface(context, fontFamily)
210
- buttons.forEach { button ->
211
- button.typeface = typeface
248
+ val fontOwner = visualFontOwnerIndex(buttons.indexOfFirst { it.isSelected })
249
+ buttons.forEachIndexed { index, button ->
250
+ button.typeface = if (textVariant && index == fontOwner) selectedTypeface() else typeface
251
+ button.setPadding(dp(if (textVariant) 10.0 else 8.0), 0, dp(if (textVariant) 10.0 else 8.0), 0)
212
252
  button.setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSize.toFloat())
213
253
  }
214
254
  }
@@ -271,7 +311,15 @@ private class CollapsiblePagerHorizontalItemsView(
271
311
  track.layoutParams = FrameLayout.LayoutParams(0, 0)
272
312
  var x = contentPaddingPx
273
313
  buttons.forEachIndexed { index, button ->
274
- val textWidth = ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt()
314
+ var textWidth = ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt()
315
+ if (textVariant) {
316
+ val original = button.paint.typeface
317
+ button.paint.typeface = resolveHeaderTypeface(context, fontFamily)
318
+ textWidth = ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt()
319
+ button.paint.typeface = selectedTypeface()
320
+ textWidth = max(textWidth, ceil(button.paint.measureText(button.text.toString()).toDouble()).toInt())
321
+ button.paint.typeface = original
322
+ }
275
323
  // Reuse the rounded padding: dp(16) can be one pixel less than two dp(8) insets.
276
324
  val horizontalPadding = if (showsProgressIndicator) {
277
325
  button.compoundPaddingLeft + button.compoundPaddingRight
@@ -343,14 +391,18 @@ private class CollapsiblePagerHorizontalItemsView(
343
391
  indicatorX + indicatorWidth,
344
392
  indicatorTop + indicatorHeight,
345
393
  )
346
- indicator.visibility = if (indicatorHeight > 0) VISIBLE else GONE
394
+ indicator.visibility = if (!textVariant && indicatorHeight > 0) VISIBLE else GONE
347
395
  scrollTargetCenterX = interpolatedIndicatorX + interpolatedIndicatorWidth / 2f
348
- selectedIndex = clamped.roundToInt()
396
+ selectedIndex = if (controlledSelectedKey != null) items.indexOfFirst { it.key == controlledSelectedKey } else clamped.roundToInt()
397
+ val fontOwner = visualFontOwnerIndex(selectedIndex)
349
398
  buttons.forEachIndexed { index, button ->
350
- val emphasis = 1f - min(1f, kotlin.math.abs(clamped - index))
399
+ val emphasis = if (mappedSource != null && mappedDestination != null) {
400
+ (if (index == mappedSource) 1f - mappedFraction else 0f) + (if (index == mappedDestination) mappedFraction else 0f)
401
+ } else 1f - min(1f, kotlin.math.abs(clamped - index))
351
402
  button.setTextColor(ColorUtils.blendARGB(inactiveTextColor, activeTextColor, emphasis))
352
403
  button.background = null
353
404
  button.isSelected = index == selectedIndex
405
+ if (textVariant) button.typeface = if (index == fontOwner) selectedTypeface() else resolveHeaderTypeface(context, fontFamily)
354
406
  }
355
407
  } else {
356
408
  selectedIndex = items.indexOfFirst { item -> item.key == selectedKey }
@@ -377,6 +429,273 @@ private class CollapsiblePagerHorizontalItemsView(
377
429
  internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(context) {
378
430
  private val row = CollapsiblePagerHorizontalItemsView(context, true)
379
431
  private var heightPx = 0
432
+ private val toolbar = ImageView(context)
433
+ private var config = JSONObject()
434
+ private var items = emptyList<CollapsiblePagerNativeHeaderItem>()
435
+ private var selectedKey = ""
436
+ private var iconOwner: Any = Any()
437
+ private var iconIdentity = ""
438
+ private var loadedIconIdentity = ""
439
+ private var iconRequestPending = false
440
+ private var readyToolbarImage: Bitmap? = null
441
+ private var readyToolbarIdentity = ""
442
+ private var failedIconIdentity = ""
443
+ private var toolbarTouchKey: String? = null
444
+ private var toolbarTouchStarted = false
445
+ private var toolbarKeyboardKey: String? = null
446
+ private var lastNativeProgress = 0f
447
+ private var hasNativeProgress = false
448
+ private var toolbarReservedWidth = 0.0
449
+ private var toolbarPagerBusy = true
450
+ private var toolbarPageIndex = -1
451
+ private var committedToolbarKey: String? = null
452
+ private var committedToolbarEntry: JSONObject? = null
453
+ private var desiredToolbarKey: String? = null
454
+ private var desiredToolbarEntry: JSONObject? = null
455
+ private var loadedToolbarImage: Bitmap? = null
456
+ private var toolbarPresenceAnimating = false
457
+ private var toolbarPresenceInterrupted = false
458
+ private var toolbarVisible = false
459
+ private var toolbarAnimationGeneration = 0
460
+ private var toolbarSize = 38.0
461
+ private var toolbarIconSize = 24.0
462
+ private var toolbarContentWidth = 24.0
463
+ private var toolbarTrailingInset = 20.0
464
+ var onToolbarPress: ((String, DoubleArray) -> Unit)? = null
465
+
466
+ private fun dp(value: Number) = (value.toDouble() * resources.displayMetrics.density).roundToInt()
467
+
468
+ fun updateConfigJSON(value: String?) {
469
+ config = runCatching { JSONObject(value ?: "{}") }.getOrDefault(JSONObject())
470
+ row.setActiveFontFamily(config.optString("activeFontFamily").takeIf(String::isNotEmpty))
471
+ selectedKey = if (config.optBoolean("controlled")) config.optString("selectedKey") else items.getOrNull(lastNativeProgress.roundToInt())?.key ?: ""
472
+ row.setControlledSelectedKey(if (config.optBoolean("controlled")) selectedKey else null, controlledPageIndices() != null)
473
+ if (hasNativeProgress) setProgress(lastNativeProgress)
474
+ else {
475
+ val index = items.indexOfFirst { it.key == selectedKey }
476
+ if (index >= 0) row.setProgress(index.toFloat())
477
+ }
478
+ updateToolbar()
479
+ }
480
+
481
+ private fun ownsSettledToolbar(): Boolean = config.optBoolean("controlled") && config.optJSONObject("toolbar") != null
482
+
483
+ internal fun usesSettledToolbarOwner(): Boolean = ownsSettledToolbar()
484
+
485
+ internal fun needsToolbarPagerSettlement(pageIndex: Int): Boolean =
486
+ ownsSettledToolbar() && (toolbarPagerBusy || toolbarPageIndex != pageIndex)
487
+
488
+ private fun toolbarCanCommit(): Boolean {
489
+ if (!isAttachedToWindow) return false
490
+ if (!ownsSettledToolbar()) return true
491
+ if (toolbarPagerBusy || toolbarPageIndex < 0) return false
492
+ return controlledPageIndices() == null || config.optJSONArray("pageKeys")?.optString(toolbarPageIndex) == selectedKey
493
+ }
494
+
495
+ fun updateToolbarPagerState(pageIndex: Int, busy: Boolean) {
496
+ if (toolbarPagerBusy == busy && (busy || toolbarPageIndex == pageIndex)) return
497
+ if (!busy) toolbarPageIndex = pageIndex
498
+ toolbarPagerBusy = busy
499
+ updateToolbar()
500
+ }
501
+
502
+ private fun cancelToolbarPending() {
503
+ if (iconRequestPending || readyToolbarImage != null) {
504
+ OneKeyNamedSVG.cancel(iconOwner)
505
+ iconOwner = Any()
506
+ }
507
+ iconRequestPending = false
508
+ readyToolbarImage = null
509
+ readyToolbarIdentity = ""
510
+ }
511
+
512
+ private fun setToolbarInteractive(enabled: Boolean) {
513
+ if (!enabled) {
514
+ toolbarKeyboardKey = null
515
+ if (toolbarTouchStarted) toolbarTouchKey = null
516
+ toolbar.isPressed = false
517
+ toolbar.isHovered = false
518
+ toolbar.clearFocus()
519
+ }
520
+ toolbar.isEnabled = enabled
521
+ toolbar.isClickable = enabled
522
+ toolbar.isFocusable = enabled
523
+ toolbar.importantForAccessibility = if (enabled) View.IMPORTANT_FOR_ACCESSIBILITY_AUTO else View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
524
+ }
525
+
526
+ private fun suspendToolbar() {
527
+ setToolbarInteractive(false)
528
+ cancelToolbarPending()
529
+ desiredToolbarKey = null
530
+ desiredToolbarEntry = null
531
+ if (toolbarPresenceAnimating) {
532
+ toolbarAnimationGeneration++
533
+ toolbar.animate().cancel()
534
+ toolbarPresenceAnimating = false
535
+ toolbarPresenceInterrupted = true
536
+ }
537
+ }
538
+
539
+ private fun setToolbarVisible(visible: Boolean) {
540
+ if (toolbarVisible == visible && !toolbarPresenceInterrupted) return
541
+ toolbarVisible = visible
542
+ toolbarPresenceInterrupted = false
543
+ val generation = ++toolbarAnimationGeneration
544
+ toolbar.animate().cancel()
545
+ if (visible) toolbar.visibility = VISIBLE
546
+ if (!isAttachedToWindow) {
547
+ toolbar.alpha = if (visible) 1f else 0f
548
+ toolbar.visibility = if (visible) VISIBLE else INVISIBLE
549
+ toolbarPresenceAnimating = false
550
+ return
551
+ }
552
+ toolbarPresenceAnimating = true
553
+ toolbar.animate().alpha(if (visible) 1f else 0f).setDuration(100).withEndAction {
554
+ if (toolbarAnimationGeneration == generation) {
555
+ toolbarPresenceAnimating = false
556
+ if (!visible) toolbar.visibility = INVISIBLE
557
+ }
558
+ }.start()
559
+ }
560
+
561
+ private fun hideToolbar(failed: Boolean) {
562
+ setToolbarInteractive(false)
563
+ committedToolbarKey = null
564
+ committedToolbarEntry = null
565
+ toolbar.contentDescription = null
566
+ toolbar.setTag(com.facebook.react.R.id.react_test_id, null)
567
+ if (failed) toolbar.setImageDrawable(null)
568
+ setToolbarVisible(false)
569
+ }
570
+
571
+ private fun commitToolbar(key: String, descriptor: JSONObject, image: Bitmap, identity: String) {
572
+ if (!toolbarCanCommit() || key != selectedKey) return
573
+ if (committedToolbarKey != key && toolbarTouchStarted) toolbarTouchKey = null
574
+ toolbarSize = descriptor.optDouble("size", 38.0).coerceIn(1.0, 128.0)
575
+ toolbarIconSize = descriptor.optDouble("iconSize", min(24.0, toolbarSize)).coerceIn(1.0, toolbarSize)
576
+ toolbarContentWidth = descriptor.optDouble("contentWidth", min(24.0, toolbarSize)).coerceIn(0.0, toolbarSize)
577
+ toolbarTrailingInset = descriptor.optDouble("trailingInset", 20.0).coerceIn(0.0, 128.0)
578
+ val inset = max(0, (dp(toolbarSize) - dp(toolbarIconSize)) / 2)
579
+ toolbar.setPadding(inset, inset, inset, inset)
580
+ fun color(name: String, fallback: Int) = (descriptor.opt(name) as? Number)?.toInt() ?: fallback
581
+ val base = if (descriptor.optBoolean("selected")) color("selectedBackgroundColor", Color.TRANSPARENT) else Color.TRANSPARENT
582
+ fun fill(value: Int) = GradientDrawable().apply { shape = GradientDrawable.OVAL; setColor(value) }
583
+ toolbar.background = StateListDrawable().apply {
584
+ addState(intArrayOf(android.R.attr.state_enabled, android.R.attr.state_pressed), fill(color("pressedBackgroundColor", base)))
585
+ addState(intArrayOf(android.R.attr.state_enabled, android.R.attr.state_hovered), fill(color("hoverBackgroundColor", base)))
586
+ addState(intArrayOf(), fill(base))
587
+ }
588
+ toolbar.setImageBitmap(image)
589
+ toolbar.contentDescription = descriptor.optString("accessibilityLabel")
590
+ toolbar.setTag(com.facebook.react.R.id.react_test_id, descriptor.optString("testID"))
591
+ committedToolbarKey = key
592
+ committedToolbarEntry = descriptor
593
+ loadedToolbarImage = image
594
+ loadedIconIdentity = identity
595
+ iconIdentity = identity
596
+ iconRequestPending = false
597
+ readyToolbarImage = null
598
+ readyToolbarIdentity = ""
599
+ setToolbarVisible(true)
600
+ setToolbarInteractive(true)
601
+ requestLayout()
602
+ }
603
+
604
+ private fun applyReadyToolbarImage() {
605
+ val image = readyToolbarImage ?: return
606
+ val key = desiredToolbarKey ?: return
607
+ val entry = desiredToolbarEntry ?: return
608
+ if (readyToolbarIdentity != iconIdentity || !toolbarCanCommit() || key != selectedKey) return
609
+ commitToolbar(key, entry, image, iconIdentity)
610
+ }
611
+
612
+ private fun updateToolbar() {
613
+ val entries = config.optJSONObject("toolbar")
614
+ toolbarReservedWidth = if (entries == null) 0.0 else 44.0
615
+ if (entries != null && entries.length() > 0) {
616
+ toolbarReservedWidth = 0.0
617
+ entries.keys().forEach { key ->
618
+ val entry = entries.optJSONObject(key) ?: JSONObject()
619
+ val size = entry.optDouble("size", 38.0).coerceIn(1.0, 128.0)
620
+ toolbarReservedWidth = max(toolbarReservedWidth, entry.optDouble("contentWidth", min(24.0, size)).coerceIn(0.0, size) + entry.optDouble("trailingInset", 20.0).coerceIn(0.0, 128.0))
621
+ }
622
+ }
623
+ if (!toolbarCanCommit()) { suspendToolbar(); requestLayout(); return }
624
+ val entry = entries?.optJSONObject(selectedKey)
625
+ if (entry == null || entry.optString("iconName").isEmpty()) {
626
+ cancelToolbarPending()
627
+ desiredToolbarKey = null
628
+ desiredToolbarEntry = null
629
+ hideToolbar(false)
630
+ requestLayout()
631
+ return
632
+ }
633
+ val descriptor = JSONObject(entry.toString())
634
+ desiredToolbarKey = selectedKey
635
+ desiredToolbarEntry = descriptor
636
+ val size = descriptor.optDouble("size", 38.0).coerceIn(1.0, 128.0)
637
+ val iconSize = descriptor.optDouble("iconSize", min(24.0, size)).coerceIn(1.0, size)
638
+ val name = descriptor.optString("iconName")
639
+ val tint = descriptor.optString("iconColor", "#000000")
640
+ val identity = "$name|$tint|${resources.displayMetrics.density}|$iconSize"
641
+ val loaded = loadedToolbarImage
642
+ if (identity == loadedIconIdentity && loaded != null) {
643
+ cancelToolbarPending()
644
+ commitToolbar(selectedKey, descriptor, loaded, identity)
645
+ return
646
+ }
647
+ if (identity == iconIdentity && iconRequestPending) { applyReadyToolbarImage(); return }
648
+ cancelToolbarPending()
649
+ setToolbarInteractive(false)
650
+ iconIdentity = identity
651
+ if (identity == failedIconIdentity) { hideToolbar(true); return }
652
+ iconOwner = Any()
653
+ val owner = iconOwner
654
+ iconRequestPending = true
655
+ OneKeyNamedSVG.request(owner, context.applicationContext.assets, name, tint,
656
+ dp(iconSize), dp(iconSize), resources.displayMetrics.density) { bitmap ->
657
+ if (iconOwner === owner && iconIdentity == identity && isAttachedToWindow) {
658
+ if (bitmap == null) {
659
+ iconRequestPending = false
660
+ failedIconIdentity = identity
661
+ hideToolbar(true)
662
+ } else {
663
+ readyToolbarImage = bitmap
664
+ readyToolbarIdentity = identity
665
+ applyReadyToolbarImage()
666
+ }
667
+ }
668
+ }
669
+ requestLayout()
670
+ }
671
+
672
+ private fun performToolbarPress(fromPointer: Boolean): Boolean {
673
+ val key = if (fromPointer) toolbarTouchKey.takeIf { toolbarTouchStarted } else committedToolbarKey
674
+ if (fromPointer) { toolbarTouchStarted = false; toolbarTouchKey = null }
675
+ if (key == null || key != committedToolbarKey || key != selectedKey || committedToolbarEntry == null ||
676
+ !toolbarCanCommit() || !toolbar.isEnabled || toolbar.visibility != VISIBLE) return false
677
+ val location = IntArray(2)
678
+ toolbar.getLocationInWindow(location)
679
+ val density = resources.displayMetrics.density.toDouble()
680
+ onToolbarPress?.invoke(key, doubleArrayOf(location[0] / density, location[1] / density, toolbar.width / density, toolbar.height / density))
681
+ return true
682
+ }
683
+
684
+ override fun onAttachedToWindow() {
685
+ super.onAttachedToWindow()
686
+ toolbarPagerBusy = true
687
+ updateToolbar()
688
+ }
689
+
690
+ override fun onDetachedFromWindow() {
691
+ suspendToolbar()
692
+ toolbarPagerBusy = true
693
+ toolbarPageIndex = -1
694
+ toolbarTouchStarted = false
695
+ toolbarTouchKey = null
696
+ failedIconIdentity = ""
697
+ super.onDetachedFromWindow()
698
+ }
380
699
 
381
700
  var onItemPress: ((Int, String) -> Unit)?
382
701
  get() = row.onItemPress
@@ -386,12 +705,52 @@ internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(
386
705
 
387
706
  init {
388
707
  addView(row, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
708
+ addView(toolbar)
709
+ toolbar.scaleType = ImageView.ScaleType.CENTER_INSIDE
710
+ toolbar.setPadding(dp(6), dp(6), dp(6), dp(6))
711
+ toolbar.isClickable = true
712
+ toolbar.isFocusable = true
713
+ toolbar.visibility = INVISIBLE
714
+ toolbar.alpha = 0f
715
+ toolbar.isEnabled = false
716
+ toolbar.isClickable = false
717
+ toolbar.isFocusable = false
718
+ toolbar.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
719
+ toolbar.setOnTouchListener { _, event ->
720
+ when (event.actionMasked) {
721
+ MotionEvent.ACTION_DOWN -> { toolbarTouchStarted = true; toolbarTouchKey = committedToolbarKey.takeIf { toolbar.isEnabled && toolbarCanCommit() } }
722
+ MotionEvent.ACTION_CANCEL -> { toolbarTouchKey = null }
723
+ }
724
+ false
725
+ }
726
+ toolbar.setOnClickListener { performToolbarPress(true) }
727
+ toolbar.accessibilityDelegate = object : View.AccessibilityDelegate() {
728
+ override fun performAccessibilityAction(host: View, action: Int, args: android.os.Bundle?): Boolean {
729
+ if (action == android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) return performToolbarPress(false)
730
+ return super.performAccessibilityAction(host, action, args)
731
+ }
732
+ }
733
+ toolbar.setOnKeyListener { _, keyCode, event ->
734
+ if (keyCode != android.view.KeyEvent.KEYCODE_ENTER && keyCode != android.view.KeyEvent.KEYCODE_DPAD_CENTER && keyCode != android.view.KeyEvent.KEYCODE_SPACE) false
735
+ else if (event.action == android.view.KeyEvent.ACTION_DOWN) {
736
+ if (event.repeatCount == 0) toolbarKeyboardKey = committedToolbarKey.takeIf { toolbar.isEnabled && toolbarCanCommit() }
737
+ toolbar.isPressed = toolbarKeyboardKey != null
738
+ toolbarKeyboardKey != null
739
+ } else if (event.action == android.view.KeyEvent.ACTION_UP) {
740
+ val valid = toolbarKeyboardKey != null && toolbarKeyboardKey == committedToolbarKey
741
+ toolbarKeyboardKey = null
742
+ toolbar.isPressed = false
743
+ valid && performToolbarPress(false)
744
+ } else false
745
+ }
389
746
  visibility = GONE
390
747
  }
391
748
 
392
749
  fun updateItemsJSON(value: String?) {
750
+ items = parseHeaderItems(value)
393
751
  row.updateItemsJSON(value)
394
752
  visibility = row.visibility
753
+ updateConfigJSON(config.toString())
395
754
  }
396
755
 
397
756
  fun updateStyle(
@@ -425,15 +784,83 @@ internal class CollapsiblePagerNativeTabBarView(context: Context) : FrameLayout(
425
784
  activeTextColor: Int,
426
785
  inactiveTextColor: Int,
427
786
  indicatorColor: Int,
428
- ) = row.updateColors(
429
- backgroundColor,
430
- activeTextColor,
431
- inactiveTextColor,
432
- Color.TRANSPARENT,
433
- indicatorColor,
434
- )
787
+ ) {
788
+ val text = row.usesTextVariant()
789
+ setBackgroundColor(if (text) backgroundColor else Color.TRANSPARENT)
790
+ row.updateColors(
791
+ if (text) Color.TRANSPARENT else backgroundColor,
792
+ activeTextColor,
793
+ inactiveTextColor,
794
+ Color.TRANSPARENT,
795
+ indicatorColor,
796
+ )
797
+ }
798
+
799
+ private fun controlledPageIndices(): List<Int>? {
800
+ val pageKeys = config.optJSONArray("pageKeys") ?: return null
801
+ val names = List(pageKeys.length()) { pageKeys.optString(it) }
802
+ val indices = names.map { name -> items.indexOfFirst { it.key == name } }
803
+ if (names.isEmpty() || selectedKey !in names || names.toSet().size != names.size ||
804
+ indices.any { it < 0 } || items.map { it.key }.toSet().size != items.size) return null
805
+ return indices
806
+ }
807
+
808
+ fun usesControlledTextProgress(): Boolean {
809
+ return row.usesTextVariant() && config.optBoolean("controlled") && controlledPageIndices() != null
810
+ }
435
811
 
436
- fun setProgress(progress: Float) = row.setProgress(progress)
812
+ fun setProgress(progress: Float) {
813
+ lastNativeProgress = progress
814
+ hasNativeProgress = true
815
+ var mapped = progress
816
+ var source: Int? = null
817
+ var destination: Int? = null
818
+ var fraction = 0f
819
+ if (config.optBoolean("controlled")) {
820
+ val pageKeys = config.optJSONArray("pageKeys") ?: return
821
+ val indices = controlledPageIndices()
822
+ if (indices == null) {
823
+ val fallback = items.indexOfFirst { it.key == config.optString("selectedKey") }
824
+ if (fallback >= 0) { row.setProgress(fallback.toFloat()); selectedKey = items[fallback].key; updateToolbar() }
825
+ return
826
+ }
827
+ val bounded = progress.coerceIn(0f, (pageKeys.length() - 1).toFloat())
828
+ val lower = floor(bounded).toInt().coerceAtLeast(0)
829
+ val upper = min(lower + 1, pageKeys.length() - 1)
830
+ val from = indices[lower]
831
+ val to = indices[upper]
832
+ if (from < 0 || to < 0) return
833
+ fraction = bounded - lower
834
+ source = from
835
+ destination = to
836
+ mapped = from + (to - from) * fraction
837
+ }
838
+ row.setProgress(mapped, source, destination, fraction)
839
+ if (config.optBoolean("controlled")) return
840
+ val selectedIndex = mapped.roundToInt()
841
+ val nextKey = items.getOrNull(selectedIndex)?.key ?: return
842
+ if (nextKey != selectedKey) { toolbarTouchKey = null; toolbar.isPressed = false; selectedKey = nextKey; updateToolbar() }
843
+ }
844
+
845
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
846
+ val width = MeasureSpec.getSize(widthMeasureSpec)
847
+ val height = MeasureSpec.getSize(heightMeasureSpec)
848
+ val reserved = dp(toolbarReservedWidth)
849
+ row.measure(MeasureSpec.makeMeasureSpec(max(0, width - reserved), MeasureSpec.EXACTLY), heightMeasureSpec)
850
+ toolbar.measure(MeasureSpec.makeMeasureSpec(dp(toolbarSize), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(dp(toolbarSize), MeasureSpec.EXACTLY))
851
+ setMeasuredDimension(width, height)
852
+ }
853
+
854
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
855
+ val width = right - left
856
+ val height = bottom - top
857
+ val reserved = dp(toolbarReservedWidth)
858
+ val rtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
859
+ row.layout(if (rtl) reserved else 0, 0, if (rtl) width else width - reserved, height)
860
+ val inset = dp(toolbarTrailingInset) - (dp(toolbarSize) - dp(toolbarContentWidth)) / 2
861
+ val x = if (rtl) inset else width - inset - dp(toolbarSize)
862
+ toolbar.layout(x, (height - dp(toolbarSize)) / 2, x + dp(toolbarSize), (height + dp(toolbarSize)) / 2)
863
+ }
437
864
 
438
865
  fun preferredHeightPx(): Int = heightPx
439
866
  }
@@ -70,6 +70,10 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
70
70
  }
71
71
  }
72
72
  })
73
+ host.onNativeToolbarPress = { key, rect ->
74
+ dispatch(reactContext, host, NativeHeaderPressEvent(host.id, 0, key,
75
+ NativeHeaderPressEvent.TOOLBAR_EVENT_NAME, rect))
76
+ }
73
77
  host.onNativeTabPress = { position, key ->
74
78
  dispatch(
75
79
  reactContext,
@@ -227,6 +231,11 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
227
231
  view?.retainedPages = value ?: "[]"
228
232
  }
229
233
 
234
+ @ReactProp(name = "nativeTabBarConfig")
235
+ override fun setNativeTabBarConfig(view: CollapsiblePagerHost?, value: String?) {
236
+ view?.updateNativeTabBarConfig(value)
237
+ }
238
+
230
239
  @ReactProp(name = "nativeTabBarItems")
231
240
  override fun setNativeTabBarItems(view: CollapsiblePagerHost?, value: String?) {
232
241
  view?.updateNativeTabBarItems(value)
@@ -242,7 +251,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
242
251
  @ReactProp(name = "nativeTabBarVariant")
243
252
  override fun setNativeTabBarVariant(view: CollapsiblePagerHost?, value: String?) {
244
253
  view ?: return
245
- view.nativeTabBarVariant = if (value == "segmented") "segmented" else "underline"
254
+ view.nativeTabBarVariant = if (value == "segmented" || value == "text") value else "underline"
246
255
  view.applyNativeHeaderStyle()
247
256
  }
248
257
 
@@ -404,6 +413,7 @@ class CollapsiblePagerViewManager : ViewGroupManager<CollapsiblePagerHost>(),
404
413
  CollapsibleStateChangedEvent.EVENT_NAME,
405
414
  MapBuilder.of("registrationName", "onCollapsibleStateChanged"),
406
415
  )
416
+ .put(NativeHeaderPressEvent.TOOLBAR_EVENT_NAME, MapBuilder.of("registrationName", "onNativeToolbarPress"))
407
417
  .put(
408
418
  NativeHeaderPressEvent.TAB_EVENT_NAME,
409
419
  MapBuilder.of("registrationName", "onNativeTabPress"),
@@ -10,6 +10,7 @@ class NativeHeaderPressEvent(
10
10
  private val position: Int,
11
11
  private val key: String,
12
12
  private val name: String,
13
+ private val anchorRect: DoubleArray? = null,
13
14
  ) : Event<NativeHeaderPressEvent>(viewTag) {
14
15
  override fun getEventName() = name
15
16
 
@@ -22,9 +23,16 @@ class NativeHeaderPressEvent(
22
23
  private fun serializeEventData(): WritableMap = Arguments.createMap().apply {
23
24
  putInt("position", position)
24
25
  putString("key", key)
26
+ anchorRect?.let { rect ->
27
+ putMap("anchorRect", Arguments.createMap().apply {
28
+ putDouble("x", rect[0]); putDouble("y", rect[1])
29
+ putDouble("width", rect[2]); putDouble("height", rect[3])
30
+ })
31
+ }
25
32
  }
26
33
 
27
34
  companion object {
35
+ const val TOOLBAR_EVENT_NAME = "topNativeToolbarPress"
28
36
  const val TAB_EVENT_NAME = "topNativeTabPress"
29
37
  const val SUB_HEADER_EVENT_NAME = "topNativeSubHeaderPress"
30
38
  }