@onekeyfe/react-native-pager-view 6.7.0-22 → 6.7.0-24

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