@onekeyfe/react-native-native-list 3.0.107 → 3.0.109

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.
@@ -15,6 +15,7 @@ internal class NativeListAdapter(
15
15
  private val context: ThemedReactContext,
16
16
  ) : RecyclerView.Adapter<NativeListViewHolder>() {
17
17
  private var suppressDifferUpdates = false
18
+ private var needsThemeRebind = false
18
19
  private var reorderItems: MutableList<NativeListItem>? = null
19
20
  private val differ = AsyncListDiffer(
20
21
  object : ListUpdateCallback {
@@ -43,6 +44,10 @@ internal class NativeListAdapter(
43
44
  )
44
45
  var usesSelectorSourceScale = false
45
46
  var theme: JSONObject? = null
47
+ set(value) {
48
+ if (field?.toString() != value?.toString()) needsThemeRebind = true
49
+ field = value
50
+ }
46
51
  var layout: String = "linear"
47
52
  var orientation: String = "vertical"
48
53
  var selectedKeys: Set<String> = emptySet()
@@ -117,11 +122,20 @@ internal class NativeListAdapter(
117
122
  (reorderItems ?: differ.currentList).indexOfFirst { it.key == key }
118
123
 
119
124
  fun submitList(items: List<NativeListItem>, commitCallback: (() -> Unit)? = null) {
125
+ // A newer submission can discard the reorder diff and its completion callback.
126
+ suppressDifferUpdates = false
120
127
  if (reorderItems != null) {
121
128
  reorderItems = null
122
129
  notifyDataSetChanged()
123
130
  }
124
- differ.submitList(items.toList()) { commitCallback?.invoke() }
131
+ differ.submitList(items.toList()) {
132
+ // Retain this invalidation across superseded submissions, even when rows are unchanged.
133
+ if (needsThemeRebind) {
134
+ needsThemeRebind = false
135
+ notifyItemRangeChanged(0, itemCount)
136
+ }
137
+ commitCallback?.invoke()
138
+ }
125
139
  }
126
140
 
127
141
  fun moveReordered(from: Int, to: Int): List<NativeListItem>? {
@@ -144,6 +158,7 @@ internal class NativeListAdapter(
144
158
  }
145
159
 
146
160
  fun cancelReorder() {
161
+ suppressDifferUpdates = false
147
162
  if (reorderItems == null) return
148
163
  reorderItems = null
149
164
  notifyDataSetChanged()
@@ -79,6 +79,7 @@ internal data class NativeListConfig(
79
79
  val endReachedThreshold: Double,
80
80
  val sectionIndexEnabled: Boolean,
81
81
  val sectionIndexHapticsEnabled: Boolean,
82
+ val sectionIndexCenteredInWindow: Boolean,
82
83
  val theme: JSONObject?,
83
84
  val fixedFooter: NativeListItem?,
84
85
  val items: List<NativeListItem>,
@@ -127,6 +128,7 @@ internal data class NativeListConfig(
127
128
  endReachedThreshold = capabilities?.optDouble("endReachedThreshold", 0.2) ?: 0.2,
128
129
  sectionIndexEnabled = sectionIndex?.optBoolean("enabled", false) ?: false,
129
130
  sectionIndexHapticsEnabled = sectionIndex?.optBoolean("hapticsEnabled", true) ?: true,
131
+ sectionIndexCenteredInWindow = sectionIndex?.optBoolean("centeredInWindow", false) ?: false,
130
132
  theme = root.optJSONObject("theme"),
131
133
  fixedFooter = fixedFooter,
132
134
  items = items,
@@ -21,6 +21,8 @@ import android.widget.FrameLayout
21
21
  import android.widget.LinearLayout
22
22
  import android.widget.SeekBar
23
23
  import android.widget.TextView
24
+ import androidx.core.view.ViewCompat
25
+ import androidx.core.view.WindowInsetsCompat
24
26
  import androidx.recyclerview.widget.GridLayoutManager
25
27
  import androidx.recyclerview.widget.ItemTouchHelper
26
28
  import androidx.recyclerview.widget.LinearLayoutManager
@@ -371,6 +373,7 @@ class NativeListView(
371
373
  previous.endReachedThreshold != next.endReachedThreshold ||
372
374
  previous.sectionIndexEnabled != next.sectionIndexEnabled ||
373
375
  previous.sectionIndexHapticsEnabled != next.sectionIndexHapticsEnabled ||
376
+ previous.sectionIndexCenteredInWindow != next.sectionIndexCenteredInWindow ||
374
377
  previous.theme?.toString() != next.theme?.toString() ||
375
378
  previous.fixedFooter?.content != next.fixedFooter?.content ||
376
379
  previous.items.size != next.items.size
@@ -907,6 +910,7 @@ class NativeListView(
907
910
  themeColor(next.theme, "secondaryText", "#646464"),
908
911
  themeColor(next.theme, "accent", "#108303"),
909
912
  themeColor(next.theme, "inverseText", "#FCFCFC"),
913
+ next.sectionIndexCenteredInWindow,
910
914
  )
911
915
  sectionIndexView.visibility = if (sectionIndexEntries.isEmpty()) GONE else VISIBLE
912
916
  sectionIndexPreview.setTextColor(themeColor(next.theme, "inverseText", "#FCFCFC"))
@@ -1345,6 +1349,7 @@ class NativeListView(
1345
1349
  val from = dragFrom
1346
1350
  val to = dragTo
1347
1351
  val reordered = pendingReorder
1352
+ var reorderPayload: JSONObject? = null
1348
1353
  val destinationPosition = if (to != RecyclerView.NO_POSITION) {
1349
1354
  to
1350
1355
  } else {
@@ -1384,12 +1389,12 @@ class NativeListView(
1384
1389
  .put("toIndex", to)
1385
1390
  reordered.getOrNull(to - 1)?.let { payload.put("beforeKey", it.key) }
1386
1391
  reordered.getOrNull(to + 1)?.let { payload.put("afterKey", it.key) }
1392
+ reorderPayload = payload
1387
1393
  adapter.commitReordered(reordered) {
1388
1394
  finishCompactWalletGroup()
1389
1395
  if (!wasCompactWalletGroupDrag) {
1390
1396
  recyclerView.postOnAnimation(::relayoutRecyclerViewImmediately)
1391
1397
  }
1392
- emit(REORDER, payload)
1393
1398
  }
1394
1399
  }
1395
1400
  } else {
@@ -1399,6 +1404,8 @@ class NativeListView(
1399
1404
  dragFrom = RecyclerView.NO_POSITION
1400
1405
  dragTo = RecyclerView.NO_POSITION
1401
1406
  pendingReorder = null
1407
+ // The gesture is committed now; a later snapshot may supersede its async diff.
1408
+ reorderPayload?.let { emit(REORDER, it) }
1402
1409
  }
1403
1410
  }
1404
1411
  itemTouchHelper = ItemTouchHelper(callback).also { it.attachToRecyclerView(recyclerView) }
@@ -1701,7 +1708,10 @@ private class NativeListSectionIndexView(
1701
1708
  private var normalColor = Color.GRAY
1702
1709
  private var activeColor = Color.BLACK
1703
1710
  private var activeTextColor = Color.WHITE
1711
+ private var centeredInWindow = false
1704
1712
  private var lastTouchIndex: Int? = null
1713
+ private val rootLocationOnScreen = IntArray(2)
1714
+ private val locationOnScreen = IntArray(2)
1705
1715
  private val activeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
1706
1716
  private val normalPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1707
1717
  textAlign = Paint.Align.CENTER
@@ -1724,11 +1734,13 @@ private class NativeListSectionIndexView(
1724
1734
  normalColor: Int,
1725
1735
  activeColor: Int,
1726
1736
  activeTextColor: Int,
1737
+ centeredInWindow: Boolean,
1727
1738
  ) {
1728
1739
  this.titles = titles
1729
1740
  this.normalColor = normalColor
1730
1741
  this.activeColor = activeColor
1731
1742
  this.activeTextColor = activeTextColor
1743
+ this.centeredInWindow = centeredInWindow
1732
1744
  activeIndex = null
1733
1745
  updateContentDescription()
1734
1746
  invalidate()
@@ -1745,7 +1757,7 @@ private class NativeListSectionIndexView(
1745
1757
  super.onDraw(canvas)
1746
1758
  if (titles.isEmpty()) return
1747
1759
  val cellHeight = cellHeight()
1748
- val originY = (height - cellHeight * titles.size) / 2f
1760
+ val originY = indexOriginY(cellHeight, titles.size)
1749
1761
  val textSize = NativeListScale.font(resources, 10f) * resources.displayMetrics.scaledDensity
1750
1762
  normalPaint.color = normalColor
1751
1763
  normalPaint.textSize = textSize
@@ -1853,7 +1865,7 @@ private class NativeListSectionIndexView(
1853
1865
 
1854
1866
  private fun selectAt(y: Float, interacting: Boolean) {
1855
1867
  val cellHeight = cellHeight()
1856
- val originY = (height - cellHeight * titles.size) / 2f
1868
+ val originY = indexOriginY(cellHeight, titles.size)
1857
1869
  val index = ((y - originY) / cellHeight).toInt().coerceIn(0, titles.lastIndex)
1858
1870
  if (interacting && lastTouchIndex == index) return
1859
1871
  lastTouchIndex = index.takeIf { interacting }
@@ -1870,6 +1882,24 @@ private class NativeListSectionIndexView(
1870
1882
  .coerceAtMost(NativeListScale.dp(resources, 16f))
1871
1883
  .coerceAtLeast(1f)
1872
1884
 
1885
+ private fun indexOriginY(cellHeight: Float, count: Int): Float {
1886
+ val totalHeight = cellHeight * count
1887
+ val centeredOriginY = (height - totalHeight) / 2f
1888
+ if (!centeredInWindow || !isAttachedToWindow) return centeredOriginY
1889
+ val systemBarInsets = ViewCompat.getRootWindowInsets(rootView)
1890
+ ?.getInsetsIgnoringVisibility(
1891
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
1892
+ ) ?: return centeredOriginY
1893
+ rootView.getLocationOnScreen(rootLocationOnScreen)
1894
+ getLocationOnScreen(locationOnScreen)
1895
+ val safeTop = rootLocationOnScreen[1] + systemBarInsets.top
1896
+ val safeBottom = rootLocationOnScreen[1] + rootView.height - systemBarInsets.bottom
1897
+ if (safeBottom <= safeTop) return centeredOriginY
1898
+ val localCenterY = (safeTop + safeBottom) / 2f - locationOnScreen[1]
1899
+ return (localCenterY - totalHeight / 2f)
1900
+ .coerceIn(0f, (height - totalHeight).coerceAtLeast(0f))
1901
+ }
1902
+
1873
1903
  private fun updateContentDescription() {
1874
1904
  contentDescription = activeIndex?.let { titles.getOrNull(it) }
1875
1905
  ?.let { "$ACCESSIBILITY_LABEL, $it" }
@@ -62,6 +62,7 @@ struct NativeListConfig {
62
62
  let endReachedThreshold: Double
63
63
  let sectionIndexEnabled: Bool
64
64
  let sectionIndexHapticsEnabled: Bool
65
+ let sectionIndexCenteredInWindow: Bool
65
66
  let theme: [String: Any]?
66
67
  let fixedFooter: NativeListItem?
67
68
  var items: [NativeListItem]
@@ -123,6 +124,7 @@ struct NativeListConfig {
123
124
  endReachedThreshold: capabilities?["endReachedThreshold"] as? Double ?? 0.2,
124
125
  sectionIndexEnabled: sectionIndex?["enabled"] as? Bool ?? false,
125
126
  sectionIndexHapticsEnabled: sectionIndex?["hapticsEnabled"] as? Bool ?? true,
127
+ sectionIndexCenteredInWindow: sectionIndex?["centeredInWindow"] as? Bool ?? false,
126
128
  theme: root["theme"] as? [String: Any],
127
129
  fixedFooter: footer,
128
130
  items: items
@@ -253,6 +253,7 @@ final class NativeListView: UIView {
253
253
  return
254
254
  }
255
255
  let oldItems = itemsByKey
256
+ let themeChanged = !dictionariesEqual(config?.theme, next.theme)
256
257
  if config?.generation != next.generation { endReachedGeneration = nil }
257
258
  config = next
258
259
  itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
@@ -267,7 +268,7 @@ final class NativeListView: UIView {
267
268
  snapshot.appendItems(keys, toSection: 0)
268
269
  let changedKeys = keys.filter { key in
269
270
  guard let old = oldItems[key], let new = itemsByKey[key] else { return false }
270
- return old.revision != new.revision || old.content != new.content
271
+ return themeChanged || old.revision != new.revision || old.content != new.content
271
272
  }
272
273
  snapshot.reconfigureItems(changedKeys)
273
274
  dataSource.apply(snapshot, animatingDifferences: false) { [weak self] in
@@ -872,7 +873,8 @@ final class NativeListView: UIView {
872
873
  titles: sectionIndexEntries.map(\.title),
873
874
  textColor: nativeListColor(config.theme, "secondaryText", "#646464"),
874
875
  activeColor: nativeListColor(config.theme, "accent", "#108303"),
875
- activeTextColor: nativeListColor(config.theme, "inverseText", "#FCFCFC")
876
+ activeTextColor: nativeListColor(config.theme, "inverseText", "#FCFCFC"),
877
+ centeredInWindow: config.sectionIndexCenteredInWindow
876
878
  )
877
879
  sectionIndexView.isHidden = sectionIndexEntries.isEmpty
878
880
  sectionIndexPreview.backgroundColor = nativeListColor(
@@ -1112,6 +1114,7 @@ final class NativeListView: UIView {
1112
1114
  current.endReachedThreshold == next.endReachedThreshold,
1113
1115
  current.sectionIndexEnabled == next.sectionIndexEnabled,
1114
1116
  current.sectionIndexHapticsEnabled == next.sectionIndexHapticsEnabled,
1117
+ current.sectionIndexCenteredInWindow == next.sectionIndexCenteredInWindow,
1115
1118
  dictionariesEqual(current.theme, next.theme),
1116
1119
  current.fixedFooter?.content == next.fixedFooter?.content,
1117
1120
  current.items.count == next.items.count else { return false }
@@ -1755,6 +1758,7 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1755
1758
  private var textColor: UIColor = .secondaryLabel
1756
1759
  private var activeColor: UIColor = .tintColor
1757
1760
  private var activeTextColor: UIColor = .white
1761
+ private var centeredInWindow = false
1758
1762
  private var lastTouchIndex: Int?
1759
1763
  private(set) var activeIndex: Int?
1760
1764
 
@@ -1795,12 +1799,14 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1795
1799
  titles: [String],
1796
1800
  textColor: UIColor,
1797
1801
  activeColor: UIColor,
1798
- activeTextColor: UIColor
1802
+ activeTextColor: UIColor,
1803
+ centeredInWindow: Bool
1799
1804
  ) {
1800
1805
  self.titles = titles
1801
1806
  self.textColor = textColor
1802
1807
  self.activeColor = activeColor
1803
1808
  self.activeTextColor = activeTextColor
1809
+ self.centeredInWindow = centeredInWindow
1804
1810
  labels.forEach { $0.removeFromSuperview() }
1805
1811
  labels = titles.map { title in
1806
1812
  let label = UILabel()
@@ -1828,7 +1834,7 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1828
1834
  super.layoutSubviews()
1829
1835
  guard !labels.isEmpty else { return }
1830
1836
  let height = min(16, max(8, bounds.height / CGFloat(labels.count)))
1831
- let originY = (bounds.height - height * CGFloat(labels.count)) / 2
1837
+ let originY = indexOriginY(cellHeight: height, count: labels.count)
1832
1838
  for (index, label) in labels.enumerated() {
1833
1839
  label.frame = CGRect(
1834
1840
  x: 6,
@@ -1874,13 +1880,24 @@ private final class NativeListSectionIndexView: UIControl, UIGestureRecognizerDe
1874
1880
 
1875
1881
  private func select(at y: CGFloat, interacting: Bool) {
1876
1882
  let height = min(16, max(8, bounds.height / CGFloat(titles.count)))
1877
- let originY = (bounds.height - height * CGFloat(titles.count)) / 2
1883
+ let originY = indexOriginY(cellHeight: height, count: titles.count)
1878
1884
  let index = Int(floor((y - originY) / height)).clamped(to: 0...(titles.count - 1))
1879
1885
  if interacting && lastTouchIndex == index { return }
1880
1886
  lastTouchIndex = interacting ? index : nil
1881
1887
  select(index: index, interacting: interacting)
1882
1888
  }
1883
1889
 
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 }
1894
+ let windowCenterY = window.safeAreaLayoutGuide.layoutFrame.midY
1895
+ 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)
1898
+ )
1899
+ }
1900
+
1884
1901
  private func select(index: Int, interacting: Bool) {
1885
1902
  onSelect?(index, interacting)
1886
1903
  setActiveIndex(index)
@@ -190,7 +190,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
190
190
  emitIndexFailure(-1, 'item-not-found');
191
191
  return;
192
192
  }
193
- dispatchIndexScroll(index, normalizePositionScroll(params, 'start'));
193
+ dispatchKeyScroll(params.item.key, normalizePositionScroll(params, 'start'));
194
194
  },
195
195
  scrollToOffset({
196
196
  offset,
@@ -144,7 +144,7 @@ export const NativeList = /*#__PURE__*/forwardRef(function NativeList({
144
144
  emitIndexFailure(-1, 'item-not-found');
145
145
  return;
146
146
  }
147
- engineRef.current?.scrollToIndex(index, normalizePositionScroll(params, 'start'));
147
+ engineRef.current?.scrollToKey(params.item.key, normalizePositionScroll(params, 'start'));
148
148
  },
149
149
  scrollToOffset({
150
150
  offset,
@@ -392,6 +392,9 @@ export function validateSnapshot(snapshot) {
392
392
  if (sectionIndex.hapticsEnabled !== undefined && typeof sectionIndex.hapticsEnabled !== 'boolean') {
393
393
  fail('snapshot.capabilities.sectionIndex.hapticsEnabled', 'must be a boolean');
394
394
  }
395
+ if (sectionIndex.centeredInWindow !== undefined && typeof sectionIndex.centeredInWindow !== 'boolean') {
396
+ fail('snapshot.capabilities.sectionIndex.centeredInWindow', 'must be a boolean');
397
+ }
395
398
  if (sectionIndex.enabled && (snapshot.layout.kind !== 'sectioned' || snapshot.layout.orientation === 'horizontal')) {
396
399
  fail('snapshot.capabilities.sectionIndex', 'requires a vertical sectioned layout');
397
400
  }
@@ -390,6 +390,7 @@ export type NativeListTheme = Readonly<{
390
390
  export type SectionIndexConfig = Readonly<{
391
391
  enabled: boolean;
392
392
  hapticsEnabled?: boolean;
393
+ centeredInWindow?: boolean;
393
394
  }>;
394
395
  export type NativeListSnapshot = Readonly<{
395
396
  schemaVersion: 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-native-list",
3
- "version": "3.0.107",
3
+ "version": "3.0.109",
4
4
  "description": "Template-driven native RecyclerView and UICollectionView for React Native",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./lib/module/index.js",
@@ -82,7 +82,7 @@
82
82
  "typescript": "^5.9.2"
83
83
  },
84
84
  "peerDependencies": {
85
- "@onekeyfe/react-native-image": "3.0.107",
85
+ "@onekeyfe/react-native-image": "3.0.109",
86
86
  "react": "*",
87
87
  "react-native": "*",
88
88
  "react-native-nitro-modules": "0.37.0"
@@ -328,7 +328,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
328
328
  emitIndexFailure(-1, 'item-not-found');
329
329
  return;
330
330
  }
331
- dispatchIndexScroll(index, normalizePositionScroll(params, 'start'));
331
+ dispatchKeyScroll(
332
+ params.item.key,
333
+ normalizePositionScroll(params, 'start')
334
+ );
332
335
  },
333
336
  scrollToOffset({ offset, animated = true }) {
334
337
  validateOffset(offset);
@@ -234,8 +234,8 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
234
234
  emitIndexFailure(-1, 'item-not-found');
235
235
  return;
236
236
  }
237
- engineRef.current?.scrollToIndex(
238
- index,
237
+ engineRef.current?.scrollToKey(
238
+ params.item.key,
239
239
  normalizePositionScroll(params, 'start')
240
240
  );
241
241
  },
package/src/models.ts CHANGED
@@ -409,6 +409,7 @@ export type NativeListTheme = Readonly<{
409
409
  export type SectionIndexConfig = Readonly<{
410
410
  enabled: boolean;
411
411
  hapticsEnabled?: boolean;
412
+ centeredInWindow?: boolean;
412
413
  }>;
413
414
 
414
415
  export type NativeListSnapshot = Readonly<{
package/src/validation.ts CHANGED
@@ -698,6 +698,15 @@ export function validateSnapshot(
698
698
  'must be a boolean'
699
699
  );
700
700
  }
701
+ if (
702
+ sectionIndex.centeredInWindow !== undefined &&
703
+ typeof sectionIndex.centeredInWindow !== 'boolean'
704
+ ) {
705
+ fail(
706
+ 'snapshot.capabilities.sectionIndex.centeredInWindow',
707
+ 'must be a boolean'
708
+ );
709
+ }
701
710
  if (
702
711
  sectionIndex.enabled &&
703
712
  (snapshot.layout.kind !== 'sectioned' ||