@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.
- package/README.md +57 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +47 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +795 -30
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +445 -91
- package/ios/NativeListCell.swift +655 -20
- package/ios/NativeListDesignAssets.swift +8 -0
- package/ios/RNCNativeListView.swift +275 -42
- package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/Contents.json +1 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_badge_verified_solid.imageset/icon.svg +1 -0
- package/lib/module/validation.js +145 -1
- package/lib/module/web/NativeListWebEngine.js +461 -51
- package/lib/typescript/src/models.d.ts +108 -2
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +37 -3
- package/package.json +4 -2
- package/src/models.ts +144 -3
- package/src/validation.ts +250 -0
- package/src/web/NativeListWebEngine.ts +766 -91
|
@@ -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.
|
|
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
|
|
@@ -87,7 +91,11 @@ class NativeListView(
|
|
|
87
91
|
var onVisibleRangeChanged: ((String) -> Unit)? = null
|
|
88
92
|
private val density = resources.displayMetrics.density
|
|
89
93
|
private val recyclerView = RecyclerView(context)
|
|
94
|
+
private var configuredTopPaddingPx = 0
|
|
95
|
+
private var configuredBottomPaddingPx = 0
|
|
90
96
|
private val refreshLayout = SwipeRefreshLayout(context)
|
|
97
|
+
private val refreshIndicatorTravelPx = refreshLayout.progressViewEndOffset
|
|
98
|
+
private var refreshIndicatorOffsetPx = 0
|
|
91
99
|
private val contentContainer = FrameLayout(context)
|
|
92
100
|
private val adapter = NativeListAdapter(reactContext)
|
|
93
101
|
private val layoutManager = GridLayoutManager(context, 1)
|
|
@@ -148,6 +156,9 @@ class NativeListView(
|
|
|
148
156
|
private var dragFrom = RecyclerView.NO_POSITION
|
|
149
157
|
private var dragTo = RecyclerView.NO_POSITION
|
|
150
158
|
private var pendingReorder: List<NativeListItem>? = null
|
|
159
|
+
private var reorderRelayoutActive = false
|
|
160
|
+
private var reorderRelayoutScheduled = false
|
|
161
|
+
private var lastReorderRelayoutLogAtMs = 0L
|
|
151
162
|
private var sectionIndexEntries: List<NativeListSectionIndexEntry> = emptyList()
|
|
152
163
|
private var sectionIndexScrubbing = false
|
|
153
164
|
private var sectionIndexProgrammaticScroll = false
|
|
@@ -196,27 +207,28 @@ class NativeListView(
|
|
|
196
207
|
contentContainer.addView(
|
|
197
208
|
sectionIndexView,
|
|
198
209
|
FrameLayout.LayoutParams(
|
|
199
|
-
|
|
210
|
+
sectionIndexDp(SECTION_INDEX_RAIL_WIDTH_DP),
|
|
200
211
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
201
212
|
Gravity.END,
|
|
202
213
|
),
|
|
203
214
|
)
|
|
204
215
|
sectionIndexPreview.apply {
|
|
205
216
|
gravity = Gravity.CENTER
|
|
206
|
-
textSize =
|
|
207
|
-
typeface = NativeListFonts.
|
|
208
|
-
|
|
217
|
+
textSize = 30f
|
|
218
|
+
typeface = NativeListFonts.regular(context)
|
|
219
|
+
setPadding(0, 0, sectionIndexDp(10), 0)
|
|
220
|
+
visibility = INVISIBLE
|
|
209
221
|
alpha = 0f
|
|
210
222
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
211
223
|
}
|
|
212
224
|
contentContainer.addView(
|
|
213
225
|
sectionIndexPreview,
|
|
214
226
|
FrameLayout.LayoutParams(
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
sectionIndexDp(SECTION_INDEX_PREVIEW_WIDTH_DP),
|
|
228
|
+
sectionIndexDp(SECTION_INDEX_PREVIEW_HEIGHT_DP),
|
|
217
229
|
Gravity.CENTER_VERTICAL or Gravity.END,
|
|
218
230
|
).apply {
|
|
219
|
-
marginEnd =
|
|
231
|
+
marginEnd = sectionIndexDp(SECTION_INDEX_PREVIEW_END_MARGIN_DP)
|
|
220
232
|
},
|
|
221
233
|
)
|
|
222
234
|
addView(contentContainer, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f))
|
|
@@ -241,6 +253,11 @@ class NativeListView(
|
|
|
241
253
|
JSONObject().put("actionKey", "nativeList.refresh"),
|
|
242
254
|
)
|
|
243
255
|
}
|
|
256
|
+
recyclerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
257
|
+
// OneKey patch: a collapsible pager owns the extra padding above the rows.
|
|
258
|
+
// Keep the refresh control below that header without moving ordinary lists.
|
|
259
|
+
updateRefreshIndicatorOffset((recyclerView.paddingTop - configuredTopPaddingPx).coerceAtLeast(0))
|
|
260
|
+
}
|
|
244
261
|
|
|
245
262
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
246
263
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
@@ -264,6 +281,12 @@ class NativeListView(
|
|
|
264
281
|
|
|
265
282
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
266
283
|
super.onLayout(changed, left, top, right, bottom)
|
|
284
|
+
val first = config?.items?.firstOrNull()
|
|
285
|
+
if (recyclerView.isLayoutRequested && (first?.type == "market" || first?.json?.optString("presentation") == "market")) {
|
|
286
|
+
// A retained Market page can keep cached parent measurements after its diff.
|
|
287
|
+
// Drain the child's pending layout so its committed rows replace the old cells.
|
|
288
|
+
relayoutRecyclerViewImmediately()
|
|
289
|
+
}
|
|
267
290
|
val nextWidth = right - left
|
|
268
291
|
val nextHeight = bottom - top
|
|
269
292
|
if (
|
|
@@ -278,6 +301,28 @@ class NativeListView(
|
|
|
278
301
|
performPendingScrollIfNeeded()
|
|
279
302
|
}
|
|
280
303
|
|
|
304
|
+
private fun updateRefreshIndicatorOffset(offset: Int) {
|
|
305
|
+
if (refreshIndicatorOffsetPx == offset) return
|
|
306
|
+
refreshIndicatorOffsetPx = offset
|
|
307
|
+
val refreshing = refreshLayout.isRefreshing
|
|
308
|
+
val start = offset - refreshLayout.progressCircleDiameter
|
|
309
|
+
refreshLayout.setProgressViewOffset(false, start, start + refreshIndicatorTravelPx)
|
|
310
|
+
refreshLayout.isRefreshing = refreshing
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
override fun onAttachedToWindow() {
|
|
314
|
+
super.onAttachedToWindow()
|
|
315
|
+
val first = config?.items?.firstOrNull()
|
|
316
|
+
if (recyclerView.isLayoutRequested && (first?.type == "market" || first?.json?.optString("presentation") == "market")) {
|
|
317
|
+
relayoutContents()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
override fun onDetachedFromWindow() {
|
|
322
|
+
updateRefreshIndicatorOffset(0)
|
|
323
|
+
super.onDetachedFromWindow()
|
|
324
|
+
}
|
|
325
|
+
|
|
281
326
|
fun applySnapshot(snapshotJson: String) {
|
|
282
327
|
val next = try {
|
|
283
328
|
NativeListConfig.parse(snapshotJson)
|
|
@@ -318,6 +363,18 @@ class NativeListView(
|
|
|
318
363
|
}
|
|
319
364
|
return
|
|
320
365
|
}
|
|
366
|
+
// Keep the first Market row at its current pixel offset when it is
|
|
367
|
+
// reordered. RecyclerView otherwise follows the previous first key.
|
|
368
|
+
val marketStartOffset = if (
|
|
369
|
+
previous?.items?.firstOrNull()?.type == "market" &&
|
|
370
|
+
next.items.firstOrNull()?.type == "market" &&
|
|
371
|
+
previous.items.firstOrNull()?.key != next.items.firstOrNull()?.key &&
|
|
372
|
+
layoutManager.findFirstVisibleItemPosition() == 0
|
|
373
|
+
) {
|
|
374
|
+
layoutManager.findViewByPosition(0)?.let {
|
|
375
|
+
layoutManager.getDecoratedTop(it) - recyclerView.paddingTop
|
|
376
|
+
}
|
|
377
|
+
} else null
|
|
321
378
|
config = next
|
|
322
379
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
323
380
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
@@ -330,6 +387,9 @@ class NativeListView(
|
|
|
330
387
|
configureSectionIndex(next)
|
|
331
388
|
updateLayout(next)
|
|
332
389
|
adapter.submitList(next.items) {
|
|
390
|
+
if (marketStartOffset != null) {
|
|
391
|
+
layoutManager.scrollToPositionWithOffset(0, marketStartOffset)
|
|
392
|
+
}
|
|
333
393
|
relayoutContents()
|
|
334
394
|
performPendingScrollIfNeeded()
|
|
335
395
|
syncSectionIndexToVisibleRows()
|
|
@@ -500,9 +560,13 @@ class NativeListView(
|
|
|
500
560
|
}
|
|
501
561
|
val nextItems = current.items.toMutableList()
|
|
502
562
|
val selected = LinkedHashSet(current.selectedKeys)
|
|
563
|
+
var marketQuoteOnly = pending.isNotEmpty()
|
|
503
564
|
try {
|
|
504
565
|
pending.forEach { (index, changes) ->
|
|
505
566
|
val previous = nextItems[index]
|
|
567
|
+
marketQuoteOnly = marketQuoteOnly &&
|
|
568
|
+
previous.type == "market" &&
|
|
569
|
+
changes.keys().asSequence().all(MARKET_QUOTE_FIELDS::contains)
|
|
506
570
|
val merged = NativeListItem.parse(mergeRow(previous.json, changes))
|
|
507
571
|
nextItems[index] = merged
|
|
508
572
|
if (changes.has("selected")) {
|
|
@@ -512,7 +576,7 @@ class NativeListView(
|
|
|
512
576
|
} catch (_: Exception) {
|
|
513
577
|
return
|
|
514
578
|
}
|
|
515
|
-
invalidateActionAnchor("snapshot")
|
|
579
|
+
if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
|
|
516
580
|
val next = current.copy(items = nextItems, selectedKeys = selected)
|
|
517
581
|
config = next
|
|
518
582
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
@@ -843,6 +907,7 @@ class NativeListView(
|
|
|
843
907
|
|
|
844
908
|
fun dispose() {
|
|
845
909
|
if (disposed) return
|
|
910
|
+
stopReorderRelayoutLoop()
|
|
846
911
|
invalidateActionAnchor("destroy")
|
|
847
912
|
actionAnchor = null
|
|
848
913
|
disposed = true
|
|
@@ -872,11 +937,17 @@ class NativeListView(
|
|
|
872
937
|
val bottomPadding = next.contentPaddingBottom ?: defaultPadding
|
|
873
938
|
// OneKey patch: the section index overlays rows and keeps only an accessory-safe inset.
|
|
874
939
|
val indexGutter = if (sectionIndexEntries.isEmpty()) 0 else SECTION_INDEX_CONTENT_INSET_DP
|
|
940
|
+
// A native scroll coordinator may add header insets to this RecyclerView.
|
|
941
|
+
// Content/theme snapshots must replace only the padding owned by NativeList.
|
|
942
|
+
val coordinatorTopPadding = (recyclerView.paddingTop - configuredTopPaddingPx).coerceAtLeast(0)
|
|
943
|
+
val coordinatorBottomPadding = (recyclerView.paddingBottom - configuredBottomPaddingPx).coerceAtLeast(0)
|
|
944
|
+
configuredTopPaddingPx = dp(topPadding)
|
|
945
|
+
configuredBottomPaddingPx = dp(bottomPadding)
|
|
875
946
|
recyclerView.setPaddingRelative(
|
|
876
947
|
dp(horizontalPadding),
|
|
877
|
-
|
|
948
|
+
configuredTopPaddingPx + coordinatorTopPadding,
|
|
878
949
|
dp(horizontalPadding + indexGutter),
|
|
879
|
-
|
|
950
|
+
configuredBottomPaddingPx + coordinatorBottomPadding,
|
|
880
951
|
)
|
|
881
952
|
recyclerView.clipToPadding = false
|
|
882
953
|
recyclerView.isVerticalScrollBarEnabled = sectionIndexEntries.isEmpty()
|
|
@@ -907,17 +978,14 @@ class NativeListView(
|
|
|
907
978
|
sectionIndexHapticsEnabled = next.sectionIndexHapticsEnabled
|
|
908
979
|
sectionIndexView.configure(
|
|
909
980
|
sectionIndexEntries.map { it.title },
|
|
910
|
-
themeColor(next.theme, "
|
|
911
|
-
themeColor(next.theme, "
|
|
981
|
+
themeColor(next.theme, "disabledText", "#8D8D8D"),
|
|
982
|
+
themeColor(next.theme, "positive", "#218358"),
|
|
912
983
|
themeColor(next.theme, "inverseText", "#FCFCFC"),
|
|
913
984
|
next.sectionIndexCenteredInWindow,
|
|
914
985
|
)
|
|
915
986
|
sectionIndexView.visibility = if (sectionIndexEntries.isEmpty()) GONE else VISIBLE
|
|
916
|
-
sectionIndexPreview.setTextColor(
|
|
917
|
-
sectionIndexPreview.background =
|
|
918
|
-
setColor(themeColor(next.theme, "inverseBackground", "#202020"))
|
|
919
|
-
cornerRadius = dp(14).toFloat()
|
|
920
|
-
}
|
|
987
|
+
sectionIndexPreview.setTextColor(Color.WHITE)
|
|
988
|
+
sectionIndexPreview.background = sectionIndexPreviewBackground()
|
|
921
989
|
sectionIndexView.setActiveIndex(
|
|
922
990
|
previousKey?.let { key -> sectionIndexEntries.indexOfFirst { it.key == key }.takeIf { it >= 0 } },
|
|
923
991
|
)
|
|
@@ -938,6 +1006,7 @@ class NativeListView(
|
|
|
938
1006
|
if (interacting) {
|
|
939
1007
|
sectionIndexPreview.animate().cancel()
|
|
940
1008
|
sectionIndexPreview.text = entry.title
|
|
1009
|
+
positionSectionIndexPreview(index)
|
|
941
1010
|
sectionIndexPreview.visibility = VISIBLE
|
|
942
1011
|
sectionIndexPreview.alpha = 1f
|
|
943
1012
|
if (changed && sectionIndexHapticsEnabled) {
|
|
@@ -953,16 +1022,41 @@ class NativeListView(
|
|
|
953
1022
|
sectionIndexPreview.animate().cancel()
|
|
954
1023
|
if (immediately) {
|
|
955
1024
|
sectionIndexPreview.alpha = 0f
|
|
956
|
-
sectionIndexPreview.visibility =
|
|
1025
|
+
sectionIndexPreview.visibility = INVISIBLE
|
|
957
1026
|
} else {
|
|
958
1027
|
sectionIndexPreview.animate()
|
|
959
1028
|
.alpha(0f)
|
|
960
1029
|
.setDuration(150)
|
|
961
|
-
.withEndAction { sectionIndexPreview.visibility =
|
|
1030
|
+
.withEndAction { sectionIndexPreview.visibility = INVISIBLE }
|
|
962
1031
|
.start()
|
|
963
1032
|
}
|
|
964
1033
|
}
|
|
965
1034
|
|
|
1035
|
+
private fun positionSectionIndexPreview(index: Int) {
|
|
1036
|
+
val halfHeight = sectionIndexDp(SECTION_INDEX_PREVIEW_HEIGHT_DP) / 2f
|
|
1037
|
+
val maximumY = (contentContainer.height - halfHeight).coerceAtLeast(halfHeight)
|
|
1038
|
+
val targetY = sectionIndexView.top + sectionIndexView.centerYForIndex(index)
|
|
1039
|
+
val clampedY = targetY.coerceIn(halfHeight, maximumY)
|
|
1040
|
+
sectionIndexPreview.translationY = clampedY - contentContainer.height / 2f
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private fun sectionIndexPreviewBackground(): ShapeDrawable {
|
|
1044
|
+
val path = Path().apply {
|
|
1045
|
+
moveTo(25f, 0f)
|
|
1046
|
+
cubicTo(11f, 0f, 0f, 11f, 0f, 25f)
|
|
1047
|
+
cubicTo(0f, 39f, 11f, 50f, 25f, 50f)
|
|
1048
|
+
cubicTo(36f, 50f, 43f, 44f, 46f, 37.5f)
|
|
1049
|
+
lineTo(60f, 25f)
|
|
1050
|
+
lineTo(46f, 12.5f)
|
|
1051
|
+
cubicTo(43f, 6f, 36f, 0f, 25f, 0f)
|
|
1052
|
+
close()
|
|
1053
|
+
}
|
|
1054
|
+
return ShapeDrawable(PathShape(path, 60f, 50f)).apply {
|
|
1055
|
+
paint.color = Color.rgb(194, 194, 194)
|
|
1056
|
+
paint.isAntiAlias = true
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
966
1060
|
private fun syncSectionIndexToVisibleRows() {
|
|
967
1061
|
if (sectionIndexScrubbing || sectionIndexProgrammaticScroll || sectionIndexEntries.isEmpty()) return
|
|
968
1062
|
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
|
@@ -1004,6 +1098,7 @@ class NativeListView(
|
|
|
1004
1098
|
updateSelection(NativeSelectionTarget("row", item.key), item.key)
|
|
1005
1099
|
} else {
|
|
1006
1100
|
val actionKey = when {
|
|
1101
|
+
item.type == "market" && item.json.optString("pressActionKey").isNotEmpty() -> item.json.optString("pressActionKey")
|
|
1007
1102
|
item.type == "action" -> item.json.optString("actionKey")
|
|
1008
1103
|
item.type == "system" && item.json.optString("variant") == "retry" -> item.json.optString("actionKey")
|
|
1009
1104
|
else -> "press"
|
|
@@ -1061,7 +1156,12 @@ class NativeListView(
|
|
|
1061
1156
|
.put("source", origin.source)
|
|
1062
1157
|
.put("generation", generation)
|
|
1063
1158
|
.put("layoutDirection", if (origin.sourceView.layoutDirection == LAYOUT_DIRECTION_RTL) "rtl" else "ltr")
|
|
1064
|
-
.also { anchor ->
|
|
1159
|
+
.also { anchor ->
|
|
1160
|
+
origin.slot?.let { anchor.put("slot", it) }
|
|
1161
|
+
origin.windowPointPixels?.let { point ->
|
|
1162
|
+
anchor.put("windowPoint", JSONObject().put("x", point.x / density).put("y", point.y / density))
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1065
1165
|
}
|
|
1066
1166
|
|
|
1067
1167
|
private fun isOriginValid(origin: NativeListActionOrigin): Boolean =
|
|
@@ -1211,6 +1311,7 @@ class NativeListView(
|
|
|
1211
1311
|
}
|
|
1212
1312
|
|
|
1213
1313
|
private fun updateReordering(next: NativeListConfig) {
|
|
1314
|
+
stopReorderRelayoutLoop()
|
|
1214
1315
|
reorderTouchHandler?.removeCallbacksAndMessages(null)
|
|
1215
1316
|
reorderTouchHandler = null
|
|
1216
1317
|
reorderTouchListener?.let(recyclerView::removeOnItemTouchListener)
|
|
@@ -1225,6 +1326,48 @@ class NativeListView(
|
|
|
1225
1326
|
) {
|
|
1226
1327
|
private var compactWalletGroupDrag = false
|
|
1227
1328
|
private var compactWalletGroupTop = Float.NaN
|
|
1329
|
+
private var dragType: String? = null
|
|
1330
|
+
private var dragStartedAtMs = 0L
|
|
1331
|
+
private var lastAutoScrollLogAtMs = 0L
|
|
1332
|
+
private var lastDragFrameLogAtMs = 0L
|
|
1333
|
+
private var lastMoveAttemptLogAtMs = 0L
|
|
1334
|
+
private var lastMoveAttemptSignature = ""
|
|
1335
|
+
private var latestDragDx = 0f
|
|
1336
|
+
private var latestDragDy = 0f
|
|
1337
|
+
|
|
1338
|
+
private fun logMoveAttempt(
|
|
1339
|
+
recyclerView: RecyclerView,
|
|
1340
|
+
from: Int,
|
|
1341
|
+
to: Int,
|
|
1342
|
+
fromItem: NativeListItem?,
|
|
1343
|
+
toItem: NativeListItem?,
|
|
1344
|
+
outcome: String,
|
|
1345
|
+
targetTop: Int,
|
|
1346
|
+
targetBottom: Int,
|
|
1347
|
+
) {
|
|
1348
|
+
val now = SystemClock.uptimeMillis()
|
|
1349
|
+
val signature = "$from:$to:$outcome"
|
|
1350
|
+
if (
|
|
1351
|
+
signature == lastMoveAttemptSignature &&
|
|
1352
|
+
now - lastMoveAttemptLogAtMs < REORDER_MOVE_ATTEMPT_LOG_INTERVAL_MS
|
|
1353
|
+
) {
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
lastMoveAttemptSignature = signature
|
|
1357
|
+
lastMoveAttemptLogAtMs = now
|
|
1358
|
+
OneKeyLog.info(
|
|
1359
|
+
REORDER_LOG_TAG,
|
|
1360
|
+
"moveAttempt outcome=$outcome from=$from to=$to " +
|
|
1361
|
+
"fromType=${fromItem?.type.orEmpty()} toType=${toItem?.type.orEmpty()} " +
|
|
1362
|
+
"fromReorderable=${fromItem?.isReorderable} toReorderable=${toItem?.isReorderable} " +
|
|
1363
|
+
"sameSection=${fromItem != null && toItem != null && fromItem.sectionKey == toItem.sectionKey} " +
|
|
1364
|
+
"elapsedDragMs=${now - dragStartedAtMs} " +
|
|
1365
|
+
"scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
|
|
1366
|
+
"targetTop=$targetTop targetBottom=$targetBottom " +
|
|
1367
|
+
"viewportTop=${recyclerView.paddingTop} " +
|
|
1368
|
+
"viewportBottom=${recyclerView.height - recyclerView.paddingBottom}",
|
|
1369
|
+
)
|
|
1370
|
+
}
|
|
1228
1371
|
|
|
1229
1372
|
override fun isLongPressDragEnabled(): Boolean = false
|
|
1230
1373
|
|
|
@@ -1236,6 +1379,23 @@ class NativeListView(
|
|
|
1236
1379
|
reorderPlaceholderDecoration.position = position
|
|
1237
1380
|
reorderPlaceholderDecoration.color = reorderActiveBackground(next.theme)
|
|
1238
1381
|
compactWalletGroupDrag = item?.type == "walletGroup" && viewHolder != null
|
|
1382
|
+
dragType = item?.type
|
|
1383
|
+
dragStartedAtMs = SystemClock.uptimeMillis()
|
|
1384
|
+
lastAutoScrollLogAtMs = 0L
|
|
1385
|
+
lastDragFrameLogAtMs = 0L
|
|
1386
|
+
lastMoveAttemptLogAtMs = 0L
|
|
1387
|
+
lastMoveAttemptSignature = ""
|
|
1388
|
+
latestDragDx = 0f
|
|
1389
|
+
latestDragDy = 0f
|
|
1390
|
+
startReorderRelayoutLoop()
|
|
1391
|
+
OneKeyLog.info(
|
|
1392
|
+
REORDER_LOG_TAG,
|
|
1393
|
+
"start type=${dragType.orEmpty()} position=$position " +
|
|
1394
|
+
"compact=$compactWalletGroupDrag itemHeight=${viewHolder?.itemView?.height ?: -1} " +
|
|
1395
|
+
"viewport=${recyclerView.width}x${recyclerView.height} " +
|
|
1396
|
+
"padding=${recyclerView.paddingLeft},${recyclerView.paddingTop}," +
|
|
1397
|
+
"${recyclerView.paddingRight},${recyclerView.paddingBottom} density=$density",
|
|
1398
|
+
)
|
|
1239
1399
|
recyclerView.invalidate()
|
|
1240
1400
|
(viewHolder as? NativeListViewHolder)?.rowView?.setReorderActive(true)
|
|
1241
1401
|
if (compactWalletGroupDrag && viewHolder != null) {
|
|
@@ -1263,22 +1423,63 @@ class NativeListView(
|
|
|
1263
1423
|
val from = source.bindingAdapterPosition
|
|
1264
1424
|
val to = target.bindingAdapterPosition
|
|
1265
1425
|
val base = pendingReorder ?: adapter.currentList
|
|
1266
|
-
val fromItem = base.getOrNull(from)
|
|
1267
|
-
val toItem = base.getOrNull(to)
|
|
1268
|
-
|
|
1426
|
+
val fromItem = base.getOrNull(from)
|
|
1427
|
+
val toItem = base.getOrNull(to)
|
|
1428
|
+
val targetTop = layoutManager.getDecoratedTop(target.itemView)
|
|
1429
|
+
val targetBottom = layoutManager.getDecoratedBottom(target.itemView)
|
|
1430
|
+
val targetClipped =
|
|
1431
|
+
next.orientation != "horizontal" &&
|
|
1432
|
+
from != RecyclerView.NO_POSITION &&
|
|
1433
|
+
to != RecyclerView.NO_POSITION &&
|
|
1434
|
+
when {
|
|
1435
|
+
to > from -> targetBottom > recyclerView.height - recyclerView.paddingBottom
|
|
1436
|
+
to < from -> targetTop < recyclerView.paddingTop
|
|
1437
|
+
else -> false
|
|
1438
|
+
}
|
|
1439
|
+
val outcome = when {
|
|
1440
|
+
fromItem == null -> "missingSource"
|
|
1441
|
+
toItem == null -> "missingTarget"
|
|
1442
|
+
!fromItem.isReorderable -> "sourceNotReorderable"
|
|
1443
|
+
!toItem.isReorderable -> "targetNotReorderable"
|
|
1444
|
+
fromItem.sectionKey != toItem.sectionKey -> "sectionMismatch"
|
|
1445
|
+
targetClipped -> "targetClipped"
|
|
1446
|
+
else -> "accepted"
|
|
1447
|
+
}
|
|
1448
|
+
logMoveAttempt(recyclerView, from, to, fromItem, toItem, outcome, targetTop, targetBottom)
|
|
1449
|
+
if (outcome != "accepted" || fromItem == null || toItem == null) return false
|
|
1269
1450
|
val crossedPosition = dragTo != to
|
|
1270
1451
|
if (dragFrom == RecyclerView.NO_POSITION) dragFrom = from
|
|
1271
1452
|
dragTo = to
|
|
1272
1453
|
if (crossedPosition) {
|
|
1273
1454
|
recyclerView.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
|
1455
|
+
OneKeyLog.info(
|
|
1456
|
+
REORDER_LOG_TAG,
|
|
1457
|
+
"move type=${dragType.orEmpty()} from=$from to=$to " +
|
|
1458
|
+
"scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
|
|
1459
|
+
"dragDx=$latestDragDx dragDy=$latestDragDy",
|
|
1460
|
+
)
|
|
1274
1461
|
}
|
|
1275
1462
|
val displacedView = target.itemView
|
|
1276
1463
|
prepareDisplacedReorderAnimation(displacedView)
|
|
1277
|
-
val reordered = adapter.moveReordered(from, to)
|
|
1464
|
+
val reordered = adapter.moveReordered(from, to)
|
|
1465
|
+
if (reordered == null) {
|
|
1466
|
+
logMoveAttempt(
|
|
1467
|
+
recyclerView,
|
|
1468
|
+
from,
|
|
1469
|
+
to,
|
|
1470
|
+
fromItem,
|
|
1471
|
+
toItem,
|
|
1472
|
+
"adapterRejected",
|
|
1473
|
+
targetTop,
|
|
1474
|
+
targetBottom,
|
|
1475
|
+
)
|
|
1476
|
+
return false
|
|
1477
|
+
}
|
|
1278
1478
|
pendingReorder = reordered
|
|
1279
1479
|
reorderPlaceholderDecoration.position = to
|
|
1280
1480
|
recyclerView.invalidate()
|
|
1281
|
-
|
|
1481
|
+
// OneKey patch: the drag-scoped loop handles deferred nested RecyclerView layouts.
|
|
1482
|
+
// recyclerView.postOnAnimation(::relayoutRecyclerViewImmediately)
|
|
1282
1483
|
return true
|
|
1283
1484
|
}
|
|
1284
1485
|
|
|
@@ -1294,11 +1495,40 @@ class NativeListView(
|
|
|
1294
1495
|
isCurrentlyActive: Boolean,
|
|
1295
1496
|
) {
|
|
1296
1497
|
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
|
1498
|
+
latestDragDx = dX
|
|
1499
|
+
latestDragDy = dY
|
|
1297
1500
|
if (compactWalletGroupDrag) {
|
|
1298
1501
|
compactWalletGroupTop = viewHolder.itemView.top + dY
|
|
1299
1502
|
}
|
|
1300
1503
|
}
|
|
1301
1504
|
super.onChildDraw(canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
|
1505
|
+
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
|
1506
|
+
val now = SystemClock.uptimeMillis()
|
|
1507
|
+
if (
|
|
1508
|
+
lastDragFrameLogAtMs == 0L ||
|
|
1509
|
+
now - lastDragFrameLogAtMs >= REORDER_DRAG_FRAME_LOG_INTERVAL_MS
|
|
1510
|
+
) {
|
|
1511
|
+
lastDragFrameLogAtMs = now
|
|
1512
|
+
val itemView = viewHolder.itemView
|
|
1513
|
+
val visualTop = itemView.top + dY
|
|
1514
|
+
val visualBottom = itemView.bottom + dY
|
|
1515
|
+
OneKeyLog.info(
|
|
1516
|
+
REORDER_LOG_TAG,
|
|
1517
|
+
"frame position=${viewHolder.bindingAdapterPosition} active=$isCurrentlyActive " +
|
|
1518
|
+
"dragDx=$dX dragDy=$dY itemTop=${itemView.top} itemBottom=${itemView.bottom} " +
|
|
1519
|
+
"translationY=${itemView.translationY} visualTop=$visualTop visualBottom=$visualBottom " +
|
|
1520
|
+
"viewportTop=${recyclerView.paddingTop} " +
|
|
1521
|
+
"viewportBottom=${recyclerView.height - recyclerView.paddingBottom} " +
|
|
1522
|
+
"firstVisible=${layoutManager.findFirstVisibleItemPosition()} " +
|
|
1523
|
+
"lastVisible=${layoutManager.findLastVisibleItemPosition()} " +
|
|
1524
|
+
"canScrollUp=${recyclerView.canScrollVertically(-1)} " +
|
|
1525
|
+
"canScrollDown=${recyclerView.canScrollVertically(1)} " +
|
|
1526
|
+
"scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
|
|
1527
|
+
"layoutRequested=${recyclerView.isLayoutRequested} " +
|
|
1528
|
+
"computingLayout=${recyclerView.isComputingLayout}",
|
|
1529
|
+
)
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1302
1532
|
}
|
|
1303
1533
|
|
|
1304
1534
|
override fun interpolateOutOfBoundsScroll(
|
|
@@ -1308,35 +1538,60 @@ class NativeListView(
|
|
|
1308
1538
|
totalSize: Int,
|
|
1309
1539
|
msSinceStartScroll: Long,
|
|
1310
1540
|
): Int {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
else -> 0
|
|
1541
|
+
val effectiveViewSize: Int
|
|
1542
|
+
val effectiveOutOfBounds: Int
|
|
1543
|
+
if (compactWalletGroupDrag) {
|
|
1544
|
+
if (compactWalletGroupTop.isNaN()) return 0
|
|
1545
|
+
effectiveViewSize = dp(68)
|
|
1546
|
+
val compactTop = compactWalletGroupTop.roundToInt()
|
|
1547
|
+
effectiveOutOfBounds = when {
|
|
1548
|
+
compactTop < recyclerView.paddingTop -> compactTop - recyclerView.paddingTop
|
|
1549
|
+
compactTop + effectiveViewSize > recyclerView.height - recyclerView.paddingBottom ->
|
|
1550
|
+
compactTop + effectiveViewSize - (recyclerView.height - recyclerView.paddingBottom)
|
|
1551
|
+
else -> 0
|
|
1552
|
+
}
|
|
1553
|
+
if (effectiveOutOfBounds == 0) return 0
|
|
1554
|
+
} else {
|
|
1555
|
+
effectiveViewSize = viewSize
|
|
1556
|
+
effectiveOutOfBounds = viewSizeOutOfBounds
|
|
1328
1557
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1558
|
+
// OneKey patch: warm-start AndroidX's two-second quintic edge-scroll ramp.
|
|
1559
|
+
val acceleratedElapsedOutMs =
|
|
1560
|
+
msSinceStartScroll + REORDER_AUTOSCROLL_ACCELERATION_OFFSET_MS
|
|
1561
|
+
val result = super.interpolateOutOfBoundsScroll(
|
|
1331
1562
|
recyclerView,
|
|
1332
|
-
|
|
1333
|
-
|
|
1563
|
+
effectiveViewSize,
|
|
1564
|
+
effectiveOutOfBounds,
|
|
1334
1565
|
totalSize,
|
|
1335
|
-
msSinceStartScroll,
|
|
1566
|
+
// msSinceStartScroll,
|
|
1567
|
+
acceleratedElapsedOutMs,
|
|
1336
1568
|
)
|
|
1569
|
+
val now = SystemClock.uptimeMillis()
|
|
1570
|
+
if (
|
|
1571
|
+
lastAutoScrollLogAtMs == 0L ||
|
|
1572
|
+
now - lastAutoScrollLogAtMs >= REORDER_AUTOSCROLL_LOG_INTERVAL_MS
|
|
1573
|
+
) {
|
|
1574
|
+
lastAutoScrollLogAtMs = now
|
|
1575
|
+
OneKeyLog.info(
|
|
1576
|
+
REORDER_LOG_TAG,
|
|
1577
|
+
"autoScroll type=${dragType.orEmpty()} " +
|
|
1578
|
+
"compact=$compactWalletGroupDrag rawViewSize=$viewSize " +
|
|
1579
|
+
"effectiveViewSize=$effectiveViewSize rawOut=$viewSizeOutOfBounds " +
|
|
1580
|
+
"effectiveOut=$effectiveOutOfBounds totalSize=$totalSize " +
|
|
1581
|
+
"elapsedOutMs=$msSinceStartScroll " +
|
|
1582
|
+
"acceleratedElapsedOutMs=$acceleratedElapsedOutMs " +
|
|
1583
|
+
"elapsedDragMs=${now - dragStartedAtMs} " +
|
|
1584
|
+
"resultPx=$result scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
|
|
1585
|
+
"canScrollUp=${recyclerView.canScrollVertically(-1)} " +
|
|
1586
|
+
"canScrollDown=${recyclerView.canScrollVertically(1)} " +
|
|
1587
|
+
"dragDx=$latestDragDx dragDy=$latestDragDy compactTop=$compactWalletGroupTop",
|
|
1588
|
+
)
|
|
1589
|
+
}
|
|
1590
|
+
return result
|
|
1337
1591
|
}
|
|
1338
1592
|
|
|
1339
1593
|
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
|
1594
|
+
stopReorderRelayoutLoop()
|
|
1340
1595
|
val rowView = (viewHolder as? NativeListViewHolder)?.rowView
|
|
1341
1596
|
val draggedGroupKey = (rowView?.tag as? NativeListItem)?.key
|
|
1342
1597
|
super.clearView(recyclerView, viewHolder)
|
|
@@ -1349,6 +1604,18 @@ class NativeListView(
|
|
|
1349
1604
|
val from = dragFrom
|
|
1350
1605
|
val to = dragTo
|
|
1351
1606
|
val reordered = pendingReorder
|
|
1607
|
+
OneKeyLog.info(
|
|
1608
|
+
REORDER_LOG_TAG,
|
|
1609
|
+
"end type=${dragType.orEmpty()} from=$from to=$to " +
|
|
1610
|
+
"compact=$wasCompactWalletGroupDrag elapsedDragMs=" +
|
|
1611
|
+
"${SystemClock.uptimeMillis() - dragStartedAtMs} " +
|
|
1612
|
+
"scrollOffset=${recyclerView.computeVerticalScrollOffset()} " +
|
|
1613
|
+
"dragDx=$latestDragDx dragDy=$latestDragDy " +
|
|
1614
|
+
"firstVisible=${layoutManager.findFirstVisibleItemPosition()} " +
|
|
1615
|
+
"lastVisible=${layoutManager.findLastVisibleItemPosition()} " +
|
|
1616
|
+
"canScrollUp=${recyclerView.canScrollVertically(-1)} " +
|
|
1617
|
+
"canScrollDown=${recyclerView.canScrollVertically(1)}",
|
|
1618
|
+
)
|
|
1352
1619
|
var reorderPayload: JSONObject? = null
|
|
1353
1620
|
val destinationPosition = if (to != RecyclerView.NO_POSITION) {
|
|
1354
1621
|
to
|
|
@@ -1404,6 +1671,7 @@ class NativeListView(
|
|
|
1404
1671
|
dragFrom = RecyclerView.NO_POSITION
|
|
1405
1672
|
dragTo = RecyclerView.NO_POSITION
|
|
1406
1673
|
pendingReorder = null
|
|
1674
|
+
dragType = null
|
|
1407
1675
|
// The gesture is committed now; a later snapshot may supersede its async diff.
|
|
1408
1676
|
reorderPayload?.let { emit(REORDER, it) }
|
|
1409
1677
|
}
|
|
@@ -1600,6 +1868,53 @@ class NativeListView(
|
|
|
1600
1868
|
}
|
|
1601
1869
|
}
|
|
1602
1870
|
|
|
1871
|
+
private fun startReorderRelayoutLoop() {
|
|
1872
|
+
reorderRelayoutActive = true
|
|
1873
|
+
lastReorderRelayoutLogAtMs = 0L
|
|
1874
|
+
scheduleReorderRelayout()
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
private fun stopReorderRelayoutLoop() {
|
|
1878
|
+
reorderRelayoutActive = false
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
private fun scheduleReorderRelayout() {
|
|
1882
|
+
if (!reorderRelayoutActive || reorderRelayoutScheduled) return
|
|
1883
|
+
reorderRelayoutScheduled = true
|
|
1884
|
+
recyclerView.postOnAnimation {
|
|
1885
|
+
reorderRelayoutScheduled = false
|
|
1886
|
+
if (
|
|
1887
|
+
!reorderRelayoutActive ||
|
|
1888
|
+
disposed ||
|
|
1889
|
+
recyclerView.width <= 0 ||
|
|
1890
|
+
recyclerView.height <= 0
|
|
1891
|
+
) {
|
|
1892
|
+
return@postOnAnimation
|
|
1893
|
+
}
|
|
1894
|
+
val requestedBefore = recyclerView.isLayoutRequested
|
|
1895
|
+
val computingBefore = recyclerView.isComputingLayout
|
|
1896
|
+
if (requestedBefore && !computingBefore) {
|
|
1897
|
+
relayoutRecyclerViewImmediately()
|
|
1898
|
+
}
|
|
1899
|
+
val requestedAfter = recyclerView.isLayoutRequested
|
|
1900
|
+
val now = SystemClock.uptimeMillis()
|
|
1901
|
+
if (
|
|
1902
|
+
(requestedBefore || computingBefore || requestedAfter) &&
|
|
1903
|
+
(lastReorderRelayoutLogAtMs == 0L ||
|
|
1904
|
+
now - lastReorderRelayoutLogAtMs >= REORDER_RELAYOUT_LOG_INTERVAL_MS)
|
|
1905
|
+
) {
|
|
1906
|
+
lastReorderRelayoutLogAtMs = now
|
|
1907
|
+
OneKeyLog.info(
|
|
1908
|
+
REORDER_LOG_TAG,
|
|
1909
|
+
"relayout requestedBefore=$requestedBefore computingBefore=$computingBefore " +
|
|
1910
|
+
"requestedAfter=$requestedAfter computingAfter=${recyclerView.isComputingLayout} " +
|
|
1911
|
+
"scrollOffset=${recyclerView.computeVerticalScrollOffset()}",
|
|
1912
|
+
)
|
|
1913
|
+
}
|
|
1914
|
+
scheduleReorderRelayout()
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1603
1918
|
private fun relayoutRecyclerViewImmediately() {
|
|
1604
1919
|
if (disposed || recyclerView.isComputingLayout || recyclerView.width <= 0 || recyclerView.height <= 0) {
|
|
1605
1920
|
return
|
|
@@ -1619,10 +1934,20 @@ class NativeListView(
|
|
|
1619
1934
|
|
|
1620
1935
|
private fun dp(value: Int): Int = if (usesSelectorSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
|
|
1621
1936
|
|
|
1937
|
+
private fun sectionIndexDp(value: Int): Int = (value * density).roundToInt()
|
|
1938
|
+
|
|
1622
1939
|
companion object {
|
|
1940
|
+
private val MARKET_QUOTE_FIELDS = setOf(
|
|
1941
|
+
"revision",
|
|
1942
|
+
"price",
|
|
1943
|
+
"priceSegments",
|
|
1944
|
+
"change",
|
|
1945
|
+
"accessibilityLabel",
|
|
1946
|
+
)
|
|
1623
1947
|
private const val SECTION_INDEX_CONTENT_INSET_DP = 16
|
|
1624
1948
|
private const val SECTION_INDEX_RAIL_WIDTH_DP = 32
|
|
1625
|
-
private const val
|
|
1949
|
+
private const val SECTION_INDEX_PREVIEW_WIDTH_DP = 60
|
|
1950
|
+
private const val SECTION_INDEX_PREVIEW_HEIGHT_DP = 50
|
|
1626
1951
|
private const val SECTION_INDEX_PREVIEW_END_MARGIN_DP = 40
|
|
1627
1952
|
private const val REORDER_LONG_PRESS_MS = 200L
|
|
1628
1953
|
private const val REORDER_ALLOWABLE_MOVEMENT_DP = 10
|
|
@@ -1632,6 +1957,12 @@ class NativeListView(
|
|
|
1632
1957
|
private const val REORDER_SPRING_STIFFNESS = 400.0
|
|
1633
1958
|
private const val REORDER_SPRING_MASS = 0.4
|
|
1634
1959
|
private const val REORDER_SPRING_DURATION_MS = 300L
|
|
1960
|
+
private const val REORDER_AUTOSCROLL_ACCELERATION_OFFSET_MS = 1_500L
|
|
1961
|
+
private const val REORDER_AUTOSCROLL_LOG_INTERVAL_MS = 100L
|
|
1962
|
+
private const val REORDER_DRAG_FRAME_LOG_INTERVAL_MS = 100L
|
|
1963
|
+
private const val REORDER_MOVE_ATTEMPT_LOG_INTERVAL_MS = 250L
|
|
1964
|
+
private const val REORDER_RELAYOUT_LOG_INTERVAL_MS = 100L
|
|
1965
|
+
private const val REORDER_LOG_TAG = "NativeListReorder"
|
|
1635
1966
|
private const val ROW_ACTION = "rowAction"
|
|
1636
1967
|
private const val ACTION_ANCHOR_INVALIDATED = "actionAnchorInvalidated"
|
|
1637
1968
|
private const val SELECTION_DELTA = "selectionDelta"
|
|
@@ -1715,11 +2046,11 @@ private class NativeListSectionIndexView(
|
|
|
1715
2046
|
private val activeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
1716
2047
|
private val normalPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1717
2048
|
textAlign = Paint.Align.CENTER
|
|
1718
|
-
typeface = NativeListFonts.
|
|
2049
|
+
typeface = NativeListFonts.regular(context)
|
|
1719
2050
|
}
|
|
1720
2051
|
private val activePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1721
2052
|
textAlign = Paint.Align.CENTER
|
|
1722
|
-
typeface = NativeListFonts.
|
|
2053
|
+
typeface = NativeListFonts.medium(context)
|
|
1723
2054
|
}
|
|
1724
2055
|
|
|
1725
2056
|
init {
|
|
@@ -1756,40 +2087,27 @@ private class NativeListSectionIndexView(
|
|
|
1756
2087
|
override fun onDraw(canvas: Canvas) {
|
|
1757
2088
|
super.onDraw(canvas)
|
|
1758
2089
|
if (titles.isEmpty()) return
|
|
1759
|
-
val
|
|
1760
|
-
val
|
|
1761
|
-
val textSize =
|
|
2090
|
+
val metrics = indexMetrics()
|
|
2091
|
+
val visibleIndices = visibleLabelIndices(metrics.trackHeight)
|
|
2092
|
+
val textSize = 10f * resources.displayMetrics.scaledDensity
|
|
2093
|
+
val centerX = width - dp(10f)
|
|
2094
|
+
val badgeRadius = dp(7f)
|
|
1762
2095
|
normalPaint.color = normalColor
|
|
1763
2096
|
normalPaint.textSize = textSize
|
|
1764
2097
|
activePaint.color = activeColor
|
|
1765
2098
|
activePaint.textSize = textSize
|
|
1766
|
-
|
|
2099
|
+
visibleIndices.forEachIndexed { visibleIndex, index ->
|
|
2100
|
+
val title = titles[index]
|
|
1767
2101
|
val active = index == activeIndex
|
|
1768
2102
|
val paint = if (active) activePaint else normalPaint
|
|
1769
|
-
val centerY =
|
|
1770
|
-
if (active
|
|
1771
|
-
val badgeWidth = NativeListScale.dp(resources, 20f)
|
|
1772
|
-
val badgeHeight = minOf(cellHeight, NativeListScale.dp(resources, 16f))
|
|
2103
|
+
val centerY = visibleLabelCenterY(visibleIndex, visibleIndices.size, metrics)
|
|
2104
|
+
if (active) {
|
|
1773
2105
|
activeBackgroundPaint.color = activeColor
|
|
1774
|
-
canvas.
|
|
1775
|
-
width / 2f - badgeWidth / 2f,
|
|
1776
|
-
centerY - badgeHeight / 2f,
|
|
1777
|
-
width / 2f + badgeWidth / 2f,
|
|
1778
|
-
centerY + badgeHeight / 2f,
|
|
1779
|
-
badgeHeight / 2f,
|
|
1780
|
-
badgeHeight / 2f,
|
|
1781
|
-
activeBackgroundPaint,
|
|
1782
|
-
)
|
|
1783
|
-
}
|
|
1784
|
-
paint.color = if (active && cellHeight >= NativeListScale.dp(resources, 12f)) {
|
|
1785
|
-
activeTextColor
|
|
1786
|
-
} else if (active) {
|
|
1787
|
-
activeColor
|
|
1788
|
-
} else {
|
|
1789
|
-
normalColor
|
|
2106
|
+
canvas.drawCircle(centerX, centerY, badgeRadius, activeBackgroundPaint)
|
|
1790
2107
|
}
|
|
2108
|
+
paint.color = if (active) activeTextColor else normalColor
|
|
1791
2109
|
val baseline = centerY - (paint.descent() + paint.ascent()) / 2f
|
|
1792
|
-
canvas.drawText(title,
|
|
2110
|
+
canvas.drawText(title, centerX, baseline, paint)
|
|
1793
2111
|
}
|
|
1794
2112
|
}
|
|
1795
2113
|
|
|
@@ -1864,9 +2182,14 @@ private class NativeListSectionIndexView(
|
|
|
1864
2182
|
}
|
|
1865
2183
|
|
|
1866
2184
|
private fun selectAt(y: Float, interacting: Boolean) {
|
|
1867
|
-
val
|
|
1868
|
-
|
|
1869
|
-
val
|
|
2185
|
+
val metrics = indexMetrics()
|
|
2186
|
+
if (metrics.trackHeight <= 0f) return
|
|
2187
|
+
val visibleIndices = visibleLabelIndices(metrics.trackHeight).sorted()
|
|
2188
|
+
val visibleTrackHeight = minOf(metrics.trackHeight, dp(16f) * visibleIndices.size)
|
|
2189
|
+
val visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2f
|
|
2190
|
+
val progress = ((y - visibleOriginY) / visibleTrackHeight).coerceIn(0f, 1f)
|
|
2191
|
+
val slot = (progress * visibleIndices.size).toInt().coerceIn(0, visibleIndices.lastIndex)
|
|
2192
|
+
val index = visibleIndices[slot]
|
|
1870
2193
|
if (interacting && lastTouchIndex == index) return
|
|
1871
2194
|
lastTouchIndex = index.takeIf { interacting }
|
|
1872
2195
|
select(index, interacting)
|
|
@@ -1877,29 +2200,60 @@ private class NativeListSectionIndexView(
|
|
|
1877
2200
|
setActiveIndex(index)
|
|
1878
2201
|
}
|
|
1879
2202
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
.coerceAtLeast(1f)
|
|
2203
|
+
fun centerYForIndex(index: Int): Float = entryCenterY(index, indexMetrics())
|
|
2204
|
+
|
|
2205
|
+
private data class Metrics(val originY: Float, val trackHeight: Float)
|
|
1884
2206
|
|
|
1885
|
-
private fun
|
|
1886
|
-
|
|
1887
|
-
val
|
|
1888
|
-
|
|
2207
|
+
private fun indexMetrics(): Metrics {
|
|
2208
|
+
if (titles.isEmpty()) return Metrics(height / 2f, 0f)
|
|
2209
|
+
val edgePadding = dp(8f)
|
|
2210
|
+
val labelSpacing = dp(16f)
|
|
2211
|
+
val availableHeight = (height - edgePadding * 2f).coerceAtLeast(0f)
|
|
2212
|
+
val trackHeight = minOf(availableHeight, labelSpacing * titles.size)
|
|
2213
|
+
val centeredOriginY = (height - trackHeight) / 2f
|
|
2214
|
+
if (!centeredInWindow || !isAttachedToWindow) {
|
|
2215
|
+
return Metrics(centeredOriginY, trackHeight)
|
|
2216
|
+
}
|
|
1889
2217
|
val systemBarInsets = ViewCompat.getRootWindowInsets(rootView)
|
|
1890
2218
|
?.getInsetsIgnoringVisibility(
|
|
1891
2219
|
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
|
1892
|
-
) ?: return centeredOriginY
|
|
2220
|
+
) ?: return Metrics(centeredOriginY, trackHeight)
|
|
1893
2221
|
rootView.getLocationOnScreen(rootLocationOnScreen)
|
|
1894
2222
|
getLocationOnScreen(locationOnScreen)
|
|
1895
2223
|
val safeTop = rootLocationOnScreen[1] + systemBarInsets.top
|
|
1896
2224
|
val safeBottom = rootLocationOnScreen[1] + rootView.height - systemBarInsets.bottom
|
|
1897
|
-
if (safeBottom <= safeTop) return centeredOriginY
|
|
2225
|
+
if (safeBottom <= safeTop) return Metrics(centeredOriginY, trackHeight)
|
|
1898
2226
|
val localCenterY = (safeTop + safeBottom) / 2f - locationOnScreen[1]
|
|
1899
|
-
|
|
1900
|
-
|
|
2227
|
+
val maximumOrigin = (height - edgePadding - trackHeight).coerceAtLeast(edgePadding)
|
|
2228
|
+
return Metrics(
|
|
2229
|
+
(localCenterY - trackHeight / 2f).coerceIn(edgePadding, maximumOrigin),
|
|
2230
|
+
trackHeight,
|
|
2231
|
+
)
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
private fun entryCenterY(index: Int, metrics: Metrics): Float {
|
|
2235
|
+
if (titles.isEmpty()) return height / 2f
|
|
2236
|
+
return metrics.originY + metrics.trackHeight * (index + 0.5f) / titles.size
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
private fun visibleLabelCenterY(visibleIndex: Int, visibleCount: Int, metrics: Metrics): Float {
|
|
2240
|
+
val visibleTrackHeight = minOf(metrics.trackHeight, dp(16f) * visibleCount)
|
|
2241
|
+
val visibleOriginY = metrics.originY + (metrics.trackHeight - visibleTrackHeight) / 2f
|
|
2242
|
+
return visibleOriginY + visibleTrackHeight * (visibleIndex + 0.5f) / visibleCount.coerceAtLeast(1)
|
|
1901
2243
|
}
|
|
1902
2244
|
|
|
2245
|
+
private fun visibleLabelIndices(trackHeight: Float): Set<Int> {
|
|
2246
|
+
if (titles.isEmpty()) return emptySet()
|
|
2247
|
+
val maxVisible = max(1, (trackHeight / dp(16f)).toInt())
|
|
2248
|
+
if (titles.size <= maxVisible) return titles.indices.toSet()
|
|
2249
|
+
if (maxVisible == 1) return setOf(0)
|
|
2250
|
+
return (0 until maxVisible).mapTo(mutableSetOf()) { slot ->
|
|
2251
|
+
(slot.toFloat() * titles.lastIndex / (maxVisible - 1)).roundToInt()
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
private fun dp(value: Float): Float = value * resources.displayMetrics.density
|
|
2256
|
+
|
|
1903
2257
|
private fun updateContentDescription() {
|
|
1904
2258
|
contentDescription = activeIndex?.let { titles.getOrNull(it) }
|
|
1905
2259
|
?.let { "$ACCESSIBILITY_LABEL, $it" }
|