@onekeyfe/react-native-native-list 3.0.114 → 3.0.116

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.
@@ -118,6 +118,7 @@ func nativeListIcon(named name: String) -> UIImage? {
118
118
  case "DragOutline": assetName = "onekey_drag"
119
119
  case "StarOutline": assetName = "onekey_star"
120
120
  case "StarSolid": assetName = "onekey_star_solid"
121
+ case "BadgeVerifiedSolid": assetName = "onekey_badge_verified_solid"
121
122
  case "ChevronGrabberVerOutline": assetName = "onekey_chevron_grabber_ver"
122
123
  case "ChevronBottomOutline": assetName = "onekey_chevron_bottom"
123
124
  case "ChevronTopOutline": assetName = "onekey_chevron_top"
@@ -131,3 +132,10 @@ func nativeListIcon(named name: String) -> UIImage? {
131
132
  return UIImage(named: assetName, in: NativeListResources.bundle, compatibleWith: nil)?
132
133
  .withRenderingMode(["GoogleIllus", "BotIllus", "AccountErrorCustom"].contains(name) ? .alwaysOriginal : .alwaysTemplate)
133
134
  }
135
+
136
+ func nativeListIcon(named name: String, size: CGSize) -> UIImage? {
137
+ guard let image = nativeListIcon(named: name) else { return nil }
138
+ return UIGraphicsImageRenderer(size: size).image { _ in
139
+ image.draw(in: CGRect(origin: .zero, size: size))
140
+ }.withRenderingMode(.alwaysTemplate)
141
+ }
@@ -64,7 +64,7 @@ final class NativeListView: UIView {
64
64
  private let footerContainer = UIView()
65
65
  private let footerCell = NativeListCell(frame: .zero)
66
66
  private let sectionIndexView = NativeListSectionIndexView()
67
- private let sectionIndexPreview = UILabel()
67
+ private let sectionIndexPreview = NativeListSectionIndexPreviewView()
68
68
  private var footerHeightConstraint: NSLayoutConstraint!
69
69
  private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
70
70
  private var config: NativeListConfig?
@@ -83,6 +83,10 @@ final class NativeListView: UIView {
83
83
  target: self,
84
84
  action: #selector(reorderLongPressChanged(_:))
85
85
  )
86
+ private lazy var marketLongPress = UILongPressGestureRecognizer(
87
+ target: self,
88
+ action: #selector(marketLongPressChanged(_:))
89
+ )
86
90
  // OneKey patch: claim only vertical drags so held rows and ancestor pagers stay responsive.
87
91
  private lazy var listBodyGestureGuard = UIPanGestureRecognizer(target: nil, action: nil)
88
92
  private var interactiveReorderSource: (key: String, index: Int)?
@@ -102,7 +106,8 @@ final class NativeListView: UIView {
102
106
 
103
107
  private static let sectionIndexContentInset: CGFloat = 16
104
108
  private static let sectionIndexRailWidth: CGFloat = 32
105
- private static let sectionIndexPreviewSize: CGFloat = 48
109
+ private static let sectionIndexPreviewWidth: CGFloat = 60
110
+ private static let sectionIndexPreviewHeight: CGFloat = 50
106
111
  private static let sectionIndexPreviewEndMargin: CGFloat = 40
107
112
 
108
113
  override init(frame: CGRect) {
@@ -122,6 +127,10 @@ final class NativeListView: UIView {
122
127
  reorderLongPress.allowableMovement = ReorderAnimation.allowableMovement
123
128
  reorderLongPress.delegate = self
124
129
  collectionView.addGestureRecognizer(reorderLongPress)
130
+ marketLongPress.minimumPressDuration = 0.8
131
+ marketLongPress.allowableMovement = 10
132
+ marketLongPress.delegate = self
133
+ collectionView.addGestureRecognizer(marketLongPress)
125
134
  interactiveReorderPlaceholder.isHidden = true
126
135
  interactiveReorderPlaceholder.isUserInteractionEnabled = false
127
136
  interactiveReorderPlaceholder.layer.cornerRadius = ReorderAnimation.placeholderRadius
@@ -158,8 +167,8 @@ final class NativeListView: UIView {
158
167
  constant: -Self.sectionIndexPreviewEndMargin
159
168
  ),
160
169
  sectionIndexPreview.centerYAnchor.constraint(equalTo: collectionView.centerYAnchor),
161
- sectionIndexPreview.widthAnchor.constraint(equalToConstant: Self.sectionIndexPreviewSize),
162
- sectionIndexPreview.heightAnchor.constraint(equalToConstant: Self.sectionIndexPreviewSize),
170
+ sectionIndexPreview.widthAnchor.constraint(equalToConstant: Self.sectionIndexPreviewWidth),
171
+ sectionIndexPreview.heightAnchor.constraint(equalToConstant: Self.sectionIndexPreviewHeight),
163
172
  ])
164
173
 
165
174
  sectionIndexView.isHidden = true
@@ -171,11 +180,6 @@ final class NativeListView: UIView {
171
180
  }
172
181
  sectionIndexPreview.isHidden = true
173
182
  sectionIndexPreview.alpha = 0
174
- sectionIndexPreview.layer.cornerRadius = 14
175
- sectionIndexPreview.layer.masksToBounds = true
176
- sectionIndexPreview.textAlignment = .center
177
- sectionIndexPreview.adjustsFontForContentSizeCategory = true
178
- sectionIndexPreview.font = nativeListFont(ofSize: 22, weight: .semibold)
179
183
  sectionIndexPreview.isAccessibilityElement = false
180
184
 
181
185
  footerCell.onAction = { [weak self] item, action, target, origin in
@@ -298,8 +302,13 @@ final class NativeListView: UIView {
298
302
  }
299
303
 
300
304
  var changedKeys: [String] = []
305
+ var marketQuoteKeys = Set<String>()
301
306
  var sizeChanged = false
307
+ let marketQuoteFields: Set<String> = [
308
+ "revision", "price", "priceSegments", "change", "accessibilityLabel",
309
+ ]
302
310
  for (index, changes) in pending {
311
+ let previous = current.items[index]
303
312
  var merged = current.items[index].data
304
313
  changes.forEach { key, value in
305
314
  if key != "key" && key != "type" { merged[key] = value }
@@ -307,14 +316,30 @@ final class NativeListView: UIView {
307
316
  guard let item = try? NativeListItem(data: merged) else { return }
308
317
  if rowHeight(current.items[index]) != rowHeight(item) { sizeChanged = true }
309
318
  current.items[index] = item
310
- changedKeys.append(item.key)
319
+ if previous.type == "market", Set(changes.keys).isSubset(of: marketQuoteFields) {
320
+ marketQuoteKeys.insert(item.key)
321
+ } else {
322
+ changedKeys.append(item.key)
323
+ }
311
324
  if let selected = changes["selected"] as? Bool {
312
325
  if selected { current.selectedKeys.insert(item.key) } else { current.selectedKeys.remove(item.key) }
313
326
  }
314
327
  }
315
- invalidateActionAnchor(reason: "snapshot")
328
+ if !changedKeys.isEmpty { invalidateActionAnchor(reason: "snapshot") }
316
329
  config = current
317
330
  itemsByKey = Dictionary(uniqueKeysWithValues: current.items.map { ($0.key, $0) })
331
+ for indexPath in collectionView.indexPathsForVisibleItems {
332
+ guard let item = current.items[safe: indexPath.item],
333
+ marketQuoteKeys.contains(item.key),
334
+ let cell = collectionView.cellForItem(at: indexPath) as? NativeListCell else { continue }
335
+ cell.updateMarketQuote(item, theme: current.theme)
336
+ }
337
+ if changedKeys.isEmpty {
338
+ configureFooter(current)
339
+ emitVisibleRangeIfNeeded()
340
+ checkEndReached()
341
+ return
342
+ }
318
343
  var snapshot = dataSource.snapshot()
319
344
  snapshot.reconfigureItems(changedKeys.filter { snapshot.indexOfItem($0) != nil })
320
345
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
@@ -871,18 +896,21 @@ final class NativeListView: UIView {
871
896
  sectionIndexHapticsEnabled = config.sectionIndexHapticsEnabled
872
897
  sectionIndexView.configure(
873
898
  titles: sectionIndexEntries.map(\.title),
874
- textColor: nativeListColor(config.theme, "secondaryText", "#646464"),
875
- activeColor: nativeListColor(config.theme, "accent", "#108303"),
899
+ textColor: nativeListColor(config.theme, "disabledText", "#8D8D8D"),
900
+ activeColor: nativeListColor(config.theme, "positive", "#218358"),
876
901
  activeTextColor: nativeListColor(config.theme, "inverseText", "#FCFCFC"),
877
902
  centeredInWindow: config.sectionIndexCenteredInWindow
878
903
  )
879
904
  sectionIndexView.isHidden = sectionIndexEntries.isEmpty
880
- sectionIndexPreview.backgroundColor = nativeListColor(
881
- config.theme,
882
- "inverseBackground",
883
- "#202020"
905
+ sectionIndexPreview.configure(
906
+ fillColor: UIColor(
907
+ red: 202.0 / 255.0,
908
+ green: 202.0 / 255.0,
909
+ blue: 202.0 / 255.0,
910
+ alpha: 1
911
+ ),
912
+ textColor: .white
884
913
  )
885
- sectionIndexPreview.textColor = nativeListColor(config.theme, "inverseText", "#FCFCFC")
886
914
  if let previousKey,
887
915
  let index = sectionIndexEntries.firstIndex(where: { $0.key == previousKey }) {
888
916
  sectionIndexView.setActiveIndex(index)
@@ -901,6 +929,7 @@ final class NativeListView: UIView {
901
929
  if interacting {
902
930
  sectionIndexPreview.layer.removeAllAnimations()
903
931
  sectionIndexPreview.text = entry.title
932
+ positionSectionIndexPreview(at: sectionIndexView.centerY(for: index))
904
933
  sectionIndexPreview.isHidden = false
905
934
  sectionIndexPreview.alpha = 1
906
935
  if changed && sectionIndexHapticsEnabled {
@@ -929,6 +958,23 @@ final class NativeListView: UIView {
929
958
  }
930
959
  }
931
960
 
961
+ private func positionSectionIndexPreview(at sectionIndexY: CGFloat) {
962
+ layoutIfNeeded()
963
+ let targetY = sectionIndexView.convert(
964
+ CGPoint(x: sectionIndexView.bounds.midX, y: sectionIndexY),
965
+ to: self
966
+ ).y
967
+ let safeFrame = safeAreaLayoutGuide.layoutFrame
968
+ let halfHeight = Self.sectionIndexPreviewHeight / 2
969
+ let minimumY = safeFrame.minY + halfHeight
970
+ let maximumY = max(minimumY, safeFrame.maxY - halfHeight)
971
+ let clampedY = targetY.clamped(to: minimumY...maximumY)
972
+ sectionIndexPreview.transform = CGAffineTransform(
973
+ translationX: 0,
974
+ y: clampedY - sectionIndexPreview.center.y
975
+ )
976
+ }
977
+
932
978
  private func syncSectionIndexToVisibleRows() {
933
979
  guard !sectionIndexScrubbing, !sectionIndexEntries.isEmpty else { return }
934
980
  let firstVisible = collectionView.indexPathsForVisibleItems.map(\.item).min() ?? 0
@@ -985,7 +1031,9 @@ final class NativeListView: UIView {
985
1031
  updateSelection(target: NativeSelectionTarget(scope: "row", key: item.key), sourceKey: item.key)
986
1032
  return
987
1033
  }
988
- if item.type == "action" {
1034
+ if item.type == "market", !item.data.string("pressActionKey").isEmpty {
1035
+ emit(onRowAction, rowActionPayload(item: item, actionKey: item.data.string("pressActionKey"), origin: origin))
1036
+ } else if item.type == "action" {
989
1037
  emit(onRowAction, rowActionPayload(item: item, actionKey: item.data.string("actionKey"), origin: origin))
990
1038
  } else if item.type == "system", item.data.string("variant") == "retry" {
991
1039
  emit(onRowAction, rowActionPayload(item: item, actionKey: item.data.string("actionKey"), origin: origin))
@@ -994,6 +1042,20 @@ final class NativeListView: UIView {
994
1042
  }
995
1043
  }
996
1044
 
1045
+ @objc private func marketLongPressChanged(_ gesture: UILongPressGestureRecognizer) {
1046
+ guard gesture.state == .began else { return }
1047
+ let point = gesture.location(in: collectionView)
1048
+ guard let indexPath = collectionView.indexPathForItem(at: point),
1049
+ let item = config?.items[safe: indexPath.item],
1050
+ item.type == "market",
1051
+ !item.data.bool("disabled") else { return }
1052
+ let actionKey = item.data.string("longPressActionKey")
1053
+ guard !actionKey.isEmpty else { return }
1054
+ let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
1055
+ origin?.windowPoint = gesture.location(in: window)
1056
+ handleAction(item: item, actionKey: actionKey, target: nil, origin: origin)
1057
+ }
1058
+
997
1059
  private func handleAction(
998
1060
  item: NativeListItem,
999
1061
  actionKey: String,
@@ -1267,10 +1329,18 @@ final class NativeListView: UIView {
1267
1329
  ? 56
1268
1330
  : config?.layout == "linear" ? 30 : 36
1269
1331
  case "system":
1270
- switch item.data.string("variant") {
1271
- case "noMatch", "end": base = 36
1272
- case "retry": base = 44
1273
- default: base = 56
1332
+ if item.data.string("variant") == "loading" && item.data.string("loadingStyle") == "skeleton" {
1333
+ base = 56
1334
+ } else if item.data.string("variant") == "loading" && item.data.string("loadingStyle") == "spinner" {
1335
+ base = 52
1336
+ } else if item.data.string("presentation") == "market" {
1337
+ base = item.data.string("variant") == "loading" ? 68 : 44
1338
+ } else {
1339
+ switch item.data.string("variant") {
1340
+ case "noMatch", "end": base = 36
1341
+ case "retry": base = 44
1342
+ default: base = 56
1343
+ }
1274
1344
  }
1275
1345
  case "action":
1276
1346
  base = item.data.string("presentation") == "accountSelector"
@@ -1280,6 +1350,14 @@ final class NativeListView: UIView {
1280
1350
  base = item.data.dictionaries("columns").contains {
1281
1351
  !$0.string("secondaryText").isEmpty
1282
1352
  } ? 60 : 56
1353
+ case "market":
1354
+ let style = item.data.dictionary("style")
1355
+ let imageHeight = CGFloat(style?.dictionary("image")?.double(
1356
+ "height",
1357
+ default: item.data.string("variant") == "stock" ? 40 : 32
1358
+ ) ?? (item.data.string("variant") == "stock" ? 40 : 32))
1359
+ let verticalPadding = CGFloat(style?.double("verticalPadding", default: 12) ?? 12)
1360
+ base = max(item.data.string("variant") == "stock" ? 72 : 68, imageHeight + verticalPadding * 2)
1283
1361
  default:
1284
1362
  if item.type == "identity", !item.data.string("tertiary").isEmpty {
1285
1363
  base = 72
@@ -1401,6 +1479,9 @@ final class NativeListView: UIView {
1401
1479
  ? "rtl"
1402
1480
  : "ltr",
1403
1481
  ]
1482
+ if let point = origin.windowPoint {
1483
+ anchor["windowPoint"] = ["x": point.x, "y": point.y]
1484
+ }
1404
1485
  if let slot = origin.slot { anchor["slot"] = slot }
1405
1486
  return anchor
1406
1487
  }
@@ -1490,6 +1571,16 @@ final class NativeListView: UIView {
1490
1571
  }
1491
1572
 
1492
1573
  override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
1574
+ if gestureRecognizer === reorderLongPress || gestureRecognizer === marketLongPress {
1575
+ let point = gestureRecognizer.location(in: collectionView)
1576
+ guard let indexPath = collectionView.indexPathForItem(at: point),
1577
+ let item = item(at: indexPath),
1578
+ !item.data.bool("disabled") else { return false }
1579
+ if gestureRecognizer === reorderLongPress {
1580
+ return config?.reorderable == true && item.isReorderable
1581
+ }
1582
+ return item.type == "market" && !item.data.string("longPressActionKey").isEmpty
1583
+ }
1493
1584
  guard gestureRecognizer === listBodyGestureGuard,
1494
1585
  let pan = gestureRecognizer as? UIPanGestureRecognizer else { return true }
1495
1586
  let velocity = pan.velocity(in: collectionView)
@@ -1608,6 +1699,14 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
1608
1699
  return !item.data.bool("disabled")
1609
1700
  }
1610
1701
 
1702
+ func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
1703
+ guard let item = config?.items[safe: indexPath.item], item.type == "market" else { return }
1704
+ let actionKey = item.data.string("pressInActionKey")
1705
+ guard !actionKey.isEmpty else { return }
1706
+ let origin = (collectionView.cellForItem(at: indexPath) as? NativeListCell)?.rowActionOrigin()
1707
+ handleAction(item: item, actionKey: actionKey, target: nil, origin: origin)
1708
+ }
1709
+
1611
1710
  func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
1612
1711
  guard let item = config?.items[safe: indexPath.item] else { return false }
1613
1712
  return !item.data.bool("disabled")
@@ -1748,9 +1847,81 @@ private struct NativeListSectionIndexEntry {
1748
1847
  let position: Int
1749
1848
  }
1750
1849
 
1850
+ private final class NativeListSectionIndexPreviewView: UIView {
1851
+ private let shapeLayer = CAShapeLayer()
1852
+ private let label = UILabel()
1853
+
1854
+ var text: String? {
1855
+ get { label.text }
1856
+ set { label.text = newValue }
1857
+ }
1858
+
1859
+ override init(frame: CGRect) {
1860
+ super.init(frame: frame)
1861
+ isUserInteractionEnabled = false
1862
+ backgroundColor = .clear
1863
+ layer.addSublayer(shapeLayer)
1864
+ label.textAlignment = .center
1865
+ label.adjustsFontForContentSizeCategory = true
1866
+ label.font = nativeListFont(ofSize: 30)
1867
+ label.isAccessibilityElement = false
1868
+ addSubview(label)
1869
+ }
1870
+
1871
+ required init?(coder: NSCoder) {
1872
+ fatalError("init(coder:) has not been implemented")
1873
+ }
1874
+
1875
+ func configure(fillColor: UIColor, textColor: UIColor) {
1876
+ shapeLayer.fillColor = fillColor.cgColor
1877
+ label.textColor = textColor
1878
+ }
1879
+
1880
+ override func layoutSubviews() {
1881
+ super.layoutSubviews()
1882
+ let height = bounds.height
1883
+ let bodyWidth = min(height, max(0, bounds.width - 10))
1884
+ let midY = height / 2
1885
+ let bodyMidX = bodyWidth / 2
1886
+ let shoulderX = bodyWidth * 0.92
1887
+ let path = UIBezierPath()
1888
+ path.move(to: CGPoint(x: bodyMidX, y: 0))
1889
+ path.addCurve(
1890
+ to: CGPoint(x: 0, y: midY),
1891
+ controlPoint1: CGPoint(x: bodyWidth * 0.22, y: 0),
1892
+ controlPoint2: CGPoint(x: 0, y: height * 0.22)
1893
+ )
1894
+ path.addCurve(
1895
+ to: CGPoint(x: bodyMidX, y: height),
1896
+ controlPoint1: CGPoint(x: 0, y: height * 0.78),
1897
+ controlPoint2: CGPoint(x: bodyWidth * 0.22, y: height)
1898
+ )
1899
+ path.addCurve(
1900
+ to: CGPoint(x: shoulderX, y: height * 0.75),
1901
+ controlPoint1: CGPoint(x: bodyWidth * 0.72, y: height),
1902
+ controlPoint2: CGPoint(x: bodyWidth * 0.86, y: height * 0.88)
1903
+ )
1904
+ path.addLine(to: CGPoint(x: bounds.width, y: midY))
1905
+ path.addLine(to: CGPoint(x: shoulderX, y: height * 0.25))
1906
+ path.addCurve(
1907
+ to: CGPoint(x: bodyMidX, y: 0),
1908
+ controlPoint1: CGPoint(x: bodyWidth * 0.86, y: height * 0.12),
1909
+ controlPoint2: CGPoint(x: bodyWidth * 0.72, y: 0)
1910
+ )
1911
+ path.close()
1912
+ shapeLayer.frame = bounds
1913
+ shapeLayer.path = path.cgPath
1914
+ label.frame = CGRect(x: 0, y: 0, width: bodyWidth, height: height)
1915
+ }
1916
+ }
1917
+
1751
1918
  // OneKey patch: arbitrate index scrubbing against ancestor dismissal gestures.
1752
1919
  // private final class NativeListSectionIndexView: UIControl {
1753
1920
  private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDelegate {
1921
+ private static let edgePadding: CGFloat = 8
1922
+ private static let labelSize: CGFloat = 14
1923
+ private static let labelSpacing: CGFloat = 16
1924
+
1754
1925
  var onSelect: ((Int, Bool) -> Void)?
1755
1926
  var onInteractionEnded: (() -> Void)?
1756
1927
  private var titles: [String] = []
@@ -1833,16 +2004,28 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1833
2004
  override func layoutSubviews() {
1834
2005
  super.layoutSubviews()
1835
2006
  guard !labels.isEmpty else { return }
1836
- let height = min(16, max(8, bounds.height / CGFloat(labels.count)))
1837
- let originY = indexOriginY(cellHeight: height, count: labels.count)
1838
- for (index, label) in labels.enumerated() {
2007
+ let metrics = indexMetrics(count: labels.count)
2008
+ let visibleIndices = visibleLabelIndices(
2009
+ count: labels.count,
2010
+ trackHeight: metrics.trackHeight
2011
+ )
2012
+ labels.forEach { $0.isHidden = true }
2013
+ for (visibleIndex, index) in visibleIndices.sorted().enumerated() {
2014
+ let label = labels[index]
2015
+ label.isHidden = false
2016
+ let centerY = visibleLabelCenterY(
2017
+ visibleIndex: visibleIndex,
2018
+ visibleCount: visibleIndices.count,
2019
+ metrics: metrics
2020
+ )
1839
2021
  label.frame = CGRect(
1840
- x: 6,
1841
- y: originY + CGFloat(index) * height,
1842
- width: 20,
1843
- height: height
2022
+ x: bounds.width - Self.labelSize - 3,
2023
+ y: centerY - Self.labelSize / 2,
2024
+ width: Self.labelSize,
2025
+ height: Self.labelSize
1844
2026
  )
1845
2027
  }
2028
+ updateLabelStyles()
1846
2029
  }
1847
2030
 
1848
2031
  override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
@@ -1879,25 +2062,75 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1879
2062
  }
1880
2063
 
1881
2064
  private func select(at y: CGFloat, interacting: Bool) {
1882
- let height = min(16, max(8, bounds.height / CGFloat(titles.count)))
1883
- let originY = indexOriginY(cellHeight: height, count: titles.count)
1884
- let index = Int(floor((y - originY) / height)).clamped(to: 0...(titles.count - 1))
2065
+ let metrics = indexMetrics(count: titles.count)
2066
+ guard metrics.trackHeight > 0 else { return }
2067
+ let visibleIndices = visibleLabelIndices(
2068
+ count: titles.count,
2069
+ trackHeight: metrics.trackHeight
2070
+ ).sorted()
2071
+ let visibleTrackHeight = min(
2072
+ metrics.trackHeight,
2073
+ Self.labelSpacing * CGFloat(visibleIndices.count)
2074
+ )
2075
+ let visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2
2076
+ let progress = ((y - visibleOriginY) / visibleTrackHeight).clamped(to: 0...1)
2077
+ let slot = Int(floor(progress * CGFloat(visibleIndices.count)))
2078
+ .clamped(to: 0...(visibleIndices.count - 1))
2079
+ let index = visibleIndices[slot]
1885
2080
  if interacting && lastTouchIndex == index { return }
1886
2081
  lastTouchIndex = interacting ? index : nil
1887
2082
  select(index: index, interacting: interacting)
1888
2083
  }
1889
2084
 
1890
- private func indexOriginY(cellHeight: CGFloat, count: Int) -> CGFloat {
1891
- let totalHeight = cellHeight * CGFloat(count)
1892
- let centeredOriginY = (bounds.height - totalHeight) / 2
1893
- guard centeredInWindow, let window else { return centeredOriginY }
2085
+ func centerY(for index: Int) -> CGFloat {
2086
+ entryCenterY(index: index, metrics: indexMetrics(count: titles.count))
2087
+ }
2088
+
2089
+ private func indexMetrics(count: Int) -> (originY: CGFloat, trackHeight: CGFloat) {
2090
+ guard count > 0 else { return (bounds.midY, 0) }
2091
+ let availableHeight = max(0, bounds.height - Self.edgePadding * 2)
2092
+ let trackHeight = min(availableHeight, Self.labelSpacing * CGFloat(count))
2093
+ let centeredOriginY = (bounds.height - trackHeight) / 2
2094
+ guard centeredInWindow, let window else { return (centeredOriginY, trackHeight) }
1894
2095
  let windowCenterY = window.safeAreaLayoutGuide.layoutFrame.midY
1895
2096
  let localCenterY = convert(CGPoint(x: 0, y: windowCenterY), from: window).y
1896
- return (localCenterY - totalHeight / 2).clamped(
1897
- to: 0...max(0, bounds.height - totalHeight)
2097
+ return (
2098
+ (localCenterY - trackHeight / 2).clamped(
2099
+ to: Self.edgePadding...max(Self.edgePadding, bounds.height - Self.edgePadding - trackHeight)
2100
+ ),
2101
+ trackHeight
1898
2102
  )
1899
2103
  }
1900
2104
 
2105
+ private func entryCenterY(
2106
+ index: Int,
2107
+ metrics: (originY: CGFloat, trackHeight: CGFloat)
2108
+ ) -> CGFloat {
2109
+ guard !titles.isEmpty else { return bounds.midY }
2110
+ return metrics.originY + metrics.trackHeight * (CGFloat(index) + 0.5) / CGFloat(titles.count)
2111
+ }
2112
+
2113
+ private func visibleLabelCenterY(
2114
+ visibleIndex: Int,
2115
+ visibleCount: Int,
2116
+ metrics: (originY: CGFloat, trackHeight: CGFloat)
2117
+ ) -> CGFloat {
2118
+ let visibleTrackHeight = min(metrics.trackHeight, Self.labelSpacing * CGFloat(visibleCount))
2119
+ let visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2
2120
+ return visibleOriginY
2121
+ + visibleTrackHeight * (CGFloat(visibleIndex) + 0.5) / CGFloat(max(visibleCount, 1))
2122
+ }
2123
+
2124
+ private func visibleLabelIndices(count: Int, trackHeight: CGFloat) -> Set<Int> {
2125
+ guard count > 0 else { return [] }
2126
+ let maxVisible = max(1, Int(floor(trackHeight / Self.labelSpacing)))
2127
+ guard count > maxVisible else { return Set(0..<count) }
2128
+ guard maxVisible > 1 else { return [0] }
2129
+ return Set((0..<maxVisible).map { slot in
2130
+ Int(round(CGFloat(slot * (count - 1)) / CGFloat(maxVisible - 1)))
2131
+ })
2132
+ }
2133
+
1901
2134
  private func select(index: Int, interacting: Bool) {
1902
2135
  onSelect?(index, interacting)
1903
2136
  setActiveIndex(index)
@@ -1905,12 +2138,12 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1905
2138
 
1906
2139
  private func updateLabelStyles() {
1907
2140
  for (index, label) in labels.enumerated() {
1908
- let active = index == activeIndex
2141
+ let active = index == activeIndex && !label.isHidden
1909
2142
  label.textColor = active ? activeTextColor : textColor
1910
2143
  label.backgroundColor = active ? activeColor : .clear
1911
- label.layer.cornerRadius = 8
2144
+ label.layer.cornerRadius = Self.labelSize / 2
1912
2145
  label.layer.masksToBounds = active
1913
- label.font = nativeListFont(ofSize: 10, weight: active ? .semibold : .medium)
2146
+ label.font = nativeListFont(ofSize: 10, weight: active ? .medium : .regular)
1914
2147
  }
1915
2148
  }
1916
2149
  }
@@ -0,0 +1 @@
1
+ {"images":[{"filename":"icon.svg","idiom":"universal"}],"info":{"author":"xcode","version":1},"properties":{"preserves-vector-representation":true,"template-rendering-intent":"template"}}
@@ -0,0 +1 @@
1
+ <svg fill="#000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.483 11.458v3.5h-1v-3.5z M10.467 2.698a2.03 2.03 0 0 1 3.065 0l1.358 1.564a.03.03 0 0 0 .028.01l2.046-.325a2.03 2.03 0 0 1 2.347 1.971l.037 2.07q0 .016.014.026l1.776 1.066a2.03 2.03 0 0 1 .532 3.019l-1.304 1.609a.03.03 0 0 0-.005.03l.675 1.956a2.03 2.03 0 0 1-1.533 2.656l-2.033.394a.03.03 0 0 0-.023.019l-.741 1.933a2.03 2.03 0 0 1-2.88 1.05l-1.811-1.006a.03.03 0 0 0-.03 0l-1.811 1.005a2.03 2.03 0 0 1-2.88-1.049l-.742-1.933a.03.03 0 0 0-.023-.019l-2.033-.394a2.03 2.03 0 0 1-1.532-2.656l.675-1.957a.03.03 0 0 0-.005-.029l-1.304-1.61a2.03 2.03 0 0 1 .532-3.018l1.776-1.066a.03.03 0 0 0 .014-.026l.035-2.07a2.03 2.03 0 0 1 2.349-1.97l2.045.324a.03.03 0 0 0 .028-.01zm1.516 3.76a.5.5 0 0 0-.447.276l-1.861 3.724H8.483a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h6.692a2 2 0 0 0 1.981-1.73l.341-2.5a2 2 0 0 0-1.982-2.27h-1.939l.197-1.269a1.5 1.5 0 0 0-1.481-1.731z"/></svg>