@onekeyfe/react-native-native-list 3.0.113 → 3.0.115

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.
@@ -4,11 +4,14 @@ import android.animation.TimeInterpolator
4
4
  import android.graphics.Canvas
5
5
  import android.graphics.Color
6
6
  import android.graphics.Paint
7
+ import android.graphics.Path
7
8
  import android.graphics.RectF
8
- import android.graphics.drawable.GradientDrawable
9
+ import android.graphics.drawable.ShapeDrawable
10
+ import android.graphics.drawable.shapes.PathShape
9
11
  import android.os.Bundle
10
12
  import android.os.Handler
11
13
  import android.os.Looper
14
+ import android.os.SystemClock
12
15
  import android.view.HapticFeedbackConstants
13
16
  import android.view.Choreographer
14
17
  import android.view.Gravity
@@ -30,6 +33,7 @@ import androidx.recyclerview.widget.LinearSmoothScroller
30
33
  import androidx.recyclerview.widget.RecyclerView
31
34
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
32
35
  import com.facebook.react.uimanager.ThemedReactContext
36
+ import com.margelo.nitro.nativelogger.OneKeyLog
33
37
  import org.json.JSONArray
34
38
  import org.json.JSONObject
35
39
  import java.lang.ref.WeakReference
@@ -150,6 +154,9 @@ class NativeListView(
150
154
  private var dragFrom = RecyclerView.NO_POSITION
151
155
  private var dragTo = RecyclerView.NO_POSITION
152
156
  private var pendingReorder: List<NativeListItem>? = null
157
+ private var reorderRelayoutActive = false
158
+ private var reorderRelayoutScheduled = false
159
+ private var lastReorderRelayoutLogAtMs = 0L
153
160
  private var sectionIndexEntries: List<NativeListSectionIndexEntry> = emptyList()
154
161
  private var sectionIndexScrubbing = false
155
162
  private var sectionIndexProgrammaticScroll = false
@@ -198,27 +205,28 @@ class NativeListView(
198
205
  contentContainer.addView(
199
206
  sectionIndexView,
200
207
  FrameLayout.LayoutParams(
201
- dp(SECTION_INDEX_RAIL_WIDTH_DP),
208
+ sectionIndexDp(SECTION_INDEX_RAIL_WIDTH_DP),
202
209
  FrameLayout.LayoutParams.MATCH_PARENT,
203
210
  Gravity.END,
204
211
  ),
205
212
  )
206
213
  sectionIndexPreview.apply {
207
214
  gravity = Gravity.CENTER
208
- textSize = NativeListScale.font(resources, 22f)
209
- typeface = NativeListFonts.semibold(context)
210
- visibility = GONE
215
+ textSize = 30f
216
+ typeface = NativeListFonts.regular(context)
217
+ setPadding(0, 0, sectionIndexDp(10), 0)
218
+ visibility = INVISIBLE
211
219
  alpha = 0f
212
220
  importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
213
221
  }
214
222
  contentContainer.addView(
215
223
  sectionIndexPreview,
216
224
  FrameLayout.LayoutParams(
217
- dp(SECTION_INDEX_PREVIEW_SIZE_DP),
218
- dp(SECTION_INDEX_PREVIEW_SIZE_DP),
225
+ sectionIndexDp(SECTION_INDEX_PREVIEW_WIDTH_DP),
226
+ sectionIndexDp(SECTION_INDEX_PREVIEW_HEIGHT_DP),
219
227
  Gravity.CENTER_VERTICAL or Gravity.END,
220
228
  ).apply {
221
- marginEnd = dp(SECTION_INDEX_PREVIEW_END_MARGIN_DP)
229
+ marginEnd = sectionIndexDp(SECTION_INDEX_PREVIEW_END_MARGIN_DP)
222
230
  },
223
231
  )
224
232
  addView(contentContainer, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f))
@@ -517,9 +525,13 @@ class NativeListView(
517
525
  }
518
526
  val nextItems = current.items.toMutableList()
519
527
  val selected = LinkedHashSet(current.selectedKeys)
528
+ var marketQuoteOnly = pending.isNotEmpty()
520
529
  try {
521
530
  pending.forEach { (index, changes) ->
522
531
  val previous = nextItems[index]
532
+ marketQuoteOnly = marketQuoteOnly &&
533
+ previous.type == "market" &&
534
+ changes.keys().asSequence().all(MARKET_QUOTE_FIELDS::contains)
523
535
  val merged = NativeListItem.parse(mergeRow(previous.json, changes))
524
536
  nextItems[index] = merged
525
537
  if (changes.has("selected")) {
@@ -529,7 +541,7 @@ class NativeListView(
529
541
  } catch (_: Exception) {
530
542
  return
531
543
  }
532
- invalidateActionAnchor("snapshot")
544
+ if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
533
545
  val next = current.copy(items = nextItems, selectedKeys = selected)
534
546
  config = next
535
547
  usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
@@ -860,6 +872,7 @@ class NativeListView(
860
872
 
861
873
  fun dispose() {
862
874
  if (disposed) return
875
+ stopReorderRelayoutLoop()
863
876
  invalidateActionAnchor("destroy")
864
877
  actionAnchor = null
865
878
  disposed = true
@@ -930,17 +943,14 @@ class NativeListView(
930
943
  sectionIndexHapticsEnabled = next.sectionIndexHapticsEnabled
931
944
  sectionIndexView.configure(
932
945
  sectionIndexEntries.map { it.title },
933
- themeColor(next.theme, "secondaryText", "#646464"),
934
- themeColor(next.theme, "accent", "#108303"),
946
+ themeColor(next.theme, "disabledText", "#8D8D8D"),
947
+ themeColor(next.theme, "positive", "#218358"),
935
948
  themeColor(next.theme, "inverseText", "#FCFCFC"),
936
949
  next.sectionIndexCenteredInWindow,
937
950
  )
938
951
  sectionIndexView.visibility = if (sectionIndexEntries.isEmpty()) GONE else VISIBLE
939
- sectionIndexPreview.setTextColor(themeColor(next.theme, "inverseText", "#FCFCFC"))
940
- sectionIndexPreview.background = GradientDrawable().apply {
941
- setColor(themeColor(next.theme, "inverseBackground", "#202020"))
942
- cornerRadius = dp(14).toFloat()
943
- }
952
+ sectionIndexPreview.setTextColor(Color.WHITE)
953
+ sectionIndexPreview.background = sectionIndexPreviewBackground()
944
954
  sectionIndexView.setActiveIndex(
945
955
  previousKey?.let { key -> sectionIndexEntries.indexOfFirst { it.key == key }.takeIf { it >= 0 } },
946
956
  )
@@ -961,6 +971,7 @@ class NativeListView(
961
971
  if (interacting) {
962
972
  sectionIndexPreview.animate().cancel()
963
973
  sectionIndexPreview.text = entry.title
974
+ positionSectionIndexPreview(index)
964
975
  sectionIndexPreview.visibility = VISIBLE
965
976
  sectionIndexPreview.alpha = 1f
966
977
  if (changed && sectionIndexHapticsEnabled) {
@@ -976,16 +987,41 @@ class NativeListView(
976
987
  sectionIndexPreview.animate().cancel()
977
988
  if (immediately) {
978
989
  sectionIndexPreview.alpha = 0f
979
- sectionIndexPreview.visibility = GONE
990
+ sectionIndexPreview.visibility = INVISIBLE
980
991
  } else {
981
992
  sectionIndexPreview.animate()
982
993
  .alpha(0f)
983
994
  .setDuration(150)
984
- .withEndAction { sectionIndexPreview.visibility = GONE }
995
+ .withEndAction { sectionIndexPreview.visibility = INVISIBLE }
985
996
  .start()
986
997
  }
987
998
  }
988
999
 
1000
+ private fun positionSectionIndexPreview(index: Int) {
1001
+ val halfHeight = sectionIndexDp(SECTION_INDEX_PREVIEW_HEIGHT_DP) / 2f
1002
+ val maximumY = (contentContainer.height - halfHeight).coerceAtLeast(halfHeight)
1003
+ val targetY = sectionIndexView.top + sectionIndexView.centerYForIndex(index)
1004
+ val clampedY = targetY.coerceIn(halfHeight, maximumY)
1005
+ sectionIndexPreview.translationY = clampedY - contentContainer.height / 2f
1006
+ }
1007
+
1008
+ private fun sectionIndexPreviewBackground(): ShapeDrawable {
1009
+ val path = Path().apply {
1010
+ moveTo(25f, 0f)
1011
+ cubicTo(11f, 0f, 0f, 11f, 0f, 25f)
1012
+ cubicTo(0f, 39f, 11f, 50f, 25f, 50f)
1013
+ cubicTo(36f, 50f, 43f, 44f, 46f, 37.5f)
1014
+ lineTo(60f, 25f)
1015
+ lineTo(46f, 12.5f)
1016
+ cubicTo(43f, 6f, 36f, 0f, 25f, 0f)
1017
+ close()
1018
+ }
1019
+ return ShapeDrawable(PathShape(path, 60f, 50f)).apply {
1020
+ paint.color = Color.rgb(194, 194, 194)
1021
+ paint.isAntiAlias = true
1022
+ }
1023
+ }
1024
+
989
1025
  private fun syncSectionIndexToVisibleRows() {
990
1026
  if (sectionIndexScrubbing || sectionIndexProgrammaticScroll || sectionIndexEntries.isEmpty()) return
991
1027
  val firstVisible = layoutManager.findFirstVisibleItemPosition()
@@ -1235,6 +1271,7 @@ class NativeListView(
1235
1271
  }
1236
1272
 
1237
1273
  private fun updateReordering(next: NativeListConfig) {
1274
+ stopReorderRelayoutLoop()
1238
1275
  reorderTouchHandler?.removeCallbacksAndMessages(null)
1239
1276
  reorderTouchHandler = null
1240
1277
  reorderTouchListener?.let(recyclerView::removeOnItemTouchListener)
@@ -1249,6 +1286,48 @@ class NativeListView(
1249
1286
  ) {
1250
1287
  private var compactWalletGroupDrag = false
1251
1288
  private var compactWalletGroupTop = Float.NaN
1289
+ private var dragType: String? = null
1290
+ private var dragStartedAtMs = 0L
1291
+ private var lastAutoScrollLogAtMs = 0L
1292
+ private var lastDragFrameLogAtMs = 0L
1293
+ private var lastMoveAttemptLogAtMs = 0L
1294
+ private var lastMoveAttemptSignature = ""
1295
+ private var latestDragDx = 0f
1296
+ private var latestDragDy = 0f
1297
+
1298
+ private fun logMoveAttempt(
1299
+ recyclerView: RecyclerView,
1300
+ from: Int,
1301
+ to: Int,
1302
+ fromItem: NativeListItem?,
1303
+ toItem: NativeListItem?,
1304
+ outcome: String,
1305
+ targetTop: Int,
1306
+ targetBottom: Int,
1307
+ ) {
1308
+ val now = SystemClock.uptimeMillis()
1309
+ val signature = "$from:$to:$outcome"
1310
+ if (
1311
+ signature == lastMoveAttemptSignature &&
1312
+ now - lastMoveAttemptLogAtMs < REORDER_MOVE_ATTEMPT_LOG_INTERVAL_MS
1313
+ ) {
1314
+ return
1315
+ }
1316
+ lastMoveAttemptSignature = signature
1317
+ lastMoveAttemptLogAtMs = now
1318
+ OneKeyLog.info(
1319
+ REORDER_LOG_TAG,
1320
+ "moveAttempt outcome=$outcome from=$from to=$to " +
1321
+ "fromType=${fromItem?.type.orEmpty()} toType=${toItem?.type.orEmpty()} " +
1322
+ "fromReorderable=${fromItem?.isReorderable} toReorderable=${toItem?.isReorderable} " +
1323
+ "sameSection=${fromItem != null && toItem != null && fromItem.sectionKey == toItem.sectionKey} " +
1324
+ "elapsedDragMs=${now - dragStartedAtMs} " +
1325
+ "scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
1326
+ "targetTop=$targetTop targetBottom=$targetBottom " +
1327
+ "viewportTop=${recyclerView.paddingTop} " +
1328
+ "viewportBottom=${recyclerView.height - recyclerView.paddingBottom}",
1329
+ )
1330
+ }
1252
1331
 
1253
1332
  override fun isLongPressDragEnabled(): Boolean = false
1254
1333
 
@@ -1260,6 +1339,23 @@ class NativeListView(
1260
1339
  reorderPlaceholderDecoration.position = position
1261
1340
  reorderPlaceholderDecoration.color = reorderActiveBackground(next.theme)
1262
1341
  compactWalletGroupDrag = item?.type == "walletGroup" && viewHolder != null
1342
+ dragType = item?.type
1343
+ dragStartedAtMs = SystemClock.uptimeMillis()
1344
+ lastAutoScrollLogAtMs = 0L
1345
+ lastDragFrameLogAtMs = 0L
1346
+ lastMoveAttemptLogAtMs = 0L
1347
+ lastMoveAttemptSignature = ""
1348
+ latestDragDx = 0f
1349
+ latestDragDy = 0f
1350
+ startReorderRelayoutLoop()
1351
+ OneKeyLog.info(
1352
+ REORDER_LOG_TAG,
1353
+ "start type=${dragType.orEmpty()} position=$position " +
1354
+ "compact=$compactWalletGroupDrag itemHeight=${viewHolder?.itemView?.height ?: -1} " +
1355
+ "viewport=${recyclerView.width}x${recyclerView.height} " +
1356
+ "padding=${recyclerView.paddingLeft},${recyclerView.paddingTop}," +
1357
+ "${recyclerView.paddingRight},${recyclerView.paddingBottom} density=$density",
1358
+ )
1263
1359
  recyclerView.invalidate()
1264
1360
  (viewHolder as? NativeListViewHolder)?.rowView?.setReorderActive(true)
1265
1361
  if (compactWalletGroupDrag && viewHolder != null) {
@@ -1287,22 +1383,63 @@ class NativeListView(
1287
1383
  val from = source.bindingAdapterPosition
1288
1384
  val to = target.bindingAdapterPosition
1289
1385
  val base = pendingReorder ?: adapter.currentList
1290
- val fromItem = base.getOrNull(from) ?: return false
1291
- val toItem = base.getOrNull(to) ?: return false
1292
- if (!fromItem.isReorderable || !toItem.isReorderable || fromItem.sectionKey != toItem.sectionKey) return false
1386
+ val fromItem = base.getOrNull(from)
1387
+ val toItem = base.getOrNull(to)
1388
+ val targetTop = layoutManager.getDecoratedTop(target.itemView)
1389
+ val targetBottom = layoutManager.getDecoratedBottom(target.itemView)
1390
+ val targetClipped =
1391
+ next.orientation != "horizontal" &&
1392
+ from != RecyclerView.NO_POSITION &&
1393
+ to != RecyclerView.NO_POSITION &&
1394
+ when {
1395
+ to > from -> targetBottom > recyclerView.height - recyclerView.paddingBottom
1396
+ to < from -> targetTop < recyclerView.paddingTop
1397
+ else -> false
1398
+ }
1399
+ val outcome = when {
1400
+ fromItem == null -> "missingSource"
1401
+ toItem == null -> "missingTarget"
1402
+ !fromItem.isReorderable -> "sourceNotReorderable"
1403
+ !toItem.isReorderable -> "targetNotReorderable"
1404
+ fromItem.sectionKey != toItem.sectionKey -> "sectionMismatch"
1405
+ targetClipped -> "targetClipped"
1406
+ else -> "accepted"
1407
+ }
1408
+ logMoveAttempt(recyclerView, from, to, fromItem, toItem, outcome, targetTop, targetBottom)
1409
+ if (outcome != "accepted" || fromItem == null || toItem == null) return false
1293
1410
  val crossedPosition = dragTo != to
1294
1411
  if (dragFrom == RecyclerView.NO_POSITION) dragFrom = from
1295
1412
  dragTo = to
1296
1413
  if (crossedPosition) {
1297
1414
  recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
1415
+ OneKeyLog.info(
1416
+ REORDER_LOG_TAG,
1417
+ "move type=${dragType.orEmpty()} from=$from to=$to " +
1418
+ "scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
1419
+ "dragDx=$latestDragDx dragDy=$latestDragDy",
1420
+ )
1298
1421
  }
1299
1422
  val displacedView = target.itemView
1300
1423
  prepareDisplacedReorderAnimation(displacedView)
1301
- val reordered = adapter.moveReordered(from, to) ?: return false
1424
+ val reordered = adapter.moveReordered(from, to)
1425
+ if (reordered == null) {
1426
+ logMoveAttempt(
1427
+ recyclerView,
1428
+ from,
1429
+ to,
1430
+ fromItem,
1431
+ toItem,
1432
+ "adapterRejected",
1433
+ targetTop,
1434
+ targetBottom,
1435
+ )
1436
+ return false
1437
+ }
1302
1438
  pendingReorder = reordered
1303
1439
  reorderPlaceholderDecoration.position = to
1304
1440
  recyclerView.invalidate()
1305
- recyclerView.postOnAnimation(::relayoutRecyclerViewImmediately)
1441
+ // OneKey patch: the drag-scoped loop handles deferred nested RecyclerView layouts.
1442
+ // recyclerView.postOnAnimation(::relayoutRecyclerViewImmediately)
1306
1443
  return true
1307
1444
  }
1308
1445
 
@@ -1318,11 +1455,40 @@ class NativeListView(
1318
1455
  isCurrentlyActive: Boolean,
1319
1456
  ) {
1320
1457
  if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
1458
+ latestDragDx = dX
1459
+ latestDragDy = dY
1321
1460
  if (compactWalletGroupDrag) {
1322
1461
  compactWalletGroupTop = viewHolder.itemView.top + dY
1323
1462
  }
1324
1463
  }
1325
1464
  super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
1465
+ if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
1466
+ val now = SystemClock.uptimeMillis()
1467
+ if (
1468
+ lastDragFrameLogAtMs == 0L ||
1469
+ now - lastDragFrameLogAtMs >= REORDER_DRAG_FRAME_LOG_INTERVAL_MS
1470
+ ) {
1471
+ lastDragFrameLogAtMs = now
1472
+ val itemView = viewHolder.itemView
1473
+ val visualTop = itemView.top + dY
1474
+ val visualBottom = itemView.bottom + dY
1475
+ OneKeyLog.info(
1476
+ REORDER_LOG_TAG,
1477
+ "frame position=${viewHolder.bindingAdapterPosition} active=$isCurrentlyActive " +
1478
+ "dragDx=$dX dragDy=$dY itemTop=${itemView.top} itemBottom=${itemView.bottom} " +
1479
+ "translationY=${itemView.translationY} visualTop=$visualTop visualBottom=$visualBottom " +
1480
+ "viewportTop=${recyclerView.paddingTop} " +
1481
+ "viewportBottom=${recyclerView.height - recyclerView.paddingBottom} " +
1482
+ "firstVisible=${layoutManager.findFirstVisibleItemPosition()} " +
1483
+ "lastVisible=${layoutManager.findLastVisibleItemPosition()} " +
1484
+ "canScrollUp=${recyclerView.canScrollVertically(-1)} " +
1485
+ "canScrollDown=${recyclerView.canScrollVertically(1)} " +
1486
+ "scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
1487
+ "layoutRequested=${recyclerView.isLayoutRequested} " +
1488
+ "computingLayout=${recyclerView.isComputingLayout}",
1489
+ )
1490
+ }
1491
+ }
1326
1492
  }
1327
1493
 
1328
1494
  override fun interpolateOutOfBoundsScroll(
@@ -1332,35 +1498,60 @@ class NativeListView(
1332
1498
  totalSize: Int,
1333
1499
  msSinceStartScroll: Long,
1334
1500
  ): Int {
1335
- if (!compactWalletGroupDrag) {
1336
- return super.interpolateOutOfBoundsScroll(
1337
- recyclerView,
1338
- viewSize,
1339
- viewSizeOutOfBounds,
1340
- totalSize,
1341
- msSinceStartScroll,
1342
- )
1343
- }
1344
- if (compactWalletGroupTop.isNaN()) return 0
1345
- val compactHeight = dp(68)
1346
- val compactTop = compactWalletGroupTop.roundToInt()
1347
- val compactOutOfBounds = when {
1348
- compactTop < recyclerView.paddingTop -> compactTop - recyclerView.paddingTop
1349
- compactTop + compactHeight > recyclerView.height - recyclerView.paddingBottom ->
1350
- compactTop + compactHeight - (recyclerView.height - recyclerView.paddingBottom)
1351
- else -> 0
1501
+ val effectiveViewSize: Int
1502
+ val effectiveOutOfBounds: Int
1503
+ if (compactWalletGroupDrag) {
1504
+ if (compactWalletGroupTop.isNaN()) return 0
1505
+ effectiveViewSize = dp(68)
1506
+ val compactTop = compactWalletGroupTop.roundToInt()
1507
+ effectiveOutOfBounds = when {
1508
+ compactTop < recyclerView.paddingTop -> compactTop - recyclerView.paddingTop
1509
+ compactTop + effectiveViewSize > recyclerView.height - recyclerView.paddingBottom ->
1510
+ compactTop + effectiveViewSize - (recyclerView.height - recyclerView.paddingBottom)
1511
+ else -> 0
1512
+ }
1513
+ if (effectiveOutOfBounds == 0) return 0
1514
+ } else {
1515
+ effectiveViewSize = viewSize
1516
+ effectiveOutOfBounds = viewSizeOutOfBounds
1352
1517
  }
1353
- if (compactOutOfBounds == 0) return 0
1354
- return super.interpolateOutOfBoundsScroll(
1518
+ // OneKey patch: warm-start AndroidX's two-second quintic edge-scroll ramp.
1519
+ val acceleratedElapsedOutMs =
1520
+ msSinceStartScroll + REORDER_AUTOSCROLL_ACCELERATION_OFFSET_MS
1521
+ val result = super.interpolateOutOfBoundsScroll(
1355
1522
  recyclerView,
1356
- compactHeight,
1357
- compactOutOfBounds,
1523
+ effectiveViewSize,
1524
+ effectiveOutOfBounds,
1358
1525
  totalSize,
1359
- msSinceStartScroll,
1526
+ // msSinceStartScroll,
1527
+ acceleratedElapsedOutMs,
1360
1528
  )
1529
+ val now = SystemClock.uptimeMillis()
1530
+ if (
1531
+ lastAutoScrollLogAtMs == 0L ||
1532
+ now - lastAutoScrollLogAtMs >= REORDER_AUTOSCROLL_LOG_INTERVAL_MS
1533
+ ) {
1534
+ lastAutoScrollLogAtMs = now
1535
+ OneKeyLog.info(
1536
+ REORDER_LOG_TAG,
1537
+ "autoScroll type=${dragType.orEmpty()} " +
1538
+ "compact=$compactWalletGroupDrag rawViewSize=$viewSize " +
1539
+ "effectiveViewSize=$effectiveViewSize rawOut=$viewSizeOutOfBounds " +
1540
+ "effectiveOut=$effectiveOutOfBounds totalSize=$totalSize " +
1541
+ "elapsedOutMs=$msSinceStartScroll " +
1542
+ "acceleratedElapsedOutMs=$acceleratedElapsedOutMs " +
1543
+ "elapsedDragMs=${now - dragStartedAtMs} " +
1544
+ "resultPx=$result scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
1545
+ "canScrollUp=${recyclerView.canScrollVertically(-1)} " +
1546
+ "canScrollDown=${recyclerView.canScrollVertically(1)} " +
1547
+ "dragDx=$latestDragDx dragDy=$latestDragDy compactTop=$compactWalletGroupTop",
1548
+ )
1549
+ }
1550
+ return result
1361
1551
  }
1362
1552
 
1363
1553
  override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
1554
+ stopReorderRelayoutLoop()
1364
1555
  val rowView = (viewHolder as? NativeListViewHolder)?.rowView
1365
1556
  val draggedGroupKey = (rowView?.tag as? NativeListItem)?.key
1366
1557
  super.clearView(recyclerView, viewHolder)
@@ -1373,6 +1564,18 @@ class NativeListView(
1373
1564
  val from = dragFrom
1374
1565
  val to = dragTo
1375
1566
  val reordered = pendingReorder
1567
+ OneKeyLog.info(
1568
+ REORDER_LOG_TAG,
1569
+ "end type=${dragType.orEmpty()} from=$from to=$to " +
1570
+ "compact=$wasCompactWalletGroupDrag elapsedDragMs=" +
1571
+ "${SystemClock.uptimeMillis() - dragStartedAtMs} " +
1572
+ "scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
1573
+ "dragDx=$latestDragDx dragDy=$latestDragDy " +
1574
+ "firstVisible=${layoutManager.findFirstVisibleItemPosition()} " +
1575
+ "lastVisible=${layoutManager.findLastVisibleItemPosition()} " +
1576
+ "canScrollUp=${recyclerView.canScrollVertically(-1)} " +
1577
+ "canScrollDown=${recyclerView.canScrollVertically(1)}",
1578
+ )
1376
1579
  var reorderPayload: JSONObject? = null
1377
1580
  val destinationPosition = if (to != RecyclerView.NO_POSITION) {
1378
1581
  to
@@ -1428,6 +1631,7 @@ class NativeListView(
1428
1631
  dragFrom = RecyclerView.NO_POSITION
1429
1632
  dragTo = RecyclerView.NO_POSITION
1430
1633
  pendingReorder = null
1634
+ dragType = null
1431
1635
  // The gesture is committed now; a later snapshot may supersede its async diff.
1432
1636
  reorderPayload?.let { emit(REORDER, it) }
1433
1637
  }
@@ -1624,6 +1828,53 @@ class NativeListView(
1624
1828
  }
1625
1829
  }
1626
1830
 
1831
+ private fun startReorderRelayoutLoop() {
1832
+ reorderRelayoutActive = true
1833
+ lastReorderRelayoutLogAtMs = 0L
1834
+ scheduleReorderRelayout()
1835
+ }
1836
+
1837
+ private fun stopReorderRelayoutLoop() {
1838
+ reorderRelayoutActive = false
1839
+ }
1840
+
1841
+ private fun scheduleReorderRelayout() {
1842
+ if (!reorderRelayoutActive || reorderRelayoutScheduled) return
1843
+ reorderRelayoutScheduled = true
1844
+ recyclerView.postOnAnimation {
1845
+ reorderRelayoutScheduled = false
1846
+ if (
1847
+ !reorderRelayoutActive ||
1848
+ disposed ||
1849
+ recyclerView.width <= 0 ||
1850
+ recyclerView.height <= 0
1851
+ ) {
1852
+ return@postOnAnimation
1853
+ }
1854
+ val requestedBefore = recyclerView.isLayoutRequested
1855
+ val computingBefore = recyclerView.isComputingLayout
1856
+ if (requestedBefore && !computingBefore) {
1857
+ relayoutRecyclerViewImmediately()
1858
+ }
1859
+ val requestedAfter = recyclerView.isLayoutRequested
1860
+ val now = SystemClock.uptimeMillis()
1861
+ if (
1862
+ (requestedBefore || computingBefore || requestedAfter) &&
1863
+ (lastReorderRelayoutLogAtMs == 0L ||
1864
+ now - lastReorderRelayoutLogAtMs >= REORDER_RELAYOUT_LOG_INTERVAL_MS)
1865
+ ) {
1866
+ lastReorderRelayoutLogAtMs = now
1867
+ OneKeyLog.info(
1868
+ REORDER_LOG_TAG,
1869
+ "relayout requestedBefore=$requestedBefore computingBefore=$computingBefore " +
1870
+ "requestedAfter=$requestedAfter computingAfter=${recyclerView.isComputingLayout} " +
1871
+ "scrollOffset=${recyclerView.computeVerticalScrollOffset()}",
1872
+ )
1873
+ }
1874
+ scheduleReorderRelayout()
1875
+ }
1876
+ }
1877
+
1627
1878
  private fun relayoutRecyclerViewImmediately() {
1628
1879
  if (disposed || recyclerView.isComputingLayout || recyclerView.width <= 0 || recyclerView.height <= 0) {
1629
1880
  return
@@ -1643,10 +1894,20 @@ class NativeListView(
1643
1894
 
1644
1895
  private fun dp(value: Int): Int = if (usesSelectorSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
1645
1896
 
1897
+ private fun sectionIndexDp(value: Int): Int = (value * density).roundToInt()
1898
+
1646
1899
  companion object {
1900
+ private val MARKET_QUOTE_FIELDS = setOf(
1901
+ "revision",
1902
+ "price",
1903
+ "priceSegments",
1904
+ "change",
1905
+ "accessibilityLabel",
1906
+ )
1647
1907
  private const val SECTION_INDEX_CONTENT_INSET_DP = 16
1648
1908
  private const val SECTION_INDEX_RAIL_WIDTH_DP = 32
1649
- private const val SECTION_INDEX_PREVIEW_SIZE_DP = 48
1909
+ private const val SECTION_INDEX_PREVIEW_WIDTH_DP = 60
1910
+ private const val SECTION_INDEX_PREVIEW_HEIGHT_DP = 50
1650
1911
  private const val SECTION_INDEX_PREVIEW_END_MARGIN_DP = 40
1651
1912
  private const val REORDER_LONG_PRESS_MS = 200L
1652
1913
  private const val REORDER_ALLOWABLE_MOVEMENT_DP = 10
@@ -1656,6 +1917,12 @@ class NativeListView(
1656
1917
  private const val REORDER_SPRING_STIFFNESS = 400.0
1657
1918
  private const val REORDER_SPRING_MASS = 0.4
1658
1919
  private const val REORDER_SPRING_DURATION_MS = 300L
1920
+ private const val REORDER_AUTOSCROLL_ACCELERATION_OFFSET_MS = 1_500L
1921
+ private const val REORDER_AUTOSCROLL_LOG_INTERVAL_MS = 100L
1922
+ private const val REORDER_DRAG_FRAME_LOG_INTERVAL_MS = 100L
1923
+ private const val REORDER_MOVE_ATTEMPT_LOG_INTERVAL_MS = 250L
1924
+ private const val REORDER_RELAYOUT_LOG_INTERVAL_MS = 100L
1925
+ private const val REORDER_LOG_TAG = "NativeListReorder"
1659
1926
  private const val ROW_ACTION = "rowAction"
1660
1927
  private const val ACTION_ANCHOR_INVALIDATED = "actionAnchorInvalidated"
1661
1928
  private const val SELECTION_DELTA = "selectionDelta"
@@ -1739,11 +2006,11 @@ private class NativeListSectionIndexView(
1739
2006
  private val activeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
1740
2007
  private val normalPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1741
2008
  textAlign = Paint.Align.CENTER
1742
- typeface = NativeListFonts.medium(context)
2009
+ typeface = NativeListFonts.regular(context)
1743
2010
  }
1744
2011
  private val activePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
1745
2012
  textAlign = Paint.Align.CENTER
1746
- typeface = NativeListFonts.semibold(context)
2013
+ typeface = NativeListFonts.medium(context)
1747
2014
  }
1748
2015
 
1749
2016
  init {
@@ -1780,40 +2047,27 @@ private class NativeListSectionIndexView(
1780
2047
  override fun onDraw(canvas: Canvas) {
1781
2048
  super.onDraw(canvas)
1782
2049
  if (titles.isEmpty()) return
1783
- val cellHeight = cellHeight()
1784
- val originY = indexOriginY(cellHeight, titles.size)
1785
- val textSize = NativeListScale.font(resources, 10f) * resources.displayMetrics.scaledDensity
2050
+ val metrics = indexMetrics()
2051
+ val visibleIndices = visibleLabelIndices(metrics.trackHeight)
2052
+ val textSize = 10f * resources.displayMetrics.scaledDensity
2053
+ val centerX = width - dp(10f)
2054
+ val badgeRadius = dp(7f)
1786
2055
  normalPaint.color = normalColor
1787
2056
  normalPaint.textSize = textSize
1788
2057
  activePaint.color = activeColor
1789
2058
  activePaint.textSize = textSize
1790
- titles.forEachIndexed { index, title ->
2059
+ visibleIndices.forEachIndexed { visibleIndex, index ->
2060
+ val title = titles[index]
1791
2061
  val active = index == activeIndex
1792
2062
  val paint = if (active) activePaint else normalPaint
1793
- val centerY = originY + cellHeight * (index + 0.5f)
1794
- if (active && cellHeight >= NativeListScale.dp(resources, 12f)) {
1795
- val badgeWidth = NativeListScale.dp(resources, 20f)
1796
- val badgeHeight = minOf(cellHeight, NativeListScale.dp(resources, 16f))
2063
+ val centerY = visibleLabelCenterY(visibleIndex, visibleIndices.size, metrics)
2064
+ if (active) {
1797
2065
  activeBackgroundPaint.color = activeColor
1798
- canvas.drawRoundRect(
1799
- width / 2f - badgeWidth / 2f,
1800
- centerY - badgeHeight / 2f,
1801
- width / 2f + badgeWidth / 2f,
1802
- centerY + badgeHeight / 2f,
1803
- badgeHeight / 2f,
1804
- badgeHeight / 2f,
1805
- activeBackgroundPaint,
1806
- )
1807
- }
1808
- paint.color = if (active && cellHeight >= NativeListScale.dp(resources, 12f)) {
1809
- activeTextColor
1810
- } else if (active) {
1811
- activeColor
1812
- } else {
1813
- normalColor
2066
+ canvas.drawCircle(centerX, centerY, badgeRadius, activeBackgroundPaint)
1814
2067
  }
2068
+ paint.color = if (active) activeTextColor else normalColor
1815
2069
  val baseline = centerY - (paint.descent() + paint.ascent()) / 2f
1816
- canvas.drawText(title, width / 2f, baseline, paint)
2070
+ canvas.drawText(title, centerX, baseline, paint)
1817
2071
  }
1818
2072
  }
1819
2073
 
@@ -1888,9 +2142,14 @@ private class NativeListSectionIndexView(
1888
2142
  }
1889
2143
 
1890
2144
  private fun selectAt(y: Float, interacting: Boolean) {
1891
- val cellHeight = cellHeight()
1892
- val originY = indexOriginY(cellHeight, titles.size)
1893
- val index = ((y - originY) / cellHeight).toInt().coerceIn(0, titles.lastIndex)
2145
+ val metrics = indexMetrics()
2146
+ if (metrics.trackHeight <= 0f) return
2147
+ val visibleIndices = visibleLabelIndices(metrics.trackHeight).sorted()
2148
+ val visibleTrackHeight = minOf(metrics.trackHeight, dp(16f) * visibleIndices.size)
2149
+ val visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2f
2150
+ val progress = ((y - visibleOriginY) / visibleTrackHeight).coerceIn(0f, 1f)
2151
+ val slot = (progress * visibleIndices.size).toInt().coerceIn(0, visibleIndices.lastIndex)
2152
+ val index = visibleIndices[slot]
1894
2153
  if (interacting && lastTouchIndex == index) return
1895
2154
  lastTouchIndex = index.takeIf { interacting }
1896
2155
  select(index, interacting)
@@ -1901,29 +2160,60 @@ private class NativeListSectionIndexView(
1901
2160
  setActiveIndex(index)
1902
2161
  }
1903
2162
 
1904
- private fun cellHeight(): Float =
1905
- (height.toFloat() / titles.size.coerceAtLeast(1))
1906
- .coerceAtMost(NativeListScale.dp(resources, 16f))
1907
- .coerceAtLeast(1f)
2163
+ fun centerYForIndex(index: Int): Float = entryCenterY(index, indexMetrics())
2164
+
2165
+ private data class Metrics(val originY: Float, val trackHeight: Float)
1908
2166
 
1909
- private fun indexOriginY(cellHeight: Float, count: Int): Float {
1910
- val totalHeight = cellHeight * count
1911
- val centeredOriginY = (height - totalHeight) / 2f
1912
- if (!centeredInWindow || !isAttachedToWindow) return centeredOriginY
2167
+ private fun indexMetrics(): Metrics {
2168
+ if (titles.isEmpty()) return Metrics(height / 2f, 0f)
2169
+ val edgePadding = dp(8f)
2170
+ val labelSpacing = dp(16f)
2171
+ val availableHeight = (height - edgePadding * 2f).coerceAtLeast(0f)
2172
+ val trackHeight = minOf(availableHeight, labelSpacing * titles.size)
2173
+ val centeredOriginY = (height - trackHeight) / 2f
2174
+ if (!centeredInWindow || !isAttachedToWindow) {
2175
+ return Metrics(centeredOriginY, trackHeight)
2176
+ }
1913
2177
  val systemBarInsets = ViewCompat.getRootWindowInsets(rootView)
1914
2178
  ?.getInsetsIgnoringVisibility(
1915
2179
  WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
1916
- ) ?: return centeredOriginY
2180
+ ) ?: return Metrics(centeredOriginY, trackHeight)
1917
2181
  rootView.getLocationOnScreen(rootLocationOnScreen)
1918
2182
  getLocationOnScreen(locationOnScreen)
1919
2183
  val safeTop = rootLocationOnScreen[1] + systemBarInsets.top
1920
2184
  val safeBottom = rootLocationOnScreen[1] + rootView.height - systemBarInsets.bottom
1921
- if (safeBottom <= safeTop) return centeredOriginY
2185
+ if (safeBottom <= safeTop) return Metrics(centeredOriginY, trackHeight)
1922
2186
  val localCenterY = (safeTop + safeBottom) / 2f - locationOnScreen[1]
1923
- return (localCenterY - totalHeight / 2f)
1924
- .coerceIn(0f, (height - totalHeight).coerceAtLeast(0f))
2187
+ val maximumOrigin = (height - edgePadding - trackHeight).coerceAtLeast(edgePadding)
2188
+ return Metrics(
2189
+ (localCenterY - trackHeight / 2f).coerceIn(edgePadding, maximumOrigin),
2190
+ trackHeight,
2191
+ )
1925
2192
  }
1926
2193
 
2194
+ private fun entryCenterY(index: Int, metrics: Metrics): Float {
2195
+ if (titles.isEmpty()) return height / 2f
2196
+ return metrics.originY + metrics.trackHeight * (index + 0.5f) / titles.size
2197
+ }
2198
+
2199
+ private fun visibleLabelCenterY(visibleIndex: Int, visibleCount: Int, metrics: Metrics): Float {
2200
+ val visibleTrackHeight = minOf(metrics.trackHeight, dp(16f) * visibleCount)
2201
+ val visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2f
2202
+ return visibleOriginY + visibleTrackHeight * (visibleIndex + 0.5f) / visibleCount.coerceAtLeast(1)
2203
+ }
2204
+
2205
+ private fun visibleLabelIndices(trackHeight: Float): Set<Int> {
2206
+ if (titles.isEmpty()) return emptySet()
2207
+ val maxVisible = max(1, (trackHeight / dp(16f)).toInt())
2208
+ if (titles.size <= maxVisible) return titles.indices.toSet()
2209
+ if (maxVisible == 1) return setOf(0)
2210
+ return (0 until maxVisible).mapTo(mutableSetOf()) { slot ->
2211
+ (slot.toFloat() * titles.lastIndex / (maxVisible - 1)).roundToInt()
2212
+ }
2213
+ }
2214
+
2215
+ private fun dp(value: Float): Float = value * resources.displayMetrics.density
2216
+
1927
2217
  private fun updateContentDescription() {
1928
2218
  contentDescription = activeIndex?.let { titles.getOrNull(it) }
1929
2219
  ?.let { "$ACCESSIBILITY_LABEL, $it" }