@onekeyfe/react-native-native-list 3.0.114 → 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.
- package/README.md +57 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +17 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +543 -26
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +404 -90
- package/ios/NativeListCell.swift +506 -20
- package/ios/NativeListDesignAssets.swift +8 -0
- package/ios/RNCNativeListView.swift +271 -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 +129 -1
- package/lib/module/web/NativeListWebEngine.js +461 -51
- package/lib/typescript/src/models.d.ts +82 -2
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +37 -3
- package/package.json +4 -2
- package/src/models.ts +121 -3
- package/src/validation.ts +206 -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,6 +91,8 @@ 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)
|
|
91
97
|
private val contentContainer = FrameLayout(context)
|
|
92
98
|
private val adapter = NativeListAdapter(reactContext)
|
|
@@ -148,6 +154,9 @@ class NativeListView(
|
|
|
148
154
|
private var dragFrom = RecyclerView.NO_POSITION
|
|
149
155
|
private var dragTo = RecyclerView.NO_POSITION
|
|
150
156
|
private var pendingReorder: List<NativeListItem>? = null
|
|
157
|
+
private var reorderRelayoutActive = false
|
|
158
|
+
private var reorderRelayoutScheduled = false
|
|
159
|
+
private var lastReorderRelayoutLogAtMs = 0L
|
|
151
160
|
private var sectionIndexEntries: List<NativeListSectionIndexEntry> = emptyList()
|
|
152
161
|
private var sectionIndexScrubbing = false
|
|
153
162
|
private var sectionIndexProgrammaticScroll = false
|
|
@@ -196,27 +205,28 @@ class NativeListView(
|
|
|
196
205
|
contentContainer.addView(
|
|
197
206
|
sectionIndexView,
|
|
198
207
|
FrameLayout.LayoutParams(
|
|
199
|
-
|
|
208
|
+
sectionIndexDp(SECTION_INDEX_RAIL_WIDTH_DP),
|
|
200
209
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
201
210
|
Gravity.END,
|
|
202
211
|
),
|
|
203
212
|
)
|
|
204
213
|
sectionIndexPreview.apply {
|
|
205
214
|
gravity = Gravity.CENTER
|
|
206
|
-
textSize =
|
|
207
|
-
typeface = NativeListFonts.
|
|
208
|
-
|
|
215
|
+
textSize = 30f
|
|
216
|
+
typeface = NativeListFonts.regular(context)
|
|
217
|
+
setPadding(0, 0, sectionIndexDp(10), 0)
|
|
218
|
+
visibility = INVISIBLE
|
|
209
219
|
alpha = 0f
|
|
210
220
|
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
211
221
|
}
|
|
212
222
|
contentContainer.addView(
|
|
213
223
|
sectionIndexPreview,
|
|
214
224
|
FrameLayout.LayoutParams(
|
|
215
|
-
|
|
216
|
-
|
|
225
|
+
sectionIndexDp(SECTION_INDEX_PREVIEW_WIDTH_DP),
|
|
226
|
+
sectionIndexDp(SECTION_INDEX_PREVIEW_HEIGHT_DP),
|
|
217
227
|
Gravity.CENTER_VERTICAL or Gravity.END,
|
|
218
228
|
).apply {
|
|
219
|
-
marginEnd =
|
|
229
|
+
marginEnd = sectionIndexDp(SECTION_INDEX_PREVIEW_END_MARGIN_DP)
|
|
220
230
|
},
|
|
221
231
|
)
|
|
222
232
|
addView(contentContainer, LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f))
|
|
@@ -318,6 +328,18 @@ class NativeListView(
|
|
|
318
328
|
}
|
|
319
329
|
return
|
|
320
330
|
}
|
|
331
|
+
// Keep the first Market row at its current pixel offset when it is
|
|
332
|
+
// reordered. RecyclerView otherwise follows the previous first key.
|
|
333
|
+
val marketStartOffset = if (
|
|
334
|
+
previous?.items?.firstOrNull()?.type == "market" &&
|
|
335
|
+
next.items.firstOrNull()?.type == "market" &&
|
|
336
|
+
previous.items.firstOrNull()?.key != next.items.firstOrNull()?.key &&
|
|
337
|
+
layoutManager.findFirstVisibleItemPosition() == 0
|
|
338
|
+
) {
|
|
339
|
+
layoutManager.findViewByPosition(0)?.let {
|
|
340
|
+
layoutManager.getDecoratedTop(it) - recyclerView.paddingTop
|
|
341
|
+
}
|
|
342
|
+
} else null
|
|
321
343
|
config = next
|
|
322
344
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
323
345
|
adapter.usesSelectorSourceScale = usesSelectorSourceScale
|
|
@@ -330,6 +352,9 @@ class NativeListView(
|
|
|
330
352
|
configureSectionIndex(next)
|
|
331
353
|
updateLayout(next)
|
|
332
354
|
adapter.submitList(next.items) {
|
|
355
|
+
if (marketStartOffset != null) {
|
|
356
|
+
layoutManager.scrollToPositionWithOffset(0, marketStartOffset)
|
|
357
|
+
}
|
|
333
358
|
relayoutContents()
|
|
334
359
|
performPendingScrollIfNeeded()
|
|
335
360
|
syncSectionIndexToVisibleRows()
|
|
@@ -500,9 +525,13 @@ class NativeListView(
|
|
|
500
525
|
}
|
|
501
526
|
val nextItems = current.items.toMutableList()
|
|
502
527
|
val selected = LinkedHashSet(current.selectedKeys)
|
|
528
|
+
var marketQuoteOnly = pending.isNotEmpty()
|
|
503
529
|
try {
|
|
504
530
|
pending.forEach { (index, changes) ->
|
|
505
531
|
val previous = nextItems[index]
|
|
532
|
+
marketQuoteOnly = marketQuoteOnly &&
|
|
533
|
+
previous.type == "market" &&
|
|
534
|
+
changes.keys().asSequence().all(MARKET_QUOTE_FIELDS::contains)
|
|
506
535
|
val merged = NativeListItem.parse(mergeRow(previous.json, changes))
|
|
507
536
|
nextItems[index] = merged
|
|
508
537
|
if (changes.has("selected")) {
|
|
@@ -512,7 +541,7 @@ class NativeListView(
|
|
|
512
541
|
} catch (_: Exception) {
|
|
513
542
|
return
|
|
514
543
|
}
|
|
515
|
-
invalidateActionAnchor("snapshot")
|
|
544
|
+
if (!marketQuoteOnly) invalidateActionAnchor("snapshot")
|
|
516
545
|
val next = current.copy(items = nextItems, selectedKeys = selected)
|
|
517
546
|
config = next
|
|
518
547
|
usesSelectorSourceScale = next.items.any { it.usesSelectorSourceScale }
|
|
@@ -843,6 +872,7 @@ class NativeListView(
|
|
|
843
872
|
|
|
844
873
|
fun dispose() {
|
|
845
874
|
if (disposed) return
|
|
875
|
+
stopReorderRelayoutLoop()
|
|
846
876
|
invalidateActionAnchor("destroy")
|
|
847
877
|
actionAnchor = null
|
|
848
878
|
disposed = true
|
|
@@ -872,11 +902,17 @@ class NativeListView(
|
|
|
872
902
|
val bottomPadding = next.contentPaddingBottom ?: defaultPadding
|
|
873
903
|
// OneKey patch: the section index overlays rows and keeps only an accessory-safe inset.
|
|
874
904
|
val indexGutter = if (sectionIndexEntries.isEmpty()) 0 else SECTION_INDEX_CONTENT_INSET_DP
|
|
905
|
+
// A native scroll coordinator may add header insets to this RecyclerView.
|
|
906
|
+
// Content/theme snapshots must replace only the padding owned by NativeList.
|
|
907
|
+
val coordinatorTopPadding = (recyclerView.paddingTop - configuredTopPaddingPx).coerceAtLeast(0)
|
|
908
|
+
val coordinatorBottomPadding = (recyclerView.paddingBottom - configuredBottomPaddingPx).coerceAtLeast(0)
|
|
909
|
+
configuredTopPaddingPx = dp(topPadding)
|
|
910
|
+
configuredBottomPaddingPx = dp(bottomPadding)
|
|
875
911
|
recyclerView.setPaddingRelative(
|
|
876
912
|
dp(horizontalPadding),
|
|
877
|
-
|
|
913
|
+
configuredTopPaddingPx + coordinatorTopPadding,
|
|
878
914
|
dp(horizontalPadding + indexGutter),
|
|
879
|
-
|
|
915
|
+
configuredBottomPaddingPx + coordinatorBottomPadding,
|
|
880
916
|
)
|
|
881
917
|
recyclerView.clipToPadding = false
|
|
882
918
|
recyclerView.isVerticalScrollBarEnabled = sectionIndexEntries.isEmpty()
|
|
@@ -907,17 +943,14 @@ class NativeListView(
|
|
|
907
943
|
sectionIndexHapticsEnabled = next.sectionIndexHapticsEnabled
|
|
908
944
|
sectionIndexView.configure(
|
|
909
945
|
sectionIndexEntries.map { it.title },
|
|
910
|
-
themeColor(next.theme, "
|
|
911
|
-
themeColor(next.theme, "
|
|
946
|
+
themeColor(next.theme, "disabledText", "#8D8D8D"),
|
|
947
|
+
themeColor(next.theme, "positive", "#218358"),
|
|
912
948
|
themeColor(next.theme, "inverseText", "#FCFCFC"),
|
|
913
949
|
next.sectionIndexCenteredInWindow,
|
|
914
950
|
)
|
|
915
951
|
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
|
-
}
|
|
952
|
+
sectionIndexPreview.setTextColor(Color.WHITE)
|
|
953
|
+
sectionIndexPreview.background = sectionIndexPreviewBackground()
|
|
921
954
|
sectionIndexView.setActiveIndex(
|
|
922
955
|
previousKey?.let { key -> sectionIndexEntries.indexOfFirst { it.key == key }.takeIf { it >= 0 } },
|
|
923
956
|
)
|
|
@@ -938,6 +971,7 @@ class NativeListView(
|
|
|
938
971
|
if (interacting) {
|
|
939
972
|
sectionIndexPreview.animate().cancel()
|
|
940
973
|
sectionIndexPreview.text = entry.title
|
|
974
|
+
positionSectionIndexPreview(index)
|
|
941
975
|
sectionIndexPreview.visibility = VISIBLE
|
|
942
976
|
sectionIndexPreview.alpha = 1f
|
|
943
977
|
if (changed && sectionIndexHapticsEnabled) {
|
|
@@ -953,16 +987,41 @@ class NativeListView(
|
|
|
953
987
|
sectionIndexPreview.animate().cancel()
|
|
954
988
|
if (immediately) {
|
|
955
989
|
sectionIndexPreview.alpha = 0f
|
|
956
|
-
sectionIndexPreview.visibility =
|
|
990
|
+
sectionIndexPreview.visibility = INVISIBLE
|
|
957
991
|
} else {
|
|
958
992
|
sectionIndexPreview.animate()
|
|
959
993
|
.alpha(0f)
|
|
960
994
|
.setDuration(150)
|
|
961
|
-
.withEndAction { sectionIndexPreview.visibility =
|
|
995
|
+
.withEndAction { sectionIndexPreview.visibility = INVISIBLE }
|
|
962
996
|
.start()
|
|
963
997
|
}
|
|
964
998
|
}
|
|
965
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
|
+
|
|
966
1025
|
private fun syncSectionIndexToVisibleRows() {
|
|
967
1026
|
if (sectionIndexScrubbing || sectionIndexProgrammaticScroll || sectionIndexEntries.isEmpty()) return
|
|
968
1027
|
val firstVisible = layoutManager.findFirstVisibleItemPosition()
|
|
@@ -1004,6 +1063,7 @@ class NativeListView(
|
|
|
1004
1063
|
updateSelection(NativeSelectionTarget("row", item.key), item.key)
|
|
1005
1064
|
} else {
|
|
1006
1065
|
val actionKey = when {
|
|
1066
|
+
item.type == "market" && item.json.optString("pressActionKey").isNotEmpty() -> item.json.optString("pressActionKey")
|
|
1007
1067
|
item.type == "action" -> item.json.optString("actionKey")
|
|
1008
1068
|
item.type == "system" && item.json.optString("variant") == "retry" -> item.json.optString("actionKey")
|
|
1009
1069
|
else -> "press"
|
|
@@ -1211,6 +1271,7 @@ class NativeListView(
|
|
|
1211
1271
|
}
|
|
1212
1272
|
|
|
1213
1273
|
private fun updateReordering(next: NativeListConfig) {
|
|
1274
|
+
stopReorderRelayoutLoop()
|
|
1214
1275
|
reorderTouchHandler?.removeCallbacksAndMessages(null)
|
|
1215
1276
|
reorderTouchHandler = null
|
|
1216
1277
|
reorderTouchListener?.let(recyclerView::removeOnItemTouchListener)
|
|
@@ -1225,6 +1286,48 @@ class NativeListView(
|
|
|
1225
1286
|
) {
|
|
1226
1287
|
private var compactWalletGroupDrag = false
|
|
1227
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
|
+
}
|
|
1228
1331
|
|
|
1229
1332
|
override fun isLongPressDragEnabled(): Boolean = false
|
|
1230
1333
|
|
|
@@ -1236,6 +1339,23 @@ class NativeListView(
|
|
|
1236
1339
|
reorderPlaceholderDecoration.position = position
|
|
1237
1340
|
reorderPlaceholderDecoration.color = reorderActiveBackground(next.theme)
|
|
1238
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
|
+
)
|
|
1239
1359
|
recyclerView.invalidate()
|
|
1240
1360
|
(viewHolder as? NativeListViewHolder)?.rowView?.setReorderActive(true)
|
|
1241
1361
|
if (compactWalletGroupDrag && viewHolder != null) {
|
|
@@ -1263,22 +1383,63 @@ class NativeListView(
|
|
|
1263
1383
|
val from = source.bindingAdapterPosition
|
|
1264
1384
|
val to = target.bindingAdapterPosition
|
|
1265
1385
|
val base = pendingReorder ?: adapter.currentList
|
|
1266
|
-
val fromItem = base.getOrNull(from)
|
|
1267
|
-
val toItem = base.getOrNull(to)
|
|
1268
|
-
|
|
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
|
|
1269
1410
|
val crossedPosition = dragTo != to
|
|
1270
1411
|
if (dragFrom == RecyclerView.NO_POSITION) dragFrom = from
|
|
1271
1412
|
dragTo = to
|
|
1272
1413
|
if (crossedPosition) {
|
|
1273
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
|
+
)
|
|
1274
1421
|
}
|
|
1275
1422
|
val displacedView = target.itemView
|
|
1276
1423
|
prepareDisplacedReorderAnimation(displacedView)
|
|
1277
|
-
val reordered = adapter.moveReordered(from, to)
|
|
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
|
+
}
|
|
1278
1438
|
pendingReorder = reordered
|
|
1279
1439
|
reorderPlaceholderDecoration.position = to
|
|
1280
1440
|
recyclerView.invalidate()
|
|
1281
|
-
|
|
1441
|
+
// OneKey patch: the drag-scoped loop handles deferred nested RecyclerView layouts.
|
|
1442
|
+
// recyclerView.postOnAnimation(::relayoutRecyclerViewImmediately)
|
|
1282
1443
|
return true
|
|
1283
1444
|
}
|
|
1284
1445
|
|
|
@@ -1294,11 +1455,40 @@ class NativeListView(
|
|
|
1294
1455
|
isCurrentlyActive: Boolean,
|
|
1295
1456
|
) {
|
|
1296
1457
|
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
|
1458
|
+
latestDragDx = dX
|
|
1459
|
+
latestDragDy = dY
|
|
1297
1460
|
if (compactWalletGroupDrag) {
|
|
1298
1461
|
compactWalletGroupTop = viewHolder.itemView.top + dY
|
|
1299
1462
|
}
|
|
1300
1463
|
}
|
|
1301
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
|
+
}
|
|
1302
1492
|
}
|
|
1303
1493
|
|
|
1304
1494
|
override fun interpolateOutOfBoundsScroll(
|
|
@@ -1308,35 +1498,60 @@ class NativeListView(
|
|
|
1308
1498
|
totalSize: Int,
|
|
1309
1499
|
msSinceStartScroll: Long,
|
|
1310
1500
|
): Int {
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
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
|
|
1328
1517
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
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(
|
|
1331
1522
|
recyclerView,
|
|
1332
|
-
|
|
1333
|
-
|
|
1523
|
+
effectiveViewSize,
|
|
1524
|
+
effectiveOutOfBounds,
|
|
1334
1525
|
totalSize,
|
|
1335
|
-
msSinceStartScroll,
|
|
1526
|
+
// msSinceStartScroll,
|
|
1527
|
+
acceleratedElapsedOutMs,
|
|
1336
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
|
|
1337
1551
|
}
|
|
1338
1552
|
|
|
1339
1553
|
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
|
1554
|
+
stopReorderRelayoutLoop()
|
|
1340
1555
|
val rowView = (viewHolder as? NativeListViewHolder)?.rowView
|
|
1341
1556
|
val draggedGroupKey = (rowView?.tag as? NativeListItem)?.key
|
|
1342
1557
|
super.clearView(recyclerView, viewHolder)
|
|
@@ -1349,6 +1564,18 @@ class NativeListView(
|
|
|
1349
1564
|
val from = dragFrom
|
|
1350
1565
|
val to = dragTo
|
|
1351
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
|
+
)
|
|
1352
1579
|
var reorderPayload: JSONObject? = null
|
|
1353
1580
|
val destinationPosition = if (to != RecyclerView.NO_POSITION) {
|
|
1354
1581
|
to
|
|
@@ -1404,6 +1631,7 @@ class NativeListView(
|
|
|
1404
1631
|
dragFrom = RecyclerView.NO_POSITION
|
|
1405
1632
|
dragTo = RecyclerView.NO_POSITION
|
|
1406
1633
|
pendingReorder = null
|
|
1634
|
+
dragType = null
|
|
1407
1635
|
// The gesture is committed now; a later snapshot may supersede its async diff.
|
|
1408
1636
|
reorderPayload?.let { emit(REORDER, it) }
|
|
1409
1637
|
}
|
|
@@ -1600,6 +1828,53 @@ class NativeListView(
|
|
|
1600
1828
|
}
|
|
1601
1829
|
}
|
|
1602
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
|
+
|
|
1603
1878
|
private fun relayoutRecyclerViewImmediately() {
|
|
1604
1879
|
if (disposed || recyclerView.isComputingLayout || recyclerView.width <= 0 || recyclerView.height <= 0) {
|
|
1605
1880
|
return
|
|
@@ -1619,10 +1894,20 @@ class NativeListView(
|
|
|
1619
1894
|
|
|
1620
1895
|
private fun dp(value: Int): Int = if (usesSelectorSourceScale) (value * resources.displayMetrics.density).roundToInt() else NativeListScale.dp(resources, value)
|
|
1621
1896
|
|
|
1897
|
+
private fun sectionIndexDp(value: Int): Int = (value * density).roundToInt()
|
|
1898
|
+
|
|
1622
1899
|
companion object {
|
|
1900
|
+
private val MARKET_QUOTE_FIELDS = setOf(
|
|
1901
|
+
"revision",
|
|
1902
|
+
"price",
|
|
1903
|
+
"priceSegments",
|
|
1904
|
+
"change",
|
|
1905
|
+
"accessibilityLabel",
|
|
1906
|
+
)
|
|
1623
1907
|
private const val SECTION_INDEX_CONTENT_INSET_DP = 16
|
|
1624
1908
|
private const val SECTION_INDEX_RAIL_WIDTH_DP = 32
|
|
1625
|
-
private const val
|
|
1909
|
+
private const val SECTION_INDEX_PREVIEW_WIDTH_DP = 60
|
|
1910
|
+
private const val SECTION_INDEX_PREVIEW_HEIGHT_DP = 50
|
|
1626
1911
|
private const val SECTION_INDEX_PREVIEW_END_MARGIN_DP = 40
|
|
1627
1912
|
private const val REORDER_LONG_PRESS_MS = 200L
|
|
1628
1913
|
private const val REORDER_ALLOWABLE_MOVEMENT_DP = 10
|
|
@@ -1632,6 +1917,12 @@ class NativeListView(
|
|
|
1632
1917
|
private const val REORDER_SPRING_STIFFNESS = 400.0
|
|
1633
1918
|
private const val REORDER_SPRING_MASS = 0.4
|
|
1634
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"
|
|
1635
1926
|
private const val ROW_ACTION = "rowAction"
|
|
1636
1927
|
private const val ACTION_ANCHOR_INVALIDATED = "actionAnchorInvalidated"
|
|
1637
1928
|
private const val SELECTION_DELTA = "selectionDelta"
|
|
@@ -1715,11 +2006,11 @@ private class NativeListSectionIndexView(
|
|
|
1715
2006
|
private val activeBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
1716
2007
|
private val normalPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1717
2008
|
textAlign = Paint.Align.CENTER
|
|
1718
|
-
typeface = NativeListFonts.
|
|
2009
|
+
typeface = NativeListFonts.regular(context)
|
|
1719
2010
|
}
|
|
1720
2011
|
private val activePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1721
2012
|
textAlign = Paint.Align.CENTER
|
|
1722
|
-
typeface = NativeListFonts.
|
|
2013
|
+
typeface = NativeListFonts.medium(context)
|
|
1723
2014
|
}
|
|
1724
2015
|
|
|
1725
2016
|
init {
|
|
@@ -1756,40 +2047,27 @@ private class NativeListSectionIndexView(
|
|
|
1756
2047
|
override fun onDraw(canvas: Canvas) {
|
|
1757
2048
|
super.onDraw(canvas)
|
|
1758
2049
|
if (titles.isEmpty()) return
|
|
1759
|
-
val
|
|
1760
|
-
val
|
|
1761
|
-
val textSize =
|
|
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)
|
|
1762
2055
|
normalPaint.color = normalColor
|
|
1763
2056
|
normalPaint.textSize = textSize
|
|
1764
2057
|
activePaint.color = activeColor
|
|
1765
2058
|
activePaint.textSize = textSize
|
|
1766
|
-
|
|
2059
|
+
visibleIndices.forEachIndexed { visibleIndex, index ->
|
|
2060
|
+
val title = titles[index]
|
|
1767
2061
|
val active = index == activeIndex
|
|
1768
2062
|
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))
|
|
2063
|
+
val centerY = visibleLabelCenterY(visibleIndex, visibleIndices.size, metrics)
|
|
2064
|
+
if (active) {
|
|
1773
2065
|
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
|
|
2066
|
+
canvas.drawCircle(centerX, centerY, badgeRadius, activeBackgroundPaint)
|
|
1790
2067
|
}
|
|
2068
|
+
paint.color = if (active) activeTextColor else normalColor
|
|
1791
2069
|
val baseline = centerY - (paint.descent() + paint.ascent()) / 2f
|
|
1792
|
-
canvas.drawText(title,
|
|
2070
|
+
canvas.drawText(title, centerX, baseline, paint)
|
|
1793
2071
|
}
|
|
1794
2072
|
}
|
|
1795
2073
|
|
|
@@ -1864,9 +2142,14 @@ private class NativeListSectionIndexView(
|
|
|
1864
2142
|
}
|
|
1865
2143
|
|
|
1866
2144
|
private fun selectAt(y: Float, interacting: Boolean) {
|
|
1867
|
-
val
|
|
1868
|
-
|
|
1869
|
-
val
|
|
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]
|
|
1870
2153
|
if (interacting && lastTouchIndex == index) return
|
|
1871
2154
|
lastTouchIndex = index.takeIf { interacting }
|
|
1872
2155
|
select(index, interacting)
|
|
@@ -1877,29 +2160,60 @@ private class NativeListSectionIndexView(
|
|
|
1877
2160
|
setActiveIndex(index)
|
|
1878
2161
|
}
|
|
1879
2162
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
.coerceAtLeast(1f)
|
|
2163
|
+
fun centerYForIndex(index: Int): Float = entryCenterY(index, indexMetrics())
|
|
2164
|
+
|
|
2165
|
+
private data class Metrics(val originY: Float, val trackHeight: Float)
|
|
1884
2166
|
|
|
1885
|
-
private fun
|
|
1886
|
-
|
|
1887
|
-
val
|
|
1888
|
-
|
|
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
|
+
}
|
|
1889
2177
|
val systemBarInsets = ViewCompat.getRootWindowInsets(rootView)
|
|
1890
2178
|
?.getInsetsIgnoringVisibility(
|
|
1891
2179
|
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
|
1892
|
-
) ?: return centeredOriginY
|
|
2180
|
+
) ?: return Metrics(centeredOriginY, trackHeight)
|
|
1893
2181
|
rootView.getLocationOnScreen(rootLocationOnScreen)
|
|
1894
2182
|
getLocationOnScreen(locationOnScreen)
|
|
1895
2183
|
val safeTop = rootLocationOnScreen[1] + systemBarInsets.top
|
|
1896
2184
|
val safeBottom = rootLocationOnScreen[1] + rootView.height - systemBarInsets.bottom
|
|
1897
|
-
if (safeBottom <= safeTop) return centeredOriginY
|
|
2185
|
+
if (safeBottom <= safeTop) return Metrics(centeredOriginY, trackHeight)
|
|
1898
2186
|
val localCenterY = (safeTop + safeBottom) / 2f - locationOnScreen[1]
|
|
1899
|
-
|
|
1900
|
-
|
|
2187
|
+
val maximumOrigin = (height - edgePadding - trackHeight).coerceAtLeast(edgePadding)
|
|
2188
|
+
return Metrics(
|
|
2189
|
+
(localCenterY - trackHeight / 2f).coerceIn(edgePadding, maximumOrigin),
|
|
2190
|
+
trackHeight,
|
|
2191
|
+
)
|
|
1901
2192
|
}
|
|
1902
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
|
+
|
|
1903
2217
|
private fun updateContentDescription() {
|
|
1904
2218
|
contentDescription = activeIndex?.let { titles.getOrNull(it) }
|
|
1905
2219
|
?.let { "$ACCESSIBILITY_LABEL, $it" }
|