@onekeyfe/react-native-native-list 6.7.0-alpha.375 → 6.7.0-alpha.382
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/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +14 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +240 -11
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -4
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +6 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListButtonGroupRenderer.kt +102 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +12 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +87 -7
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +177 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +43 -14
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +33 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +22 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +2 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +3 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +69 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +55 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +1912 -107
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListPressPolicyTest.kt +1 -0
- package/docs/ROW_TEMPLATES.md +60 -9
- package/docs/SPEC.md +450 -5
- package/docs/STYLE_SPEC.md +6 -0
- package/ios/HybridNativeList.swift +21 -2
- package/ios/NativeListAccessoryStack.swift +227 -39
- package/ios/NativeListActivityDetailsView.swift +18 -8
- package/ios/NativeListActivityRenderer.swift +8 -2
- package/ios/NativeListButtonGroupRenderer.swift +89 -0
- package/ios/NativeListDataRowRenderer.swift +155 -27
- package/ios/NativeListDesignAssets.swift +3 -0
- package/ios/NativeListIdentityRenderer.swift +166 -30
- package/ios/NativeListLeadingVisual.swift +49 -14
- package/ios/NativeListMessageRenderer.swift +34 -1
- package/ios/NativeListModels.swift +42 -2
- package/ios/NativeListRendererRegistry.swift +1 -0
- package/ios/NativeListResolvedText.swift +2 -1
- package/ios/NativeListRowHost.swift +14 -0
- package/ios/NativeListSectionHeaderRenderer.swift +88 -0
- package/ios/RNCNativeListView.swift +1013 -55
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_crypto_coin_outline.imageset/icon.svg +4 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_gas_solid.imageset/icon.svg +3 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/Contents.json +15 -0
- package/ios/Resources/NativeListIcons.xcassets/onekey_refresh_ccw_outline.imageset/icon.svg +3 -0
- package/ios/tests/NativeListActivityPresentationTests.swift +56 -0
- package/ios/tests/NativeListLifecycleTests.swift +2720 -3
- package/lib/module/NativeList.js +175 -28
- package/lib/module/NativeList.web.js +129 -30
- package/lib/module/index.js +1 -1
- package/lib/module/models.js +1 -0
- package/lib/module/selection.js +1 -1
- package/lib/module/validation.js +421 -8
- package/lib/module/web/NativeListWebEngine.js +507 -19
- package/lib/module/web/templates/ActivityRowRenderer.js +8 -7
- package/lib/module/web/templates/ButtonGroupRowRenderer.js +47 -0
- package/lib/module/web/templates/DataRowRenderer.js +40 -5
- package/lib/module/web/templates/IdentityRowRenderer.js +90 -4
- package/lib/module/web/templates/MessageRowRenderer.js +18 -2
- package/lib/module/web/templates/RowContainerStyle.js +1 -0
- package/lib/module/web/templates/RowRendererRegistry.js +3 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +36 -2
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/lib/typescript/src/NativeList.d.ts +1 -1
- package/lib/typescript/src/NativeList.nitro.d.ts +2 -0
- package/lib/typescript/src/NativeList.types.d.ts +18 -0
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/models.d.ts +105 -9
- package/lib/typescript/src/validation.d.ts +12 -0
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +32 -0
- package/lib/typescript/src/web/templates/ButtonGroupRowRenderer.d.ts +14 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +1 -1
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +9 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +26 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +10 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +20 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +12 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +2 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +43 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +4 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +6 -0
- package/nitrogen/generated/shared/json/NativeListConfig.json +2 -0
- package/package.json +3 -3
- package/react-native-native-list.podspec +1 -0
- package/src/NativeList.nitro.ts +2 -0
- package/src/NativeList.tsx +313 -44
- package/src/NativeList.types.ts +22 -0
- package/src/NativeList.web.tsx +204 -48
- package/src/index.ts +2 -0
- package/src/models.ts +117 -3
- package/src/selection.ts +1 -0
- package/src/validation.ts +852 -6
- package/src/web/NativeListWebEngine.ts +803 -20
- package/src/web/templates/ActivityRowRenderer.ts +14 -6
- package/src/web/templates/ButtonGroupRowRenderer.ts +58 -0
- package/src/web/templates/DataRowRenderer.ts +93 -20
- package/src/web/templates/IdentityRowRenderer.ts +150 -5
- package/src/web/templates/MessageRowRenderer.ts +23 -7
- package/src/web/templates/RowContainerStyle.ts +2 -0
- package/src/web/templates/RowRendererRegistry.ts +11 -1
- package/src/web/templates/SectionHeaderRowRenderer.ts +47 -4
|
@@ -30,11 +30,20 @@ final class NativeListView: UIView {
|
|
|
30
30
|
}
|
|
31
31
|
private var refreshEmittedForDrag = false
|
|
32
32
|
private var minimumDragOffset: CGFloat = 0
|
|
33
|
-
private var containerSlotHeights: [CGFloat] = [0, 0, 0]
|
|
33
|
+
private var containerSlotHeights: [CGFloat] = [0, 0, 0, 0, 0, 0]
|
|
34
|
+
private var disclosureCVisibleHeight: CGFloat = 0
|
|
35
|
+
private var disclosureInset: CGFloat = 0
|
|
36
|
+
private var disclosureStartCHeight: CGFloat = 0
|
|
37
|
+
private var disclosureStartInset: CGFloat = 0
|
|
38
|
+
private var disclosureSegmentStartEased: CFTimeInterval = 0
|
|
39
|
+
private var deferredDisclosureGeometryInvalid = false
|
|
40
|
+
private var pendingDisclosureSettlement: (id: String, expanded: Bool)?
|
|
34
41
|
// Fabric keeps assigning each slot root the frame React computed (inset, 0, width, height).
|
|
35
42
|
// Each root therefore lives in a list-owned host whose origin the list alone controls, so a
|
|
36
43
|
// React layout update can never move a slot out of its native position, even for a frame.
|
|
37
44
|
private var reactContainerSlots: [(content: UIView, host: UIView)] = []
|
|
45
|
+
private var sidecarLayout: (width: CGFloat, gap: CGFloat, maxContentWidth: CGFloat)?
|
|
46
|
+
private let pagerStickyHeights = NSMapTable<AnyObject, NSNumber>(keyOptions: .weakMemory, valueOptions: .strongMemory)
|
|
38
47
|
private let topOverlayHost = UIView()
|
|
39
48
|
private weak var topOverlayContent: UIView?
|
|
40
49
|
private var topEdgeInteraction: UIInteraction?
|
|
@@ -47,16 +56,239 @@ final class NativeListView: UIView {
|
|
|
47
56
|
private var lastLaidOutContentHeight: CGFloat?
|
|
48
57
|
private var needsContentShrinkCorrection = false
|
|
49
58
|
private var contentShrinkCorrectionScheduled = false
|
|
59
|
+
private var consumedStructuralTransitionID: String?
|
|
60
|
+
private var structuralDisplayLink: CADisplayLink?
|
|
61
|
+
private var structuralScrollPhaseActive = false
|
|
62
|
+
private var structuralSpacerHeight: CGFloat = 0
|
|
63
|
+
private var structuralAnimationStart: CFTimeInterval = 0
|
|
64
|
+
private var structuralAnimationDuration: CFTimeInterval = 0
|
|
65
|
+
private var structuralStartSpacer: CGFloat = 0
|
|
66
|
+
private var structuralStartOffset: CGFloat = 0
|
|
67
|
+
private var structuralTargetOffset: CGFloat = 0
|
|
68
|
+
private var structuralScrollInterrupted = false
|
|
69
|
+
private var structuralFooterStartY: CGFloat?
|
|
70
|
+
private var structuralFooterProgress: CGFloat = 0
|
|
71
|
+
private var structuralRemovedItems: [String: NativeListItem] = [:]
|
|
72
|
+
private var structuralRemovedHeights: [String: CGFloat] = [:]
|
|
73
|
+
private var structuralRemovalProgress: CGFloat = 0
|
|
74
|
+
private final class StructuralTail {
|
|
75
|
+
var items: [NativeListItem]
|
|
76
|
+
let starts: [CGFloat]
|
|
77
|
+
let heights: [CGFloat]
|
|
78
|
+
let indexByKey: [String: Int]
|
|
79
|
+
let extent: CGFloat
|
|
80
|
+
let prefixCount: Int
|
|
81
|
+
let finalExtent: CGFloat
|
|
82
|
+
let retiringKeys: Set<String>
|
|
83
|
+
init(items: [NativeListItem], heights: [CGFloat], spacing: CGFloat,
|
|
84
|
+
prefixCount: Int, finalExtent: CGFloat) {
|
|
85
|
+
self.items = items
|
|
86
|
+
self.heights = heights
|
|
87
|
+
self.indexByKey = Dictionary(uniqueKeysWithValues: items.enumerated().map { ($1.key, $0) })
|
|
88
|
+
self.prefixCount = prefixCount
|
|
89
|
+
self.finalExtent = finalExtent
|
|
90
|
+
self.retiringKeys = finalExtent == 0 ? Set(items.map(\.key)) : []
|
|
91
|
+
var edge: CGFloat = prefixCount == 0 ? 0 : spacing
|
|
92
|
+
var starts: [CGFloat] = []
|
|
93
|
+
for (index, height) in heights.enumerated() {
|
|
94
|
+
starts.append(edge)
|
|
95
|
+
edge += height + (index + 1 < heights.count ? spacing : 0)
|
|
96
|
+
}
|
|
97
|
+
self.starts = starts
|
|
98
|
+
self.extent = edge
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
private var structuralTail: StructuralTail?
|
|
102
|
+
private var structuralTailStartExtent: CGFloat = 0
|
|
103
|
+
private var structuralTailSegmentStartEased: CFTimeInterval = 0
|
|
104
|
+
private var structuralTailStartY: CGFloat = 0
|
|
105
|
+
private func prefixTailCount(from old: NativeListConfig, to next: NativeListConfig) -> Int? {
|
|
106
|
+
guard old.orientation == "vertical", next.orientation == "vertical",
|
|
107
|
+
old.layout == next.layout, next.layout == "linear" || next.layout == "table",
|
|
108
|
+
old.itemSpacing == next.itemSpacing,
|
|
109
|
+
old.contentPaddingTop == next.contentPaddingTop,
|
|
110
|
+
old.contentPaddingBottom == next.contentPaddingBottom,
|
|
111
|
+
old.contentPaddingHorizontal == next.contentPaddingHorizontal,
|
|
112
|
+
dictionariesEqual(old.theme, next.theme),
|
|
113
|
+
dictionariesEqual(old.listStyle, next.listStyle),
|
|
114
|
+
old.items.count != next.items.count,
|
|
115
|
+
structuralDisplayLink == nil || structuralTail != nil,
|
|
116
|
+
!old.items.contains(where: { $0.type == "sectionHeader" }),
|
|
117
|
+
!next.items.contains(where: { $0.type == "sectionHeader" }) else { return nil }
|
|
118
|
+
let prefix = min(old.items.count, next.items.count)
|
|
119
|
+
guard prefix > 0 else { return nil }
|
|
120
|
+
for index in 0..<prefix {
|
|
121
|
+
let before = old.items[index], after = next.items[index]
|
|
122
|
+
guard before.key == after.key, before.type == after.type,
|
|
123
|
+
rowHeight(before) == rowHeight(after) else { return nil }
|
|
124
|
+
}
|
|
125
|
+
return prefix
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private func tailExceedsHostLimit(_ tail: StructuralTail) -> Bool {
|
|
129
|
+
let window = max(0, collectionView.bounds.height) * 3
|
|
130
|
+
var end = 0
|
|
131
|
+
for first in tail.items.indices {
|
|
132
|
+
end = max(end, first)
|
|
133
|
+
while end < tail.items.count && tail.starts[end] <= tail.starts[first] + window {
|
|
134
|
+
end += 1
|
|
135
|
+
}
|
|
136
|
+
if end - first > 128 { return true }
|
|
137
|
+
}
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private func clearStructuralTail() {
|
|
142
|
+
structuralTail = nil
|
|
143
|
+
flowLayout.animatedBodyContentHeight = nil
|
|
144
|
+
flowLayout.tailClip = nil
|
|
145
|
+
for case let cell as NativeListRowHost in collectionView.visibleCells {
|
|
146
|
+
let indexPath = collectionView.indexPath(for: cell)
|
|
147
|
+
let key = indexPath.flatMap { dataSource?.itemIdentifier(for: $0) }
|
|
148
|
+
let accepted = indexPath.flatMap { config?.items[safe: $0.item]?.key } == key && key != nil
|
|
149
|
+
cell.layer.mask = nil
|
|
150
|
+
cell.isHidden = !accepted
|
|
151
|
+
cell.accessibilityElementsHidden = !accepted
|
|
152
|
+
cell.isUserInteractionEnabled = accepted && !(key.flatMap { itemsByKey[$0] }?.data.bool("disabled") ?? false)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private func updateStructuralTailWindow() {
|
|
157
|
+
guard let tail = structuralTail else { return }
|
|
158
|
+
let clipEnd = structuralTailStartY + structuralSpacerHeight
|
|
159
|
+
flowLayout.animatedBodyContentHeight = clipEnd
|
|
160
|
+
+ (config?.contentPaddingBottom ?? 0) + containerSlotHeights[2]
|
|
161
|
+
+ (config?.disclosure == nil ? 0 : disclosureCVisibleHeight
|
|
162
|
+
+ containerSlotHeights[5] + disclosureInset)
|
|
163
|
+
flowLayout.tailClip = (tail.prefixCount, clipEnd)
|
|
164
|
+
for case let cell as NativeListRowHost in collectionView.visibleCells {
|
|
165
|
+
guard let indexPath = collectionView.indexPath(for: cell),
|
|
166
|
+
let key = dataSource.itemIdentifier(for: indexPath) else { continue }
|
|
167
|
+
presentTailCell(cell, key: key, index: indexPath.item, tail: tail)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func presentTailCell(_ cell: NativeListRowHost, key: String, index: Int,
|
|
172
|
+
tail: StructuralTail) {
|
|
173
|
+
let clipEnd = structuralTailStartY + structuralSpacerHeight
|
|
174
|
+
let visibleHeight = index >= tail.prefixCount
|
|
175
|
+
? max(0, min(cell.bounds.height, clipEnd - cell.frame.minY)) : cell.bounds.height
|
|
176
|
+
let clipped = visibleHeight < cell.bounds.height
|
|
177
|
+
CATransaction.begin()
|
|
178
|
+
CATransaction.setDisableActions(true)
|
|
179
|
+
cell.isHidden = visibleHeight <= 0
|
|
180
|
+
if clipped && visibleHeight > 0 {
|
|
181
|
+
let mask = cell.layer.mask ?? CALayer()
|
|
182
|
+
mask.backgroundColor = UIColor.black.cgColor
|
|
183
|
+
mask.frame = CGRect(x: 0, y: 0, width: cell.bounds.width, height: visibleHeight)
|
|
184
|
+
cell.layer.mask = mask
|
|
185
|
+
} else {
|
|
186
|
+
cell.layer.mask = nil
|
|
187
|
+
}
|
|
188
|
+
CATransaction.commit()
|
|
189
|
+
let inert = index >= tail.prefixCount || clipped
|
|
190
|
+
cell.isUserInteractionEnabled = !inert && !(itemsByKey[key]?.data.bool("disabled") ?? false)
|
|
191
|
+
cell.accessibilityElementsHidden = inert
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func finishStructuralTail(interrupted: Bool) {
|
|
195
|
+
guard let tail = structuralTail, let config else { return }
|
|
196
|
+
stopStructuralAnimation()
|
|
197
|
+
if !tail.retiringKeys.isEmpty {
|
|
198
|
+
var snapshot = dataSource.snapshot()
|
|
199
|
+
snapshot.deleteItems(Array(tail.retiringKeys))
|
|
200
|
+
dataSource.apply(snapshot, animatingDifferences: false)
|
|
201
|
+
itemsByKey = Dictionary(uniqueKeysWithValues: config.items.map { ($0.key, $0) })
|
|
202
|
+
}
|
|
203
|
+
clearStructuralTail()
|
|
204
|
+
structuralSpacerHeight = 0
|
|
205
|
+
disclosureCVisibleHeight = config.disclosure?.expanded == true ? containerSlotHeights[4] : 0
|
|
206
|
+
disclosureInset = config.disclosure?.expanded == true
|
|
207
|
+
? config.disclosure?.expandedTrailingInset ?? 0 : 0
|
|
208
|
+
structuralFooterStartY = nil
|
|
209
|
+
structuralFooterProgress = 0
|
|
210
|
+
configureLayout(config)
|
|
211
|
+
collectionView.layoutIfNeeded()
|
|
212
|
+
if interrupted {
|
|
213
|
+
// A diffable deletion can leave UIKit's content-shrink scroll animation active.
|
|
214
|
+
// Cancel it without choosing a new offset for the user's gesture.
|
|
215
|
+
collectionView.setContentOffset(collectionView.contentOffset, animated: false)
|
|
216
|
+
} else if !collectionView.isTracking && !collectionView.isDragging
|
|
217
|
+
&& !collectionView.isDecelerating {
|
|
218
|
+
let inset = collectionView.adjustedContentInset
|
|
219
|
+
let maximum = max(-inset.top,
|
|
220
|
+
collectionView.contentSize.height - collectionView.bounds.height + inset.bottom)
|
|
221
|
+
let target = min(max(-inset.top, structuralTargetOffset), maximum)
|
|
222
|
+
collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: target), animated: false)
|
|
223
|
+
}
|
|
224
|
+
layoutContainerSlots()
|
|
225
|
+
emitVisibleRangeIfNeeded()
|
|
226
|
+
checkEndReached()
|
|
227
|
+
settleDisclosure(interrupted ? "interrupted" : "completed")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func updateStructuralTailTarget() {
|
|
231
|
+
guard let tail = structuralTail, let config else { return }
|
|
232
|
+
let inset = collectionView.adjustedContentInset
|
|
233
|
+
let leftHeight = structuralTailStartY + tail.finalExtent
|
|
234
|
+
+ config.contentPaddingBottom + containerSlotHeights[2]
|
|
235
|
+
+ (config.disclosure == nil ? 0 :
|
|
236
|
+
(config.disclosure?.expanded == true ? containerSlotHeights[4] : 0)
|
|
237
|
+
+ containerSlotHeights[5]
|
|
238
|
+
+ (config.disclosure?.expanded == true
|
|
239
|
+
? config.disclosure?.expandedTrailingInset ?? 0 : 0))
|
|
240
|
+
let sidecarHeight = sidecarLayout == nil ? 0 :
|
|
241
|
+
config.contentPaddingTop + containerSlotHeights[3] + config.contentPaddingBottom
|
|
242
|
+
let finalHeight = max(leftHeight, sidecarHeight)
|
|
243
|
+
let maximum = max(-inset.top,
|
|
244
|
+
finalHeight - collectionView.bounds.height + inset.bottom)
|
|
245
|
+
var target = min(max(-inset.top, structuralStartOffset), maximum)
|
|
246
|
+
if tail.finalExtent == 0, config.disclosure != nil, containerSlotHeights[5] > 0 {
|
|
247
|
+
let toggleTop = structuralTailStartY
|
|
248
|
+
let toggleBottom = toggleTop + containerSlotHeights[5]
|
|
249
|
+
var sticky: CGFloat = 0
|
|
250
|
+
for case let value as NSNumber in pagerStickyHeights.objectEnumerator()?.allObjects ?? [] {
|
|
251
|
+
sticky += CGFloat(value.doubleValue)
|
|
252
|
+
}
|
|
253
|
+
let lower = toggleBottom - collectionView.bounds.height + inset.bottom
|
|
254
|
+
let upper = toggleTop - inset.top - sticky
|
|
255
|
+
target = lower <= upper ? min(max(target, lower), upper) : upper
|
|
256
|
+
} else if tail.finalExtent == 0, containerSlotHeights[2] > 0 {
|
|
257
|
+
let footerTop = structuralTailStartY
|
|
258
|
+
let footerBottom = footerTop + containerSlotHeights[2]
|
|
259
|
+
var sticky: CGFloat = 0
|
|
260
|
+
for case let value as NSNumber in pagerStickyHeights.objectEnumerator()?.allObjects ?? [] {
|
|
261
|
+
sticky += CGFloat(value.doubleValue)
|
|
262
|
+
}
|
|
263
|
+
let lower = footerBottom - collectionView.bounds.height + inset.bottom
|
|
264
|
+
let upper = footerTop - inset.top - sticky
|
|
265
|
+
target = lower <= upper ? min(max(target, lower), upper) : upper
|
|
266
|
+
}
|
|
267
|
+
structuralTargetOffset = min(max(-inset.top, target), maximum)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func structuralTailSegmentProgress(_ eased: CFTimeInterval) -> CFTimeInterval {
|
|
271
|
+
if eased >= 1 { return 1 }
|
|
272
|
+
let remaining = max(0.001, 1 - structuralTailSegmentStartEased)
|
|
273
|
+
return min(1, max(0, (eased - structuralTailSegmentStartEased) / remaining))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private func disclosureSegmentProgress(_ eased: CFTimeInterval) -> CFTimeInterval {
|
|
277
|
+
if eased >= 1 { return 1 }
|
|
278
|
+
let remaining = max(0.001, 1 - disclosureSegmentStartEased)
|
|
279
|
+
return min(1, max(0, (eased - disclosureSegmentStartEased) / remaining))
|
|
280
|
+
}
|
|
50
281
|
|
|
51
282
|
@objc func mountContainerSlot(_ view: UIView, atIndex index: Int) {
|
|
52
|
-
if index ==
|
|
283
|
+
if index == 6 {
|
|
53
284
|
topOverlayContent?.removeFromSuperview()
|
|
54
285
|
topOverlayContent = view
|
|
55
286
|
topOverlayHost.addSubview(view)
|
|
56
287
|
updateTopScrollEdge()
|
|
57
288
|
return
|
|
58
289
|
}
|
|
59
|
-
let host =
|
|
290
|
+
let host: UIView = index == 2 || index == 4 || index == 5
|
|
291
|
+
? NativeListClippedSlotHost() : UIView()
|
|
60
292
|
// Clipping to the reported height keeps new content from overlapping rows until the list
|
|
61
293
|
// has reserved space for it.
|
|
62
294
|
host.clipsToBounds = true
|
|
@@ -142,16 +374,113 @@ final class NativeListView: UIView {
|
|
|
142
374
|
}
|
|
143
375
|
}
|
|
144
376
|
|
|
145
|
-
func setContainerSlotHeightsJson(_ json: String) {
|
|
377
|
+
func setContainerSlotHeightsJson(_ json: String, followedBySnapshot: Bool = false) {
|
|
146
378
|
guard let data = json.data(using: .utf8),
|
|
147
379
|
let heights = try? JSONSerialization.jsonObject(with: data) as? [Double],
|
|
148
|
-
heights.count ==
|
|
149
|
-
|
|
380
|
+
(heights.count == 4 || heights.count == 6),
|
|
381
|
+
heights.allSatisfy({ $0.isFinite && $0 >= 0 }) else { return }
|
|
382
|
+
let previousHeights = containerSlotHeights
|
|
383
|
+
let footerDelta = containerSlotHeights[2] - CGFloat(heights[2])
|
|
384
|
+
containerSlotHeights = heights.map { CGFloat($0) } + (heights.count == 4 ? [0, 0] : [])
|
|
385
|
+
let contentHeightChanged = previousHeights[4] != containerSlotHeights[4]
|
|
386
|
+
let toggleHeightChanged = previousHeights[5] != containerSlotHeights[5]
|
|
387
|
+
let disclosureGeometryInvalid = structuralTail != nil && config?.disclosure != nil
|
|
388
|
+
&& (toggleHeightChanged || footerDelta != 0
|
|
389
|
+
|| containerSlotHeights[4] < disclosureCVisibleHeight - 0.5)
|
|
390
|
+
let surroundingSlotGeometryInvalid = structuralTail != nil
|
|
391
|
+
&& (previousHeights[0] != containerSlotHeights[0]
|
|
392
|
+
|| previousHeights[1] != containerSlotHeights[1]
|
|
393
|
+
|| previousHeights[3] != containerSlotHeights[3])
|
|
394
|
+
if surroundingSlotGeometryInvalid || disclosureGeometryInvalid {
|
|
395
|
+
if followedBySnapshot, config?.disclosure != nil {
|
|
396
|
+
deferredDisclosureGeometryInvalid = true
|
|
397
|
+
}
|
|
398
|
+
else { finishStructuralTail(interrupted: true) }
|
|
399
|
+
}
|
|
400
|
+
if structuralTail != nil, contentHeightChanged, !disclosureGeometryInvalid,
|
|
401
|
+
!surroundingSlotGeometryInvalid,
|
|
402
|
+
config?.disclosure != nil, structuralDisplayLink != nil {
|
|
403
|
+
let progress = min(1, max(0,
|
|
404
|
+
(CACurrentMediaTime() - structuralAnimationStart) / max(0.001, structuralAnimationDuration)))
|
|
405
|
+
let eased = progress * progress * (3 - 2 * progress)
|
|
406
|
+
disclosureStartCHeight = disclosureCVisibleHeight
|
|
407
|
+
disclosureSegmentStartEased = eased
|
|
408
|
+
}
|
|
409
|
+
if structuralTail != nil, structuralDisplayLink != nil, footerDelta != 0,
|
|
410
|
+
config?.disclosure == nil {
|
|
411
|
+
let progress = min(1, max(0,
|
|
412
|
+
(CACurrentMediaTime() - structuralAnimationStart) / max(0.001, structuralAnimationDuration)))
|
|
413
|
+
let eased = progress * progress * (3 - 2 * progress)
|
|
414
|
+
// Rebase from the last extent actually presented, not elapsed wall time.
|
|
415
|
+
structuralTailStartExtent = structuralSpacerHeight + footerDelta
|
|
416
|
+
structuralSpacerHeight = structuralTailStartExtent
|
|
417
|
+
structuralTailSegmentStartEased = eased
|
|
418
|
+
disclosureStartCHeight = disclosureCVisibleHeight
|
|
419
|
+
disclosureSegmentStartEased = eased
|
|
420
|
+
disclosureStartInset = disclosureInset
|
|
421
|
+
structuralStartOffset = collectionView.contentOffset.y
|
|
422
|
+
} else if structuralTail == nil, structuralDisplayLink != nil,
|
|
423
|
+
footerDelta != 0, config?.disclosure == nil {
|
|
424
|
+
let progress = min(1, max(0,
|
|
425
|
+
(CACurrentMediaTime() - structuralAnimationStart) / max(0.001, structuralAnimationDuration)))
|
|
426
|
+
let eased = progress * progress * (3 - 2 * progress)
|
|
427
|
+
structuralStartSpacer = max(0, structuralStartSpacer + footerDelta)
|
|
428
|
+
structuralSpacerHeight = structuralStartSpacer * (1 - eased)
|
|
429
|
+
if let startY = structuralFooterStartY { structuralFooterStartY = startY + footerDelta }
|
|
430
|
+
}
|
|
431
|
+
if structuralTail == nil, let disclosure = config?.disclosure {
|
|
432
|
+
disclosureCVisibleHeight = disclosure.expanded ? containerSlotHeights[4] : 0
|
|
433
|
+
disclosureInset = disclosure.expanded ? disclosure.expandedTrailingInset : 0
|
|
434
|
+
}
|
|
435
|
+
if let config { configureLayout(config) }
|
|
436
|
+
if structuralTail != nil {
|
|
437
|
+
updateStructuralTailTarget()
|
|
438
|
+
updateStructuralTailWindow()
|
|
439
|
+
flowLayout.invalidateLayout()
|
|
440
|
+
collectionView.layoutIfNeeded()
|
|
441
|
+
updateStructuralTailWindow()
|
|
442
|
+
} else if structuralDisplayLink != nil {
|
|
443
|
+
collectionView.layoutIfNeeded()
|
|
444
|
+
let inset = collectionView.adjustedContentInset
|
|
445
|
+
let finalMaximum = max(-inset.top,
|
|
446
|
+
collectionView.contentSize.height - structuralSpacerHeight
|
|
447
|
+
- collectionView.bounds.height + inset.bottom)
|
|
448
|
+
structuralTargetOffset = min(structuralTargetOffset, finalMaximum)
|
|
449
|
+
}
|
|
450
|
+
layoutContainerSlots()
|
|
451
|
+
setNeedsLayout()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
func setSidecarLayoutJson(_ json: String) {
|
|
455
|
+
if json.isEmpty {
|
|
456
|
+
sidecarLayout = nil
|
|
457
|
+
} else if let data = json.data(using: .utf8),
|
|
458
|
+
let value = try? JSONSerialization.jsonObject(with: data) as? [String: Double],
|
|
459
|
+
let width = value["width"], let gap = value["gap"],
|
|
460
|
+
let maxWidth = value["maxContentWidth"],
|
|
461
|
+
width.isFinite, width > 0, gap.isFinite, gap >= 0,
|
|
462
|
+
maxWidth.isFinite, maxWidth > width + gap {
|
|
463
|
+
sidecarLayout = (CGFloat(width), CGFloat(gap), CGFloat(maxWidth))
|
|
464
|
+
} else { return }
|
|
465
|
+
if structuralTail != nil { finishStructuralTail(interrupted: true) }
|
|
150
466
|
if let config { configureLayout(config) }
|
|
151
467
|
layoutContainerSlots()
|
|
152
468
|
setNeedsLayout()
|
|
153
469
|
}
|
|
154
470
|
|
|
471
|
+
private func sidecarBaseInset(_ width: CGFloat) -> CGFloat {
|
|
472
|
+
guard let sidecarLayout else { return 0 }
|
|
473
|
+
return max(0, (width - min(width, sidecarLayout.maxContentWidth)) / 2)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@objc(setPagerStickyHeight:forOwner:)
|
|
477
|
+
func setPagerStickyHeight(_ height: CGFloat, forOwner owner: AnyObject) {
|
|
478
|
+
let next = height.isFinite ? max(0, height) : 0
|
|
479
|
+
if next == 0 { pagerStickyHeights.removeObject(forKey: owner) }
|
|
480
|
+
else { pagerStickyHeights.setObject(NSNumber(value: Double(next)), forKey: owner) }
|
|
481
|
+
layoutContainerSlots()
|
|
482
|
+
}
|
|
483
|
+
|
|
155
484
|
private func layoutContainerSlots() {
|
|
156
485
|
let empty = config?.items.isEmpty ?? true
|
|
157
486
|
let header = containerSlotHeights[0]
|
|
@@ -159,15 +488,76 @@ final class NativeListView: UIView {
|
|
|
159
488
|
let footer = containerSlotHeights[2]
|
|
160
489
|
let paddingTop = config?.contentPaddingTop ?? 0
|
|
161
490
|
let paddingBottom = config?.contentPaddingBottom ?? 0
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
|
|
491
|
+
let naturalFooterY = flowLayout.bodyContentHeight - paddingBottom - footer
|
|
492
|
+
let footerY = structuralTail == nil
|
|
493
|
+
? max(paddingTop + header + emptyHeight, naturalFooterY) : naturalFooterY
|
|
494
|
+
let footerPosition = structuralTail == nil && structuralRemovedItems.isEmpty ? structuralFooterStartY.map {
|
|
495
|
+
$0 + (footerY - $0) * structuralFooterProgress
|
|
496
|
+
} ?? footerY : footerY
|
|
497
|
+
let positions = [paddingTop, paddingTop + header, footerPosition]
|
|
165
498
|
for (index, slot) in reactContainerSlots.prefix(3).enumerated() {
|
|
166
499
|
slot.host.isHidden = index == 1 && !empty
|
|
167
500
|
// React places the root at the horizontal inset inside this full-width host.
|
|
168
501
|
slot.host.frame = CGRect(x: 0, y: positions[index],
|
|
169
502
|
width: collectionView.bounds.width, height: containerSlotHeights[index])
|
|
170
503
|
}
|
|
504
|
+
if reactContainerSlots.count > 3 {
|
|
505
|
+
let slot = reactContainerSlots[3]
|
|
506
|
+
let width = sidecarLayout?.width ?? 0
|
|
507
|
+
let base = sidecarBaseInset(collectionView.bounds.width)
|
|
508
|
+
slot.host.isHidden = sidecarLayout == nil || containerSlotHeights[3] <= 0
|
|
509
|
+
let x = effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
510
|
+
? base : collectionView.bounds.width - base - width
|
|
511
|
+
slot.host.frame = CGRect(x: x,
|
|
512
|
+
y: paddingTop, width: width, height: containerSlotHeights[3])
|
|
513
|
+
}
|
|
514
|
+
if reactContainerSlots.count > 5 {
|
|
515
|
+
let disclosureEnabled = config?.disclosure != nil
|
|
516
|
+
let content = reactContainerSlots[4].host
|
|
517
|
+
let toggle = reactContainerSlots[5].host
|
|
518
|
+
let toggleHeight = disclosureEnabled ? containerSlotHeights[5] : 0
|
|
519
|
+
let contentY = footerPosition - disclosureInset - toggleHeight
|
|
520
|
+
- disclosureCVisibleHeight
|
|
521
|
+
content.frame = CGRect(x: 0, y: contentY, width: collectionView.bounds.width,
|
|
522
|
+
height: containerSlotHeights[4])
|
|
523
|
+
toggle.frame = CGRect(x: 0, y: contentY + disclosureCVisibleHeight,
|
|
524
|
+
width: collectionView.bounds.width, height: containerSlotHeights[5])
|
|
525
|
+
content.isHidden = !disclosureEnabled || disclosureCVisibleHeight <= 0
|
|
526
|
+
toggle.isHidden = !disclosureEnabled || toggleHeight <= 0
|
|
527
|
+
let contentSettled = structuralDisplayLink == nil && structuralTail == nil
|
|
528
|
+
&& config?.disclosure?.expanded == true
|
|
529
|
+
&& disclosureCVisibleHeight >= containerSlotHeights[4] - 0.5
|
|
530
|
+
content.isUserInteractionEnabled = contentSettled
|
|
531
|
+
content.accessibilityElementsHidden = !contentSettled
|
|
532
|
+
toggle.isUserInteractionEnabled = disclosureEnabled && toggleHeight > 0
|
|
533
|
+
toggle.accessibilityElementsHidden = !disclosureEnabled || toggleHeight <= 0
|
|
534
|
+
}
|
|
535
|
+
var stickyHeight: CGFloat = 0
|
|
536
|
+
for case let value as NSNumber in pagerStickyHeights.objectEnumerator()?.allObjects ?? [] {
|
|
537
|
+
stickyHeight += CGFloat(value.doubleValue)
|
|
538
|
+
}
|
|
539
|
+
CATransaction.begin()
|
|
540
|
+
CATransaction.setDisableActions(true)
|
|
541
|
+
for (index, slot) in reactContainerSlots.enumerated() {
|
|
542
|
+
let stickyTop = max(0, collectionView.contentOffset.y + stickyHeight - slot.host.frame.minY)
|
|
543
|
+
let tailTop = index == 2 && structuralTail != nil ? max(0, -structuralSpacerHeight) : 0
|
|
544
|
+
let top = max(stickyHeight > 0 ? stickyTop : 0, tailTop)
|
|
545
|
+
let bottom = index == 4 && config?.disclosure != nil
|
|
546
|
+
? min(slot.host.bounds.height, max(0, disclosureCVisibleHeight))
|
|
547
|
+
: slot.host.bounds.height
|
|
548
|
+
(slot.host as? NativeListClippedSlotHost)?.visibleTop = top
|
|
549
|
+
(slot.host as? NativeListClippedSlotHost)?.visibleBottom = index == 4 ? bottom : nil
|
|
550
|
+
guard top > 0 || bottom < slot.host.bounds.height else {
|
|
551
|
+
slot.host.layer.mask = nil
|
|
552
|
+
continue
|
|
553
|
+
}
|
|
554
|
+
let mask = slot.host.layer.mask ?? CALayer()
|
|
555
|
+
mask.backgroundColor = UIColor.black.cgColor
|
|
556
|
+
mask.frame = CGRect(x: 0, y: top, width: slot.host.bounds.width,
|
|
557
|
+
height: max(0, bottom - top))
|
|
558
|
+
slot.host.layer.mask = mask
|
|
559
|
+
}
|
|
560
|
+
CATransaction.commit()
|
|
171
561
|
}
|
|
172
562
|
|
|
173
563
|
private final class ActionAnchorRecord {
|
|
@@ -238,6 +628,7 @@ final class NativeListView: UIView {
|
|
|
238
628
|
var onSelectionDelta: ((String) -> Void)?
|
|
239
629
|
var onReorder: ((String) -> Void)?
|
|
240
630
|
var onEndReached: ((String) -> Void)?
|
|
631
|
+
var onStructuralTransitionSettled: ((String) -> Void)?
|
|
241
632
|
var onVisibleRangeChanged: ((String) -> Void)? {
|
|
242
633
|
didSet {
|
|
243
634
|
lastVisibleRange = nil
|
|
@@ -263,7 +654,16 @@ final class NativeListView: UIView {
|
|
|
263
654
|
private var sectionIndexHostHeightConstraint: NSLayoutConstraint?
|
|
264
655
|
private var footerHeightConstraint: NSLayoutConstraint!
|
|
265
656
|
private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
|
|
657
|
+
private final class AcceptedRowIndex {
|
|
658
|
+
let byKey: [String: Int]
|
|
659
|
+
|
|
660
|
+
init(items: [NativeListItem]) {
|
|
661
|
+
byKey = Dictionary(uniqueKeysWithValues: items.enumerated().map { ($1.key, $0) })
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
266
665
|
private var config: NativeListConfig?
|
|
666
|
+
private var acceptedRowIndex: AcceptedRowIndex?
|
|
267
667
|
private var itemsByKey: [String: NativeListItem] = [:]
|
|
268
668
|
private var endReachedGeneration: Int?
|
|
269
669
|
private var lastVisibleRange: (first: Int, last: Int, firstKey: String?, lastKey: String?)?
|
|
@@ -273,9 +673,11 @@ final class NativeListView: UIView {
|
|
|
273
673
|
private var sectionIndexHapticsEnabled = true
|
|
274
674
|
private var lastLayoutDirection: UIUserInterfaceLayoutDirection?
|
|
275
675
|
private var lastLayoutSize: CGSize?
|
|
676
|
+
private var lastConfiguredCollectionWidth: CGFloat?
|
|
276
677
|
private var commandAcceptedForPendingScroll = false
|
|
277
678
|
private var performingExplicitScrollCommand = false
|
|
278
679
|
private var animatedExplicitScrollCommand = false
|
|
680
|
+
private var explicitScrollRevision = 0
|
|
279
681
|
private var pagerCommandCancelObserver: NSObjectProtocol?
|
|
280
682
|
private var pendingScrollRequest: ScrollRequest?
|
|
281
683
|
private let sectionIndexFeedback = UISelectionFeedbackGenerator()
|
|
@@ -452,7 +854,8 @@ final class NativeListView: UIView {
|
|
|
452
854
|
}
|
|
453
855
|
dataSource.reorderingHandlers.canReorderItem = { [weak self] key in
|
|
454
856
|
guard let self else { return false }
|
|
455
|
-
return self.config?.reorderable == true && self.
|
|
857
|
+
return self.config?.reorderable == true && !self.isInertPresentationKey(key)
|
|
858
|
+
&& self.itemsByKey[key]?.isReorderable == true
|
|
456
859
|
}
|
|
457
860
|
dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
|
|
458
861
|
guard self?.interactiveReorderUsesAtomicTargeting != true else { return }
|
|
@@ -510,7 +913,7 @@ final class NativeListView: UIView {
|
|
|
510
913
|
let point = touch.location(in: collectionView)
|
|
511
914
|
guard collectionView.bounds.contains(point),
|
|
512
915
|
let indexPath = collectionView.indexPathForItem(at: point),
|
|
513
|
-
let item = item(at: indexPath) else { return false }
|
|
916
|
+
let item = item(at: indexPath), !isInertPresentationKey(item.key) else { return false }
|
|
514
917
|
return item.isRowPressEnabled
|
|
515
918
|
}
|
|
516
919
|
|
|
@@ -523,7 +926,10 @@ final class NativeListView: UIView {
|
|
|
523
926
|
}
|
|
524
927
|
|
|
525
928
|
override func willMove(toWindow newWindow: UIWindow?) {
|
|
526
|
-
if newWindow == nil {
|
|
929
|
+
if newWindow == nil {
|
|
930
|
+
attachSectionIndexToList()
|
|
931
|
+
if structuralTail != nil { finishStructuralTail(interrupted: true) }
|
|
932
|
+
}
|
|
527
933
|
super.willMove(toWindow: newWindow)
|
|
528
934
|
}
|
|
529
935
|
|
|
@@ -553,14 +959,17 @@ final class NativeListView: UIView {
|
|
|
553
959
|
height: requestedTopOverlayHeight)
|
|
554
960
|
updateSectionIndexAttachment()
|
|
555
961
|
let direction = effectiveUserInterfaceLayoutDirection
|
|
556
|
-
|
|
557
|
-
|
|
962
|
+
let sizeChanged = lastLayoutSize != nil && lastLayoutSize != bounds.size
|
|
963
|
+
if sizeChanged && structuralTail != nil { finishStructuralTail(interrupted: true) }
|
|
964
|
+
if sizeChanged || lastLayoutDirection != nil && lastLayoutDirection != direction {
|
|
558
965
|
invalidateActionAnchor(reason: "layout")
|
|
559
966
|
}
|
|
560
967
|
lastLayoutSize = bounds.size
|
|
561
|
-
|
|
968
|
+
let collectionWidthChanged = collectionView.bounds.width > 0
|
|
969
|
+
&& lastConfiguredCollectionWidth != collectionView.bounds.width
|
|
970
|
+
if (lastLayoutDirection != direction || collectionWidthChanged), let config {
|
|
562
971
|
configureLayout(config)
|
|
563
|
-
rebindRowsForLayoutDirection(config)
|
|
972
|
+
if lastLayoutDirection != direction { rebindRowsForLayoutDirection(config) }
|
|
564
973
|
}
|
|
565
974
|
performPendingScrollIfNeeded()
|
|
566
975
|
layoutContainerSlots()
|
|
@@ -596,10 +1005,163 @@ final class NativeListView: UIView {
|
|
|
596
1005
|
}
|
|
597
1006
|
}
|
|
598
1007
|
|
|
1008
|
+
private func acceptRowIndex(for next: NativeListConfig, replacing previous: NativeListConfig?) {
|
|
1009
|
+
if let previous, acceptedRowIndex != nil,
|
|
1010
|
+
previous.generation == next.generation,
|
|
1011
|
+
previous.items.count == next.items.count,
|
|
1012
|
+
zip(previous.items, next.items).allSatisfy({ old, new in
|
|
1013
|
+
old.key == new.key && old.type == new.type
|
|
1014
|
+
}) {
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
acceptedRowIndex = AcceptedRowIndex(items: next.items)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
599
1020
|
func applySnapshotJson(_ json: String) {
|
|
600
1021
|
guard let next = try? NativeListConfig.parse(json: json) else { return }
|
|
1022
|
+
let transition = next.structuralTransition
|
|
1023
|
+
let isNewTransition = transition != nil && transition?.id != consumedStructuralTransitionID
|
|
1024
|
+
let disclosureGeometryInvalid = deferredDisclosureGeometryInvalid
|
|
1025
|
+
deferredDisclosureGeometryInvalid = false
|
|
1026
|
+
let disclosureTargetChanged = config?.disclosure?.expanded != next.disclosure?.expanded
|
|
1027
|
+
let disclosureInsetChanged = config?.disclosure?.expandedTrailingInset
|
|
1028
|
+
!= next.disclosure?.expandedTrailingInset
|
|
1029
|
+
let disclosureStateChanged = disclosureTargetChanged || disclosureInsetChanged
|
|
1030
|
+
let activePendingID = structuralTail == nil ? nil : pendingDisclosureSettlement?.id
|
|
1031
|
+
let oldKeys = dataSource.snapshot().itemIdentifiers
|
|
1032
|
+
let logicalOldKeys = config?.items.map(\.key) ?? []
|
|
1033
|
+
let presentationItems = itemsByKey
|
|
1034
|
+
let newKeys = next.items.map(\.key)
|
|
1035
|
+
let newDisclosureCommand = config != nil && next.disclosure != nil && isNewTransition
|
|
1036
|
+
if let transition { consumedStructuralTransitionID = transition.id }
|
|
1037
|
+
if newDisclosureCommand, let transition, let disclosure = next.disclosure {
|
|
1038
|
+
pendingDisclosureSettlement = (transition.id, disclosure.expanded)
|
|
1039
|
+
} else if next.disclosure == nil || disclosureTargetChanged {
|
|
1040
|
+
// A target change without a new command cannot settle the previous target.
|
|
1041
|
+
pendingDisclosureSettlement = nil
|
|
1042
|
+
}
|
|
1043
|
+
let wantsStructure = config?.orientation == "vertical"
|
|
1044
|
+
&& (config?.layout == "linear" || config?.layout == "table")
|
|
1045
|
+
&& isNewTransition && logicalOldKeys != newKeys
|
|
1046
|
+
&& next.orientation == "vertical" && (next.layout == "linear" || next.layout == "table")
|
|
1047
|
+
&& !UIAccessibility.isReduceMotionEnabled
|
|
1048
|
+
&& (transition?.duration ?? 0) > 0
|
|
1049
|
+
let tailPrefix = wantsStructure ? config.flatMap { prefixTailCount(from: $0, to: next) } : nil
|
|
1050
|
+
// A carried tail extent belongs to its original prefix boundary.
|
|
1051
|
+
// Commit a new boundary atomically instead of starting a legacy row animation.
|
|
1052
|
+
let prefixChangedDuringTail = structuralTail != nil && tailPrefix != nil
|
|
1053
|
+
&& structuralTail?.prefixCount != tailPrefix
|
|
1054
|
+
let rawTailCandidate: StructuralTail? = tailPrefix.flatMap { prefix in
|
|
1055
|
+
guard let previous = config else { return nil }
|
|
1056
|
+
let items = previous.items.count > next.items.count
|
|
1057
|
+
? Array(previous.items.dropFirst(prefix)) : Array(next.items.dropFirst(prefix))
|
|
1058
|
+
let heights = items.map { rowHeight($0) }
|
|
1059
|
+
let tail = StructuralTail(items: items, heights: heights,
|
|
1060
|
+
spacing: next.itemSpacing, prefixCount: prefix,
|
|
1061
|
+
finalExtent: previous.items.count > next.items.count ? 0 :
|
|
1062
|
+
heights.reduce(0, +) + CGFloat(max(0, items.count - 1)) * next.itemSpacing
|
|
1063
|
+
+ (prefix == 0 ? 0 : next.itemSpacing))
|
|
1064
|
+
return tail
|
|
1065
|
+
}
|
|
1066
|
+
let tailOverCap = rawTailCandidate.map(tailExceedsHostLimit) ?? false
|
|
1067
|
+
let animateStructure = wantsStructure && !tailOverCap && !prefixChangedDuringTail
|
|
1068
|
+
&& !disclosureGeometryInvalid
|
|
1069
|
+
&& (next.disclosure == nil ||
|
|
1070
|
+
(rawTailCandidate != nil && reactContainerSlots.count > 5))
|
|
1071
|
+
let directDisclosureStatus: String? = newDisclosureCommand && !animateStructure
|
|
1072
|
+
? (disclosureGeometryInvalid || logicalOldKeys != newKeys && (transition?.duration ?? 0) > 0
|
|
1073
|
+
&& !UIAccessibility.isReduceMotionEnabled ? "interrupted" : "completed")
|
|
1074
|
+
: nil
|
|
1075
|
+
var interruptedPendingID: String?
|
|
1076
|
+
let tailCandidate = animateStructure ? rawTailCandidate : nil
|
|
1077
|
+
let presentationHeights: [String: CGFloat] = animateStructure ? Dictionary(uniqueKeysWithValues: oldKeys.compactMap { key -> (String, CGFloat)? in
|
|
1078
|
+
guard let item = presentationItems[key] else { return nil }
|
|
1079
|
+
let height = structuralRemovedHeights[key].map { $0 * (1 - structuralRemovalProgress) }
|
|
1080
|
+
?? rowHeight(item)
|
|
1081
|
+
return (key, height)
|
|
1082
|
+
}) : [:]
|
|
1083
|
+
let previousOffset = collectionView.contentOffset.y
|
|
1084
|
+
let oldLeftContentHeight = animateStructure ? flowLayout.bodyContentHeight : 0
|
|
1085
|
+
let oldSpacerHeight = structuralSpacerHeight
|
|
1086
|
+
let oldRowsHeight = animateStructure
|
|
1087
|
+
? presentationHeights.values.reduce(CGFloat(0), +)
|
|
1088
|
+
+ CGFloat(max(0, oldKeys.count - 1)) * (config?.itemSpacing ?? 0)
|
|
1089
|
+
: 0
|
|
1090
|
+
let newRowsHeight = animateStructure
|
|
1091
|
+
? next.items.reduce(CGFloat(0)) { $0 + rowHeight($1) }
|
|
1092
|
+
+ CGFloat(max(0, newKeys.count - 1)) * next.itemSpacing
|
|
1093
|
+
: 0
|
|
1094
|
+
let survivingKeys = animateStructure ? Set(newKeys) : Set<String>()
|
|
1095
|
+
let visibleAnchor = animateStructure
|
|
1096
|
+
? collectionView.indexPathsForVisibleItems.sorted().first {
|
|
1097
|
+
oldKeys.indices.contains($0.item) && survivingKeys.contains(oldKeys[$0.item])
|
|
1098
|
+
}
|
|
1099
|
+
: nil
|
|
1100
|
+
let anchorKey = visibleAnchor.map { oldKeys[$0.item] }
|
|
1101
|
+
let oldPrefix = (animateStructure ? anchorKey : nil).flatMap { key -> CGFloat? in
|
|
1102
|
+
guard let index = oldKeys.firstIndex(of: key) else { return nil }
|
|
1103
|
+
return oldKeys.prefix(index).reduce(CGFloat(0)) { $0 + (presentationHeights[$1] ?? 0) }
|
|
1104
|
+
+ CGFloat(index) * (config?.itemSpacing ?? 0)
|
|
1105
|
+
} ?? 0
|
|
1106
|
+
let newPrefix = (animateStructure ? anchorKey : nil).flatMap { key -> CGFloat? in
|
|
1107
|
+
guard let index = newKeys.firstIndex(of: key) else { return nil }
|
|
1108
|
+
return next.items.prefix(index).reduce(CGFloat(0)) { $0 + rowHeight($1) }
|
|
1109
|
+
+ CGFloat(index) * next.itemSpacing
|
|
1110
|
+
} ?? 0
|
|
1111
|
+
if animateStructure {
|
|
1112
|
+
let carriedExtent = structuralTail == nil ? nil : structuralSpacerHeight
|
|
1113
|
+
stopStructuralAnimation()
|
|
1114
|
+
clearStructuralTail()
|
|
1115
|
+
structuralScrollInterrupted = false
|
|
1116
|
+
structuralFooterStartY = reactContainerSlots.count > 2
|
|
1117
|
+
? reactContainerSlots[2].host.frame.minY : nil
|
|
1118
|
+
structuralFooterProgress = 0
|
|
1119
|
+
let removedKeys = Set(oldKeys).subtracting(newKeys)
|
|
1120
|
+
let retainsOrder = oldKeys.filter { survivingKeys.contains($0) } == newKeys
|
|
1121
|
+
structuralRemovedItems = tailCandidate == nil && retainsOrder
|
|
1122
|
+
? presentationItems.filter { removedKeys.contains($0.key) } : [:]
|
|
1123
|
+
structuralRemovedHeights = tailCandidate == nil && retainsOrder
|
|
1124
|
+
? presentationHeights.filter { removedKeys.contains($0.key) } : [:]
|
|
1125
|
+
structuralRemovalProgress = 0
|
|
1126
|
+
if let tail = tailCandidate {
|
|
1127
|
+
structuralTail = tail
|
|
1128
|
+
structuralTailStartExtent = carriedExtent ?? (tail.finalExtent == 0 ? tail.extent : 0)
|
|
1129
|
+
structuralTailSegmentStartEased = 0
|
|
1130
|
+
structuralSpacerHeight = structuralTailStartExtent
|
|
1131
|
+
let prefixHeights = next.items.prefix(tail.prefixCount).reduce(CGFloat(0)) {
|
|
1132
|
+
$0 + rowHeight($1)
|
|
1133
|
+
}
|
|
1134
|
+
structuralTailStartY = next.contentPaddingTop + containerSlotHeights[0]
|
|
1135
|
+
+ prefixHeights + CGFloat(max(0, tail.prefixCount - 1)) * next.itemSpacing
|
|
1136
|
+
disclosureStartCHeight = disclosureCVisibleHeight
|
|
1137
|
+
disclosureStartInset = disclosureInset
|
|
1138
|
+
disclosureSegmentStartEased = 0
|
|
1139
|
+
} else {
|
|
1140
|
+
structuralSpacerHeight = structuralRemovedItems.isEmpty
|
|
1141
|
+
? max(0, structuralSpacerHeight + oldRowsHeight - newRowsHeight) : 0
|
|
1142
|
+
}
|
|
1143
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1144
|
+
object: collectionView, userInfo: ["phase": 0])
|
|
1145
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1146
|
+
object: collectionView, userInfo: ["phase": 1])
|
|
1147
|
+
structuralScrollPhaseActive = true
|
|
1148
|
+
needsContentShrinkCorrection = false
|
|
1149
|
+
} else if logicalOldKeys != newKeys
|
|
1150
|
+
|| (newDisclosureCommand || disclosureGeometryInvalid || disclosureStateChanged)
|
|
1151
|
+
&& structuralTail != nil {
|
|
1152
|
+
if !newDisclosureCommand && !disclosureTargetChanged { interruptedPendingID = activePendingID }
|
|
1153
|
+
stopStructuralAnimation()
|
|
1154
|
+
clearStructuralTail()
|
|
1155
|
+
structuralSpacerHeight = 0
|
|
1156
|
+
structuralFooterStartY = nil
|
|
1157
|
+
structuralRemovedItems.removeAll()
|
|
1158
|
+
structuralRemovedHeights.removeAll()
|
|
1159
|
+
}
|
|
601
1160
|
invalidateActionAnchor(reason: "snapshot")
|
|
602
|
-
if let current = config,
|
|
1161
|
+
if let current = config, !newDisclosureCommand,
|
|
1162
|
+
structuralRemovedItems.isEmpty, structuralTail == nil,
|
|
1163
|
+
isControlledSelectionSnapshotUpdate(from: current, to: next) {
|
|
1164
|
+
acceptRowIndex(for: next, replacing: current)
|
|
603
1165
|
config = next
|
|
604
1166
|
itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
|
|
605
1167
|
// OneKey patch: A controlled selection echo is fully handled by the
|
|
@@ -608,7 +1170,7 @@ final class NativeListView: UIView {
|
|
|
608
1170
|
return
|
|
609
1171
|
}
|
|
610
1172
|
if let current = config, interactiveReorderSource != nil {
|
|
611
|
-
if canDeferSnapshotDuringInteractiveReorder(from: current, to: next) {
|
|
1173
|
+
if !newDisclosureCommand && canDeferSnapshotDuringInteractiveReorder(from: current, to: next) {
|
|
612
1174
|
deferSnapshotDuringInteractiveReorder(from: current, to: next)
|
|
613
1175
|
return
|
|
614
1176
|
}
|
|
@@ -619,17 +1181,111 @@ final class NativeListView: UIView {
|
|
|
619
1181
|
// rows whose own content is unchanged.
|
|
620
1182
|
let themeChanged = !dictionariesEqual(config?.theme, next.theme)
|
|
621
1183
|
|| !dictionariesEqual(config?.listStyle, next.listStyle)
|
|
1184
|
+
|| config?.layout != next.layout
|
|
1185
|
+
let tailGeometryChanged = config.map { current in
|
|
1186
|
+
current.disclosure?.expanded != next.disclosure?.expanded
|
|
1187
|
+
|| current.disclosure?.expandedTrailingInset != next.disclosure?.expandedTrailingInset
|
|
1188
|
+
|| current.orientation != next.orientation
|
|
1189
|
+
|| current.stickyHeaders != next.stickyHeaders
|
|
1190
|
+
|| current.sectionIndexEnabled != next.sectionIndexEnabled
|
|
1191
|
+
|| current.contentPadding != next.contentPadding
|
|
1192
|
+
|| current.itemSpacing != next.itemSpacing
|
|
1193
|
+
|| current.contentPaddingTop != next.contentPaddingTop
|
|
1194
|
+
|| current.contentPaddingBottom != next.contentPaddingBottom
|
|
1195
|
+
|| current.contentPaddingHorizontal != next.contentPaddingHorizontal
|
|
1196
|
+
|| current.items.count != next.items.count
|
|
1197
|
+
|| zip(current.items, next.items).contains { old, new in
|
|
1198
|
+
old.key != new.key || old.rendererKey != new.rendererKey
|
|
1199
|
+
|| rowHeight(old) != rowHeight(new)
|
|
1200
|
+
}
|
|
1201
|
+
} ?? false
|
|
622
1202
|
if config?.generation != next.generation { endReachedGeneration = nil }
|
|
1203
|
+
acceptRowIndex(for: next, replacing: config)
|
|
623
1204
|
config = next
|
|
1205
|
+
if structuralTail == nil {
|
|
1206
|
+
disclosureCVisibleHeight = next.disclosure?.expanded == true ? containerSlotHeights[4] : 0
|
|
1207
|
+
disclosureInset = next.disclosure?.expanded == true
|
|
1208
|
+
? next.disclosure?.expandedTrailingInset ?? 0 : 0
|
|
1209
|
+
}
|
|
624
1210
|
itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
|
|
1211
|
+
itemsByKey.merge(structuralRemovedItems) { current, _ in current }
|
|
1212
|
+
if let tail = structuralTail, !tail.retiringKeys.isEmpty {
|
|
1213
|
+
itemsByKey.merge(presentationItems.filter { tail.retiringKeys.contains($0.key) }) {
|
|
1214
|
+
current, _ in current
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if !animateStructure, let tail = structuralTail {
|
|
1218
|
+
if themeChanged || tailGeometryChanged {
|
|
1219
|
+
if !newDisclosureCommand { interruptedPendingID = activePendingID }
|
|
1220
|
+
stopStructuralAnimation()
|
|
1221
|
+
clearStructuralTail()
|
|
1222
|
+
structuralSpacerHeight = 0
|
|
1223
|
+
structuralFooterStartY = nil
|
|
1224
|
+
disclosureCVisibleHeight = next.disclosure?.expanded == true ? containerSlotHeights[4] : 0
|
|
1225
|
+
disclosureInset = next.disclosure?.expanded == true
|
|
1226
|
+
? next.disclosure?.expandedTrailingInset ?? 0 : 0
|
|
1227
|
+
} else {
|
|
1228
|
+
for (index, previous) in tail.items.enumerated() {
|
|
1229
|
+
guard let item = itemsByKey[previous.key], item.content != previous.content else { continue }
|
|
1230
|
+
tail.items[index] = item
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
625
1234
|
configureSectionIndex(next)
|
|
626
1235
|
configureLayout(next)
|
|
627
1236
|
configureRefresh(next)
|
|
628
1237
|
configureFooter(next)
|
|
1238
|
+
if structuralTail != nil { updateStructuralTailWindow() }
|
|
1239
|
+
|
|
1240
|
+
if animateStructure, let transition {
|
|
1241
|
+
let inset = collectionView.adjustedContentInset
|
|
1242
|
+
let finalLeftHeight = structuralTail.map { tail in
|
|
1243
|
+
structuralTailStartY + tail.finalExtent + next.contentPaddingBottom + containerSlotHeights[2]
|
|
1244
|
+
+ (next.disclosure == nil ? 0 :
|
|
1245
|
+
(next.disclosure?.expanded == true ? containerSlotHeights[4] : 0)
|
|
1246
|
+
+ containerSlotHeights[5]
|
|
1247
|
+
+ (next.disclosure?.expanded == true
|
|
1248
|
+
? next.disclosure?.expandedTrailingInset ?? 0 : 0))
|
|
1249
|
+
} ?? max(0, oldLeftContentHeight + newRowsHeight - oldRowsHeight - oldSpacerHeight)
|
|
1250
|
+
let sidecarHeight = sidecarLayout == nil ? 0 :
|
|
1251
|
+
next.contentPaddingTop + containerSlotHeights[3] + next.contentPaddingBottom
|
|
1252
|
+
let finalHeight = max(finalLeftHeight, sidecarHeight)
|
|
1253
|
+
let finalMaximum = max(-inset.top, finalHeight - collectionView.bounds.height + inset.bottom)
|
|
1254
|
+
structuralStartSpacer = structuralSpacerHeight
|
|
1255
|
+
structuralStartOffset = previousOffset
|
|
1256
|
+
var targetOffset = structuralTail == nil
|
|
1257
|
+
? min(max(-inset.top, previousOffset + newPrefix - oldPrefix), finalMaximum)
|
|
1258
|
+
: min(max(-inset.top, previousOffset), finalMaximum)
|
|
1259
|
+
if let tail = structuralTail, tail.finalExtent == 0,
|
|
1260
|
+
(next.disclosure != nil ? containerSlotHeights[5] : containerSlotHeights[2]) > 0 {
|
|
1261
|
+
let targetTop = structuralTailStartY
|
|
1262
|
+
let targetBottom = targetTop + (next.disclosure != nil
|
|
1263
|
+
? containerSlotHeights[5] : containerSlotHeights[2])
|
|
1264
|
+
var sticky: CGFloat = 0
|
|
1265
|
+
for case let value as NSNumber in pagerStickyHeights.objectEnumerator()?.allObjects ?? [] {
|
|
1266
|
+
sticky += CGFloat(value.doubleValue)
|
|
1267
|
+
}
|
|
1268
|
+
let lower = targetBottom - collectionView.bounds.height + inset.bottom
|
|
1269
|
+
let upper = targetTop - inset.top - sticky
|
|
1270
|
+
targetOffset = lower <= upper
|
|
1271
|
+
? min(max(targetOffset, lower), upper) : upper
|
|
1272
|
+
targetOffset = min(max(-inset.top, targetOffset), finalMaximum)
|
|
1273
|
+
} else if structuralTail == nil, visibleAnchor == nil, let fallbackKey = transition.anchorKey,
|
|
1274
|
+
let fallbackIndex = newKeys.firstIndex(of: fallbackKey) {
|
|
1275
|
+
let fallbackY = next.contentPaddingTop + containerSlotHeights[0]
|
|
1276
|
+
+ next.items.prefix(fallbackIndex).reduce(CGFloat(0)) { $0 + rowHeight($1) }
|
|
1277
|
+
+ CGFloat(fallbackIndex) * next.itemSpacing
|
|
1278
|
+
targetOffset = min(targetOffset, max(-inset.top, fallbackY - inset.top))
|
|
1279
|
+
}
|
|
1280
|
+
structuralTargetOffset = targetOffset
|
|
1281
|
+
if structuralTail != nil { updateStructuralTailTarget() }
|
|
1282
|
+
structuralAnimationDuration = transition.duration
|
|
1283
|
+
}
|
|
629
1284
|
|
|
630
1285
|
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
|
|
631
1286
|
snapshot.appendSections([0])
|
|
632
|
-
let keys =
|
|
1287
|
+
let keys = structuralTail.map { $0.retiringKeys.isEmpty ? newKeys : oldKeys }
|
|
1288
|
+
?? (structuralRemovedItems.isEmpty ? newKeys : oldKeys)
|
|
633
1289
|
snapshot.appendItems(keys, toSection: 0)
|
|
634
1290
|
var changedKeys = Set(keys.filter { key in
|
|
635
1291
|
guard let old = oldItems[key], let new = itemsByKey[key] else { return false }
|
|
@@ -644,27 +1300,219 @@ final class NativeListView: UIView {
|
|
|
644
1300
|
let replacedKeys = changedRetainedKeys.filter { oldItems[$0]?.rendererKey != itemsByKey[$0]?.rendererKey }
|
|
645
1301
|
snapshot.reloadItems(Array(replacedKeys))
|
|
646
1302
|
snapshot.reconfigureItems(Array(changedRetainedKeys.subtracting(replacedKeys)))
|
|
647
|
-
|
|
1303
|
+
let scrollRevisionAtApply = explicitScrollRevision
|
|
1304
|
+
let explicitScrollPendingAtApply = pendingScrollRequest != nil
|
|
1305
|
+
dataSource.apply(snapshot, animatingDifferences: animateStructure && structuralRemovedItems.isEmpty
|
|
1306
|
+
&& structuralTail == nil) { [weak self] in
|
|
648
1307
|
guard let self else { return }
|
|
649
1308
|
self.collectionView.layoutIfNeeded()
|
|
1309
|
+
self.updateStructuralTailWindow()
|
|
1310
|
+
self.layoutContainerSlots()
|
|
650
1311
|
self.notifyPagerScrollViewReadyIfNeeded()
|
|
651
1312
|
self.performPendingScrollIfNeeded()
|
|
1313
|
+
if directDisclosureStatus == "completed", let transition {
|
|
1314
|
+
self.revealDirectCollapsedToggle(expectedID: transition.id,
|
|
1315
|
+
scrollRevision: scrollRevisionAtApply,
|
|
1316
|
+
explicitScrollPendingAtApply: explicitScrollPendingAtApply)
|
|
1317
|
+
}
|
|
652
1318
|
self.emitVisibleRangeIfNeeded()
|
|
653
1319
|
self.checkEndReached()
|
|
654
1320
|
self.syncSectionIndexToVisibleRows()
|
|
1321
|
+
if let directDisclosureStatus, let transition {
|
|
1322
|
+
self.settleDisclosure(directDisclosureStatus, expectedID: transition.id)
|
|
1323
|
+
} else if let interruptedPendingID {
|
|
1324
|
+
self.settleDisclosure("interrupted", expectedID: interruptedPendingID)
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if animateStructure && (structuralTail != nil || tailCandidate == nil) {
|
|
1328
|
+
updateStructuralTailWindow()
|
|
1329
|
+
if tailCandidate != nil && structuralTail == nil { return }
|
|
1330
|
+
structuralAnimationStart = CACurrentMediaTime()
|
|
1331
|
+
let displayLink = CADisplayLink(target: self, selector: #selector(advanceStructuralAnimation(_:)))
|
|
1332
|
+
structuralDisplayLink = displayLink
|
|
1333
|
+
displayLink.add(to: .main, forMode: .common)
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private func stopStructuralAnimation() {
|
|
1338
|
+
if structuralScrollPhaseActive {
|
|
1339
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1340
|
+
object: collectionView, userInfo: ["phase": 2])
|
|
1341
|
+
structuralScrollPhaseActive = false
|
|
1342
|
+
}
|
|
1343
|
+
structuralDisplayLink?.invalidate()
|
|
1344
|
+
structuralDisplayLink = nil
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private func revealDirectCollapsedToggle(expectedID: String, scrollRevision: Int,
|
|
1348
|
+
explicitScrollPendingAtApply: Bool) {
|
|
1349
|
+
guard !disposed, let pending = pendingDisclosureSettlement,
|
|
1350
|
+
pending.id == expectedID, !pending.expanded,
|
|
1351
|
+
consumedStructuralTransitionID == expectedID,
|
|
1352
|
+
config?.disclosure?.expanded == false,
|
|
1353
|
+
structuralTail == nil, structuralDisplayLink == nil,
|
|
1354
|
+
!structuralScrollPhaseActive, scrollRevision == explicitScrollRevision,
|
|
1355
|
+
!explicitScrollPendingAtApply,
|
|
1356
|
+
pendingScrollRequest == nil, !animatedExplicitScrollCommand,
|
|
1357
|
+
!collectionView.isTracking, !collectionView.isDragging,
|
|
1358
|
+
!collectionView.isDecelerating,
|
|
1359
|
+
collectionView.bounds.height > 0, reactContainerSlots.count > 5 else { return }
|
|
1360
|
+
let toggle = reactContainerSlots[5].host
|
|
1361
|
+
guard !toggle.isHidden, toggle.bounds.height > 0 else { return }
|
|
1362
|
+
let inset = collectionView.adjustedContentInset
|
|
1363
|
+
let maximum = max(-inset.top,
|
|
1364
|
+
collectionView.contentSize.height - collectionView.bounds.height + inset.bottom)
|
|
1365
|
+
var sticky: CGFloat = 0
|
|
1366
|
+
for case let value as NSNumber in pagerStickyHeights.objectEnumerator()?.allObjects ?? [] {
|
|
1367
|
+
sticky += CGFloat(value.doubleValue)
|
|
1368
|
+
}
|
|
1369
|
+
let lower = toggle.frame.maxY - collectionView.bounds.height + inset.bottom
|
|
1370
|
+
let upper = toggle.frame.minY - inset.top - sticky
|
|
1371
|
+
let current = collectionView.contentOffset.y
|
|
1372
|
+
let visibleTarget = lower <= upper ? min(max(current, lower), upper) : upper
|
|
1373
|
+
let target = min(max(-inset.top, visibleTarget), maximum)
|
|
1374
|
+
guard abs(target - current) > 0.5 else { return }
|
|
1375
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1376
|
+
object: collectionView, userInfo: ["phase": 0])
|
|
1377
|
+
defer {
|
|
1378
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1379
|
+
object: collectionView, userInfo: ["phase": 2])
|
|
1380
|
+
}
|
|
1381
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
1382
|
+
object: collectionView, userInfo: ["phase": 1])
|
|
1383
|
+
collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: target), animated: false)
|
|
1384
|
+
layoutContainerSlots()
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
private func settleDisclosure(_ status: String, expectedID: String? = nil) {
|
|
1388
|
+
guard let pending = pendingDisclosureSettlement,
|
|
1389
|
+
expectedID == nil || pending.id == expectedID,
|
|
1390
|
+
consumedStructuralTransitionID == pending.id,
|
|
1391
|
+
config?.disclosure?.expanded == pending.expanded else { return }
|
|
1392
|
+
pendingDisclosureSettlement = nil
|
|
1393
|
+
guard let callback = onStructuralTransitionSettled else { return }
|
|
1394
|
+
emit(callback, [
|
|
1395
|
+
"transitionId": pending.id,
|
|
1396
|
+
"status": status,
|
|
1397
|
+
"settledExpanded": pending.expanded,
|
|
1398
|
+
])
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
@objc private func advanceStructuralAnimation(_ link: CADisplayLink) {
|
|
1402
|
+
guard let config else { stopStructuralAnimation(); return }
|
|
1403
|
+
if structuralTail != nil && UIAccessibility.isReduceMotionEnabled {
|
|
1404
|
+
finishStructuralTail(interrupted: false)
|
|
1405
|
+
return
|
|
1406
|
+
}
|
|
1407
|
+
let elapsed = CACurrentMediaTime() - structuralAnimationStart
|
|
1408
|
+
let progress = min(1, max(0, elapsed / max(0.001, structuralAnimationDuration)))
|
|
1409
|
+
let eased = progress * progress * (3 - 2 * progress)
|
|
1410
|
+
if let tail = structuralTail {
|
|
1411
|
+
// Commit cell clipping, viewport movement, and slots together for this tick.
|
|
1412
|
+
CATransaction.begin()
|
|
1413
|
+
CATransaction.setDisableActions(true)
|
|
1414
|
+
defer { CATransaction.commit() }
|
|
1415
|
+
let segment = structuralTailSegmentProgress(eased)
|
|
1416
|
+
structuralSpacerHeight = structuralTailStartExtent
|
|
1417
|
+
+ (tail.finalExtent - structuralTailStartExtent) * segment
|
|
1418
|
+
if let disclosure = config.disclosure {
|
|
1419
|
+
let targetC = disclosure.expanded ? containerSlotHeights[4] : 0
|
|
1420
|
+
let targetInset = disclosure.expanded ? disclosure.expandedTrailingInset : 0
|
|
1421
|
+
let contentSegment = disclosureSegmentProgress(eased)
|
|
1422
|
+
disclosureCVisibleHeight = disclosureStartCHeight
|
|
1423
|
+
+ (targetC - disclosureStartCHeight) * contentSegment
|
|
1424
|
+
disclosureInset = disclosureStartInset
|
|
1425
|
+
+ (targetInset - disclosureStartInset) * segment
|
|
1426
|
+
}
|
|
1427
|
+
structuralFooterProgress = CGFloat(eased)
|
|
1428
|
+
updateStructuralTailWindow()
|
|
1429
|
+
flowLayout.invalidateLayout()
|
|
1430
|
+
collectionView.layoutIfNeeded()
|
|
1431
|
+
if !structuralScrollInterrupted && !collectionView.isTracking
|
|
1432
|
+
&& !collectionView.isDragging && !collectionView.isDecelerating {
|
|
1433
|
+
collectionView.contentOffset.y = structuralStartOffset
|
|
1434
|
+
+ (structuralTargetOffset - structuralStartOffset) * segment
|
|
1435
|
+
collectionView.layoutIfNeeded()
|
|
1436
|
+
}
|
|
1437
|
+
updateStructuralTailWindow()
|
|
1438
|
+
layoutContainerSlots()
|
|
1439
|
+
if progress >= 1 { finishStructuralTail(interrupted: structuralScrollInterrupted) }
|
|
1440
|
+
return
|
|
1441
|
+
}
|
|
1442
|
+
structuralSpacerHeight = structuralStartSpacer * (1 - eased)
|
|
1443
|
+
structuralFooterProgress = CGFloat(eased)
|
|
1444
|
+
structuralRemovalProgress = CGFloat(eased)
|
|
1445
|
+
for case let cell as NativeListRowHost in collectionView.visibleCells {
|
|
1446
|
+
guard let indexPath = collectionView.indexPath(for: cell),
|
|
1447
|
+
let key = dataSource.itemIdentifier(for: indexPath) else { continue }
|
|
1448
|
+
cell.structuralContentHeight = structuralRemovedItems[key].map {
|
|
1449
|
+
rowHeight($0, allocatedWidth: cell.bounds.width)
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
var inset = flowLayout.sectionInset
|
|
1453
|
+
inset.bottom = config.contentPaddingBottom + containerSlotHeights[2] + structuralSpacerHeight
|
|
1454
|
+
flowLayout.sectionInset = inset
|
|
1455
|
+
flowLayout.invalidateLayout()
|
|
1456
|
+
collectionView.layoutIfNeeded()
|
|
1457
|
+
if !structuralScrollInterrupted && !collectionView.isTracking
|
|
1458
|
+
&& !collectionView.isDragging && !collectionView.isDecelerating {
|
|
1459
|
+
collectionView.contentOffset.y = structuralStartOffset
|
|
1460
|
+
+ (structuralTargetOffset - structuralStartOffset) * eased
|
|
1461
|
+
// Materialize the new viewport before updating presentation cells.
|
|
1462
|
+
collectionView.layoutIfNeeded()
|
|
1463
|
+
}
|
|
1464
|
+
for cell in collectionView.visibleCells {
|
|
1465
|
+
guard let indexPath = collectionView.indexPath(for: cell),
|
|
1466
|
+
let key = dataSource.itemIdentifier(for: indexPath) else { continue }
|
|
1467
|
+
let retiring = structuralRemovedItems[key] != nil
|
|
1468
|
+
if retiring { cell.clipsToBounds = true }
|
|
1469
|
+
cell.alpha = retiring ? 1 - structuralRemovalProgress : 1
|
|
1470
|
+
cell.isUserInteractionEnabled = !retiring && !(itemsByKey[key]?.data.bool("disabled") ?? false)
|
|
1471
|
+
}
|
|
1472
|
+
layoutContainerSlots()
|
|
1473
|
+
if progress >= 1 {
|
|
1474
|
+
if !structuralRemovedItems.isEmpty {
|
|
1475
|
+
var snapshot = dataSource.snapshot()
|
|
1476
|
+
snapshot.deleteItems(Array(structuralRemovedItems.keys))
|
|
1477
|
+
structuralRemovedItems.removeAll()
|
|
1478
|
+
structuralRemovedHeights.removeAll()
|
|
1479
|
+
dataSource.apply(snapshot, animatingDifferences: false)
|
|
1480
|
+
itemsByKey = Dictionary(uniqueKeysWithValues: config.items.map { ($0.key, $0) })
|
|
1481
|
+
collectionView.layoutIfNeeded()
|
|
1482
|
+
if !structuralScrollInterrupted && !collectionView.isTracking
|
|
1483
|
+
&& !collectionView.isDragging && !collectionView.isDecelerating {
|
|
1484
|
+
let inset = collectionView.adjustedContentInset
|
|
1485
|
+
let maximum = max(-inset.top,
|
|
1486
|
+
collectionView.contentSize.height - collectionView.bounds.height + inset.bottom)
|
|
1487
|
+
let target = min(max(-inset.top, structuralTargetOffset), maximum)
|
|
1488
|
+
// Cancel UIKit's residual content-shrink scroll animation after deleting retired rows.
|
|
1489
|
+
collectionView.setContentOffset(
|
|
1490
|
+
CGPoint(x: collectionView.contentOffset.x, y: target), animated: false)
|
|
1491
|
+
collectionView.layoutIfNeeded()
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
stopStructuralAnimation()
|
|
1495
|
+
structuralSpacerHeight = 0
|
|
1496
|
+
structuralFooterStartY = nil
|
|
1497
|
+
layoutContainerSlots()
|
|
1498
|
+
emitVisibleRangeIfNeeded()
|
|
1499
|
+
checkEndReached()
|
|
1500
|
+
scheduleContentShrinkCorrection()
|
|
655
1501
|
}
|
|
656
1502
|
}
|
|
657
1503
|
|
|
658
1504
|
func applyPatchesJson(_ json: String) {
|
|
659
1505
|
guard var current = config,
|
|
1506
|
+
let indexByKey = acceptedRowIndex?.byKey,
|
|
660
1507
|
let bytes = json.data(using: .utf8),
|
|
661
1508
|
let patches = try? JSONSerialization.jsonObject(with: bytes) as? [[String: Any]] else { return }
|
|
662
|
-
let indexByKey = Dictionary(uniqueKeysWithValues: current.items.enumerated().map { ($1.key, $0) })
|
|
663
1509
|
var seen = Set<String>()
|
|
664
1510
|
var pending: [(Int, [String: Any])] = []
|
|
665
1511
|
for patch in patches {
|
|
666
1512
|
let key = patch.string("key")
|
|
667
1513
|
guard let index = indexByKey[key],
|
|
1514
|
+
current.items.indices.contains(index),
|
|
1515
|
+
current.items[index].key == key,
|
|
668
1516
|
seen.insert(key).inserted,
|
|
669
1517
|
patch.string("type") == current.items[index].type,
|
|
670
1518
|
let changes = patch.dictionary("changes") else { return }
|
|
@@ -713,7 +1561,17 @@ final class NativeListView: UIView {
|
|
|
713
1561
|
cancelInteractiveReorderForStructuralUpdate()
|
|
714
1562
|
}
|
|
715
1563
|
config = current
|
|
716
|
-
|
|
1564
|
+
for (index, _) in pending {
|
|
1565
|
+
let item = current.items[index]
|
|
1566
|
+
itemsByKey[item.key] = item
|
|
1567
|
+
}
|
|
1568
|
+
if let tail = structuralTail {
|
|
1569
|
+
for key in changedKeys.union(marketQuoteKeys) {
|
|
1570
|
+
guard let index = tail.indexByKey[key], let item = itemsByKey[key] else { continue }
|
|
1571
|
+
tail.items[index] = item
|
|
1572
|
+
}
|
|
1573
|
+
if sizeChanged { finishStructuralTail(interrupted: true) }
|
|
1574
|
+
}
|
|
717
1575
|
for indexPath in collectionView.indexPathsForVisibleItems {
|
|
718
1576
|
guard let item = item(at: indexPath),
|
|
719
1577
|
marketQuoteKeys.contains(item.key),
|
|
@@ -814,6 +1672,8 @@ final class NativeListView: UIView {
|
|
|
814
1672
|
|
|
815
1673
|
private func requestScroll(_ request: ScrollRequest) {
|
|
816
1674
|
if config != nil && explicitTopForScrollCommand(request) == nil { return }
|
|
1675
|
+
explicitScrollRevision &+= 1
|
|
1676
|
+
if structuralTail != nil { finishStructuralTail(interrupted: true) }
|
|
817
1677
|
animatedExplicitScrollCommand = false
|
|
818
1678
|
commandAcceptedForPendingScroll = false
|
|
819
1679
|
acceptScrollCommand(request)
|
|
@@ -977,6 +1837,7 @@ final class NativeListView: UIView {
|
|
|
977
1837
|
guard let self else { return }
|
|
978
1838
|
self.contentShrinkCorrectionScheduled = false
|
|
979
1839
|
guard !self.disposed, self.needsContentShrinkCorrection,
|
|
1840
|
+
self.structuralDisplayLink == nil,
|
|
980
1841
|
!self.collectionView.isTracking, !self.collectionView.isDragging,
|
|
981
1842
|
!self.collectionView.isDecelerating,
|
|
982
1843
|
self.collectionView.refreshControl?.isRefreshing != true,
|
|
@@ -1012,15 +1873,25 @@ final class NativeListView: UIView {
|
|
|
1012
1873
|
flowLayout.scrollDirection = isHorizontal ? .horizontal : .vertical
|
|
1013
1874
|
flowLayout.minimumLineSpacing = config.itemSpacing
|
|
1014
1875
|
flowLayout.minimumInteritemSpacing = config.itemSpacing
|
|
1876
|
+
let sidecarInset = sidecarBaseInset(collectionView.bounds.width)
|
|
1877
|
+
let sidecarSpace = sidecarLayout.map { $0.width + $0.gap } ?? 0
|
|
1878
|
+
let horizontalInset = config.contentPaddingHorizontal + sidecarInset
|
|
1879
|
+
let disclosureHeight = config.disclosure == nil ? 0 :
|
|
1880
|
+
disclosureCVisibleHeight + containerSlotHeights[5] + disclosureInset
|
|
1015
1881
|
flowLayout.sectionInset = UIEdgeInsets(
|
|
1016
1882
|
top: config.contentPaddingTop + containerSlotHeights[0],
|
|
1017
|
-
left:
|
|
1018
|
-
bottom: config.contentPaddingBottom + containerSlotHeights[2]
|
|
1019
|
-
|
|
1883
|
+
left: horizontalInset + (isRightToLeft ? sidecarSpace + indexGutter : 0),
|
|
1884
|
+
bottom: config.contentPaddingBottom + containerSlotHeights[2] + disclosureHeight
|
|
1885
|
+
+ (structuralTail == nil ? structuralSpacerHeight : 0),
|
|
1886
|
+
right: horizontalInset + (isRightToLeft ? 0 : sidecarSpace + indexGutter)
|
|
1020
1887
|
)
|
|
1021
|
-
|
|
1022
|
-
? config.contentPaddingTop + config.contentPaddingBottom
|
|
1888
|
+
let emptyMinimum = config.items.isEmpty
|
|
1889
|
+
? config.contentPaddingTop + config.contentPaddingBottom
|
|
1890
|
+
+ containerSlotHeights.prefix(3).reduce(0, +) + disclosureHeight
|
|
1023
1891
|
: 0
|
|
1892
|
+
let sidecarMinimum = sidecarLayout == nil ? 0 :
|
|
1893
|
+
config.contentPaddingTop + containerSlotHeights[3] + config.contentPaddingBottom
|
|
1894
|
+
flowLayout.minimumContainerContentHeight = max(emptyMinimum, sidecarMinimum)
|
|
1024
1895
|
flowLayout.stickyItemIndexes = config.stickyHeaders
|
|
1025
1896
|
? Set(config.items.enumerated().compactMap {
|
|
1026
1897
|
$0.element.type == "sectionHeader" && $0.element.data.bool("sticky", default: true) && $0.element.data.string("variant") != "summary"
|
|
@@ -1034,6 +1905,7 @@ final class NativeListView: UIView {
|
|
|
1034
1905
|
listBodyGestureGuard.isEnabled = !isHorizontal
|
|
1035
1906
|
collectionView.showsVerticalScrollIndicator = sectionIndexEntries.isEmpty
|
|
1036
1907
|
collectionView.dragInteractionEnabled = false
|
|
1908
|
+
lastConfiguredCollectionWidth = collectionView.bounds.width
|
|
1037
1909
|
lastLayoutDirection = effectiveUserInterfaceLayoutDirection
|
|
1038
1910
|
}
|
|
1039
1911
|
|
|
@@ -1301,6 +2173,7 @@ final class NativeListView: UIView {
|
|
|
1301
2173
|
let toIndex = keys.firstIndex(of: source.key),
|
|
1302
2174
|
items.count == keys.count else { return }
|
|
1303
2175
|
current.items = items
|
|
2176
|
+
acceptedRowIndex = AcceptedRowIndex(items: items)
|
|
1304
2177
|
config = current
|
|
1305
2178
|
guard source.index != toIndex else { return }
|
|
1306
2179
|
var payload: [String: Any] = [
|
|
@@ -1371,6 +2244,10 @@ final class NativeListView: UIView {
|
|
|
1371
2244
|
return config?.items[safe: indexPath.item]
|
|
1372
2245
|
}
|
|
1373
2246
|
|
|
2247
|
+
private func isInertPresentationKey(_ key: String) -> Bool {
|
|
2248
|
+
structuralTail?.indexByKey[key] != nil || structuralRemovedItems[key] != nil
|
|
2249
|
+
}
|
|
2250
|
+
|
|
1374
2251
|
private func updateAtomicReorderTarget(to targetIndex: Int, key targetKey: String, animated: Bool) {
|
|
1375
2252
|
guard let source = interactiveReorderSource else { return }
|
|
1376
2253
|
interactiveReorderTargetIndex = targetIndex
|
|
@@ -1714,11 +2591,23 @@ final class NativeListView: UIView {
|
|
|
1714
2591
|
if item.key == interactiveReorderCompactKey {
|
|
1715
2592
|
cell.setWalletGroupReorderCompact(true)
|
|
1716
2593
|
}
|
|
2594
|
+
let retiring = structuralRemovedItems[item.key] != nil
|
|
2595
|
+
cell.structuralContentHeight = retiring
|
|
2596
|
+
? rowHeight(item, allocatedWidth: cell.bounds.width) : nil
|
|
2597
|
+
if retiring { cell.clipsToBounds = true }
|
|
2598
|
+
cell.alpha = retiring ? 1 - structuralRemovalProgress : 1
|
|
2599
|
+
cell.isUserInteractionEnabled = !retiring && !item.data.bool("disabled")
|
|
2600
|
+
cell.layer.mask = nil
|
|
2601
|
+
cell.isHidden = false
|
|
2602
|
+
cell.accessibilityElementsHidden = false
|
|
2603
|
+
if let structuralTail, let itemIndex {
|
|
2604
|
+
presentTailCell(cell, key: item.key, index: itemIndex, tail: structuralTail)
|
|
2605
|
+
}
|
|
1717
2606
|
}
|
|
1718
2607
|
|
|
1719
2608
|
private func handleRowPress(_ item: NativeListItem, origin: NativeListActionOrigin?) {
|
|
1720
2609
|
// OneKey patch: missing-address rows keep accessory actions available.
|
|
1721
|
-
guard let config, item.isRowPressEnabled else { return }
|
|
2610
|
+
guard let config, item.isRowPressEnabled, !isInertPresentationKey(item.key) else { return }
|
|
1722
2611
|
if config.rowPressToggles && item.isSelectable && config.selectionMode != "none" {
|
|
1723
2612
|
updateSelection(target: NativeSelectionTarget(scope: "row", key: item.key), sourceKey: item.key)
|
|
1724
2613
|
return
|
|
@@ -1738,8 +2627,8 @@ final class NativeListView: UIView {
|
|
|
1738
2627
|
guard gesture.state == .began else { return }
|
|
1739
2628
|
let point = gesture.location(in: collectionView)
|
|
1740
2629
|
guard let indexPath = collectionView.indexPathForItem(at: point),
|
|
1741
|
-
let item =
|
|
1742
|
-
item.type == "market",
|
|
2630
|
+
let item = item(at: indexPath), !isInertPresentationKey(item.key),
|
|
2631
|
+
(item.type == "market" || item.type == "identity"),
|
|
1743
2632
|
item.isRowPressEnabled else { return }
|
|
1744
2633
|
let actionKey = item.data.string("longPressActionKey")
|
|
1745
2634
|
guard !actionKey.isEmpty else { return }
|
|
@@ -1754,7 +2643,7 @@ final class NativeListView: UIView {
|
|
|
1754
2643
|
target: NativeSelectionTarget?,
|
|
1755
2644
|
origin: NativeListActionOrigin?
|
|
1756
2645
|
) {
|
|
1757
|
-
guard !item.data.bool("disabled") else { return }
|
|
2646
|
+
guard !item.data.bool("disabled"), !isInertPresentationKey(item.key) else { return }
|
|
1758
2647
|
if let target, config?.selectionMode != "none" {
|
|
1759
2648
|
updateSelection(target: target, sourceKey: item.key)
|
|
1760
2649
|
return
|
|
@@ -1763,7 +2652,8 @@ final class NativeListView: UIView {
|
|
|
1763
2652
|
}
|
|
1764
2653
|
|
|
1765
2654
|
private func updateSelection(target: NativeSelectionTarget, sourceKey: String) {
|
|
1766
|
-
guard var current = config, current.selectionMode != "none"
|
|
2655
|
+
guard var current = config, current.selectionMode != "none",
|
|
2656
|
+
!isInertPresentationKey(sourceKey) else { return }
|
|
1767
2657
|
let keys = selectionKeys(target: target, items: current.items)
|
|
1768
2658
|
guard !keys.isEmpty else { return }
|
|
1769
2659
|
let before = current.selectedKeys
|
|
@@ -1824,7 +2714,7 @@ final class NativeListView: UIView {
|
|
|
1824
2714
|
self?.resolveCheckboxState(item: item, target: target, fallback: fallback) ?? fallback
|
|
1825
2715
|
}
|
|
1826
2716
|
for indexPath in collectionView.indexPathsForVisibleItems {
|
|
1827
|
-
guard let item = item(at: indexPath),
|
|
2717
|
+
guard let item = item(at: indexPath), !isInertPresentationKey(item.key),
|
|
1828
2718
|
let cell = collectionView.cellForItem(at: indexPath) as? NativeListRowHost else { continue }
|
|
1829
2719
|
if changedSummaryKeys.contains(item.key) {
|
|
1830
2720
|
bind(cell: cell, item: item, itemIndex: indexPath.item)
|
|
@@ -1975,6 +2865,7 @@ final class NativeListView: UIView {
|
|
|
1975
2865
|
let selectionChanged = current.selectionMode != next.selectionMode ||
|
|
1976
2866
|
current.selectedKeys != next.selectedKeys
|
|
1977
2867
|
if current.generation != next.generation { endReachedGeneration = nil }
|
|
2868
|
+
acceptRowIndex(for: next, replacing: current)
|
|
1978
2869
|
config = next
|
|
1979
2870
|
itemsByKey = Dictionary(uniqueKeysWithValues: next.items.map { ($0.key, $0) })
|
|
1980
2871
|
let changedKeys = next.items.compactMap { item -> String? in
|
|
@@ -2175,6 +3066,13 @@ final class NativeListView: UIView {
|
|
|
2175
3066
|
dataSource = nil
|
|
2176
3067
|
|
|
2177
3068
|
config = nil
|
|
3069
|
+
acceptedRowIndex = nil
|
|
3070
|
+
pagerStickyHeights.removeAllObjects()
|
|
3071
|
+
for slot in reactContainerSlots { slot.host.layer.mask = nil }
|
|
3072
|
+
stopStructuralAnimation()
|
|
3073
|
+
clearStructuralTail()
|
|
3074
|
+
structuralSpacerHeight = 0
|
|
3075
|
+
structuralFooterStartY = nil
|
|
2178
3076
|
itemsByKey.removeAll()
|
|
2179
3077
|
sectionIndexEntries.removeAll()
|
|
2180
3078
|
if let pagerCommandCancelObserver {
|
|
@@ -2193,8 +3091,10 @@ final class NativeListView: UIView {
|
|
|
2193
3091
|
onSelectionDelta = nil
|
|
2194
3092
|
onReorder = nil
|
|
2195
3093
|
onEndReached = nil
|
|
3094
|
+
onStructuralTransitionSettled = nil
|
|
2196
3095
|
onVisibleRangeChanged = nil
|
|
2197
3096
|
onScrollPositionThresholdChange = nil
|
|
3097
|
+
pendingDisclosureSettlement = nil
|
|
2198
3098
|
}
|
|
2199
3099
|
|
|
2200
3100
|
private func createActionAnchor(origin: NativeListActionOrigin) -> [String: Any]? {
|
|
@@ -2290,8 +3190,9 @@ final class NativeListView: UIView {
|
|
|
2290
3190
|
DispatchQueue.main.async { [weak self] in
|
|
2291
3191
|
guard let self else { return }
|
|
2292
3192
|
self.visibleEventScheduled = false
|
|
2293
|
-
guard
|
|
2294
|
-
|
|
3193
|
+
guard self.structuralRemovedItems.isEmpty,
|
|
3194
|
+
let onVisibleRangeChanged = self.onVisibleRangeChanged else { return }
|
|
3195
|
+
let indexes = self.acceptedVisibleIndexes()
|
|
2295
3196
|
let first = indexes.first ?? -1
|
|
2296
3197
|
let last = indexes.last ?? -1
|
|
2297
3198
|
let firstKey = self.config?.items[safe: first]?.key
|
|
@@ -2309,11 +3210,11 @@ final class NativeListView: UIView {
|
|
|
2309
3210
|
}
|
|
2310
3211
|
|
|
2311
3212
|
private func checkEndReached() {
|
|
2312
|
-
guard let config,
|
|
3213
|
+
guard let config, structuralRemovedItems.isEmpty,
|
|
2313
3214
|
config.loadMore,
|
|
2314
3215
|
endReachedGeneration != config.generation,
|
|
2315
3216
|
!config.items.isEmpty else { return }
|
|
2316
|
-
let lastVisible =
|
|
3217
|
+
let lastVisible = acceptedVisibleIndexes().last ?? -1
|
|
2317
3218
|
let threshold = max(1, Int(ceil(Double(config.items.count) * config.endReachedThreshold)))
|
|
2318
3219
|
guard lastVisible >= config.items.count - threshold else { return }
|
|
2319
3220
|
endReachedGeneration = config.generation
|
|
@@ -2322,6 +3223,18 @@ final class NativeListView: UIView {
|
|
|
2322
3223
|
emit(onEndReached, payload)
|
|
2323
3224
|
}
|
|
2324
3225
|
|
|
3226
|
+
private func acceptedVisibleIndexes() -> [Int] {
|
|
3227
|
+
guard let config else { return [] }
|
|
3228
|
+
let presentedPrefix = structuralTail?.prefixCount
|
|
3229
|
+
return collectionView.indexPathsForVisibleItems.compactMap { indexPath in
|
|
3230
|
+
guard presentedPrefix.map({ indexPath.item < $0 }) ?? true,
|
|
3231
|
+
let item = config.items[safe: indexPath.item],
|
|
3232
|
+
dataSource.itemIdentifier(for: indexPath) == item.key,
|
|
3233
|
+
let cell = collectionView.cellForItem(at: indexPath), !cell.isHidden else { return nil }
|
|
3234
|
+
return indexPath.item
|
|
3235
|
+
}.sorted()
|
|
3236
|
+
}
|
|
3237
|
+
|
|
2325
3238
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
2326
3239
|
if gestureRecognizer === keyboardDragRecognizer {
|
|
2327
3240
|
return keyboardDismissMode == .onDrag && focusedTextInput(in: window) != nil
|
|
@@ -2329,12 +3242,13 @@ final class NativeListView: UIView {
|
|
|
2329
3242
|
if gestureRecognizer === reorderLongPress || gestureRecognizer === marketLongPress {
|
|
2330
3243
|
let point = gestureRecognizer.location(in: collectionView)
|
|
2331
3244
|
guard let indexPath = collectionView.indexPathForItem(at: point),
|
|
2332
|
-
let item = item(at: indexPath),
|
|
3245
|
+
let item = item(at: indexPath), !isInertPresentationKey(item.key),
|
|
2333
3246
|
!item.data.bool("disabled") else { return false }
|
|
2334
3247
|
if gestureRecognizer === reorderLongPress {
|
|
2335
3248
|
return config?.reorderable == true && item.isReorderable
|
|
2336
3249
|
}
|
|
2337
|
-
return item.type == "market"
|
|
3250
|
+
return (item.type == "market" || item.type == "identity")
|
|
3251
|
+
&& item.isRowPressEnabled && !item.data.string("longPressActionKey").isEmpty
|
|
2338
3252
|
}
|
|
2339
3253
|
guard gestureRecognizer === listBodyGestureGuard,
|
|
2340
3254
|
let pan = gestureRecognizer as? UIPanGestureRecognizer else { return true }
|
|
@@ -2445,14 +3359,16 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2445
3359
|
return CGSize(width: width, height: height)
|
|
2446
3360
|
}
|
|
2447
3361
|
let available = max(0, collectionView.bounds.width - insets.left - insets.right)
|
|
2448
|
-
let structural = item.type == "sectionHeader" || item.type == "system" || item.type == "action"
|
|
3362
|
+
let structural = item.type == "sectionHeader" || item.type == "system" || item.type == "action" || item.type == "buttonGroup"
|
|
2449
3363
|
if config.layout == "grid", !structural {
|
|
2450
3364
|
let spacing = CGFloat(config.gridColumns - 1) * config.itemSpacing
|
|
2451
3365
|
let width = floor((available - spacing) / CGFloat(config.gridColumns))
|
|
2452
3366
|
let height = item.styledHeight ?? (item.type == "mediaTile" && item.data["height"] == nil ? width + 48 : rowHeight(item, allocatedWidth: width))
|
|
2453
3367
|
return CGSize(width: width, height: height)
|
|
2454
3368
|
}
|
|
2455
|
-
|
|
3369
|
+
let height = structuralRemovedHeights[item.key].map { $0 * (1 - structuralRemovalProgress) }
|
|
3370
|
+
?? rowHeight(item, allocatedWidth: available)
|
|
3371
|
+
return CGSize(width: available, height: max(0.01, height))
|
|
2456
3372
|
}
|
|
2457
3373
|
|
|
2458
3374
|
private func railWidth(_ item: NativeListItem) -> CGFloat {
|
|
@@ -2460,19 +3376,20 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2460
3376
|
}
|
|
2461
3377
|
|
|
2462
3378
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
2463
|
-
guard let item =
|
|
3379
|
+
guard let item = item(at: indexPath), !isInertPresentationKey(item.key) else { return }
|
|
2464
3380
|
if item.type == "walletGroup" { return }
|
|
2465
3381
|
let origin = (collectionView.cellForItem(at: indexPath) as? NativeListRowHost)?.rowActionOrigin()
|
|
2466
3382
|
handleRowPress(item, origin: origin)
|
|
2467
3383
|
}
|
|
2468
3384
|
|
|
2469
3385
|
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
|
|
2470
|
-
guard let item =
|
|
3386
|
+
guard let item = item(at: indexPath), !isInertPresentationKey(item.key) else { return false }
|
|
2471
3387
|
return item.isRowPressEnabled
|
|
2472
3388
|
}
|
|
2473
3389
|
|
|
2474
3390
|
func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
|
|
2475
|
-
guard let item =
|
|
3391
|
+
guard let item = item(at: indexPath), !isInertPresentationKey(item.key),
|
|
3392
|
+
item.type == "market" else { return }
|
|
2476
3393
|
let actionKey = item.data.string("pressInActionKey")
|
|
2477
3394
|
guard !actionKey.isEmpty else { return }
|
|
2478
3395
|
let origin = (collectionView.cellForItem(at: indexPath) as? NativeListRowHost)?.rowActionOrigin()
|
|
@@ -2480,7 +3397,7 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2480
3397
|
}
|
|
2481
3398
|
|
|
2482
3399
|
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
|
2483
|
-
guard let item =
|
|
3400
|
+
guard let item = item(at: indexPath), !isInertPresentationKey(item.key) else { return false }
|
|
2484
3401
|
return item.isRowPressEnabled
|
|
2485
3402
|
}
|
|
2486
3403
|
|
|
@@ -2492,6 +3409,15 @@ extension NativeListView: UICollectionViewDelegateFlowLayout {
|
|
|
2492
3409
|
}
|
|
2493
3410
|
|
|
2494
3411
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
3412
|
+
if structuralDisplayLink != nil {
|
|
3413
|
+
structuralScrollInterrupted = true
|
|
3414
|
+
if structuralTail != nil { finishStructuralTail(interrupted: true) }
|
|
3415
|
+
else {
|
|
3416
|
+
NotificationCenter.default.post(name: Notification.Name("OneKeyNativeListScrollCommandAccepted"),
|
|
3417
|
+
object: collectionView, userInfo: ["phase": 2])
|
|
3418
|
+
structuralScrollPhaseActive = false
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
2495
3421
|
scrollViewDidEndScrollingAnimation(scrollView)
|
|
2496
3422
|
minimumDragOffset = 0
|
|
2497
3423
|
refreshEmittedForDrag = false
|
|
@@ -2530,7 +3456,7 @@ extension NativeListView: UICollectionViewDragDelegate, UICollectionViewDropDele
|
|
|
2530
3456
|
itemsForBeginning session: UIDragSession,
|
|
2531
3457
|
at indexPath: IndexPath
|
|
2532
3458
|
) -> [UIDragItem] {
|
|
2533
|
-
guard config?.reorderable == true,
|
|
3459
|
+
guard config?.reorderable == true, structuralDisplayLink == nil,
|
|
2534
3460
|
let item = config?.items[safe: indexPath.item],
|
|
2535
3461
|
item.isReorderable else { return [] }
|
|
2536
3462
|
let dragItem = UIDragItem(itemProvider: NSItemProvider(object: item.key as NSString))
|
|
@@ -2557,6 +3483,7 @@ extension NativeListView: UICollectionViewDragDelegate, UICollectionViewDropDele
|
|
|
2557
3483
|
guard sourceItem.isReorderable, targetItem.isReorderable, sourceItem.sectionKey == targetItem.sectionKey else { return }
|
|
2558
3484
|
current.items.remove(at: source.item)
|
|
2559
3485
|
current.items.insert(sourceItem, at: destination.item)
|
|
3486
|
+
acceptedRowIndex = AcceptedRowIndex(items: current.items)
|
|
2560
3487
|
config = current
|
|
2561
3488
|
itemsByKey = Dictionary(uniqueKeysWithValues: current.items.map { ($0.key, $0) })
|
|
2562
3489
|
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
|
|
@@ -2602,12 +3529,26 @@ private final class NativeListCollectionView: UICollectionView {
|
|
|
2602
3529
|
}
|
|
2603
3530
|
}
|
|
2604
3531
|
|
|
3532
|
+
private final class NativeListClippedSlotHost: UIView {
|
|
3533
|
+
var visibleTop: CGFloat = 0
|
|
3534
|
+
var visibleBottom: CGFloat?
|
|
3535
|
+
|
|
3536
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
3537
|
+
super.point(inside: point, with: event) && point.y >= visibleTop
|
|
3538
|
+
&& point.y < (visibleBottom ?? bounds.height)
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
|
|
2605
3542
|
final class NativeListFlowLayout: UICollectionViewFlowLayout {
|
|
2606
3543
|
var stickyItemIndexes: Set<Int> = []
|
|
2607
3544
|
var minimumContainerContentHeight: CGFloat = 0
|
|
3545
|
+
var animatedBodyContentHeight: CGFloat?
|
|
3546
|
+
var tailClip: (firstIndex: Int, endY: CGFloat)?
|
|
3547
|
+
var bodyContentHeight: CGFloat {
|
|
3548
|
+
animatedBodyContentHeight ?? super.collectionViewContentSize.height
|
|
3549
|
+
}
|
|
2608
3550
|
private var horizontalAttributes: [IndexPath: UICollectionViewLayoutAttributes] = [:]
|
|
2609
3551
|
private var horizontalContentSize: CGSize = .zero
|
|
2610
|
-
|
|
2611
3552
|
override func prepare() {
|
|
2612
3553
|
super.prepare()
|
|
2613
3554
|
horizontalAttributes.removeAll(keepingCapacity: true)
|
|
@@ -2635,7 +3576,7 @@ final class NativeListFlowLayout: UICollectionViewFlowLayout {
|
|
|
2635
3576
|
override var collectionViewContentSize: CGSize {
|
|
2636
3577
|
if scrollDirection == .horizontal { return horizontalContentSize }
|
|
2637
3578
|
let size = super.collectionViewContentSize
|
|
2638
|
-
return CGSize(width: size.width, height: max(
|
|
3579
|
+
return CGSize(width: size.width, height: max(bodyContentHeight, minimumContainerContentHeight))
|
|
2639
3580
|
}
|
|
2640
3581
|
|
|
2641
3582
|
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
@@ -2672,10 +3613,17 @@ final class NativeListFlowLayout: UICollectionViewFlowLayout {
|
|
|
2672
3613
|
.filter { $0.frame.intersects(rect) }
|
|
2673
3614
|
.sorted { $0.indexPath.item < $1.indexPath.item }
|
|
2674
3615
|
}
|
|
2675
|
-
guard scrollDirection == .vertical,
|
|
2676
|
-
!stickyItemIndexes.isEmpty,
|
|
2677
|
-
let collectionView else { return super.layoutAttributesForElements(in: rect) }
|
|
2678
3616
|
let base = super.layoutAttributesForElements(in: rect)?.compactMap { $0.copy() as? UICollectionViewLayoutAttributes } ?? []
|
|
3617
|
+
guard scrollDirection == .vertical else { return base }
|
|
3618
|
+
let clipped = base.filter { attributes in
|
|
3619
|
+
guard let tailClip, attributes.representedElementCategory == .cell,
|
|
3620
|
+
attributes.indexPath.item >= tailClip.firstIndex else { return true }
|
|
3621
|
+
return attributes.frame.minY < tailClip.endY
|
|
3622
|
+
}
|
|
3623
|
+
guard !stickyItemIndexes.isEmpty,
|
|
3624
|
+
let collectionView else {
|
|
3625
|
+
return clipped
|
|
3626
|
+
}
|
|
2679
3627
|
let pinY = collectionView.contentOffset.y + collectionView.adjustedContentInset.top
|
|
2680
3628
|
// Visible cells can still describe the old viewport during an imperative
|
|
2681
3629
|
// scroll. Resolve the pinned header from unmodified layout positions.
|
|
@@ -2684,20 +3632,30 @@ final class NativeListFlowLayout: UICollectionViewFlowLayout {
|
|
|
2684
3632
|
return attributes.frame.minY <= pinY
|
|
2685
3633
|
}).max(),
|
|
2686
3634
|
let original = super.layoutAttributesForItem(at: IndexPath(item: stickyIndex, section: 0)),
|
|
2687
|
-
let sticky = original.copy() as? UICollectionViewLayoutAttributes else {
|
|
3635
|
+
let sticky = original.copy() as? UICollectionViewLayoutAttributes else {
|
|
3636
|
+
return clipped
|
|
3637
|
+
}
|
|
2688
3638
|
let nextHeaderY = stickyItemIndexes
|
|
2689
3639
|
.filter { $0 > stickyIndex }
|
|
2690
3640
|
.min()
|
|
2691
3641
|
.flatMap { super.layoutAttributesForItem(at: IndexPath(item: $0, section: 0))?.frame.minY }
|
|
2692
3642
|
sticky.frame.origin.y = min(max(pinY, original.frame.minY), (nextHeaderY ?? .greatestFiniteMagnitude) - sticky.frame.height)
|
|
2693
3643
|
sticky.zIndex = 10_000
|
|
2694
|
-
let filtered =
|
|
2695
|
-
|
|
3644
|
+
let filtered = clipped.filter { $0.indexPath != sticky.indexPath }
|
|
3645
|
+
let returned = filtered + [sticky]
|
|
3646
|
+
return returned
|
|
2696
3647
|
}
|
|
2697
3648
|
|
|
2698
3649
|
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
2699
3650
|
if scrollDirection == .horizontal { return horizontalAttributes[indexPath] }
|
|
2700
|
-
|
|
3651
|
+
guard let attributes = super.layoutAttributesForItem(at: indexPath) else {
|
|
3652
|
+
return nil
|
|
3653
|
+
}
|
|
3654
|
+
if let tailClip, indexPath.item >= tailClip.firstIndex,
|
|
3655
|
+
attributes.frame.minY >= tailClip.endY {
|
|
3656
|
+
return nil
|
|
3657
|
+
}
|
|
3658
|
+
return attributes
|
|
2701
3659
|
}
|
|
2702
3660
|
|
|
2703
3661
|
}
|