@onekeyfe/react-native-native-list 3.0.157-alpha.254 → 3.0.157-alpha.258
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/build.gradle +7 -0
- package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +20 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +179 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +53 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListEndAlignment.kt +5 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaPreviewSlot.kt +215 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +15 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +3 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListScrollPositionTracker.kt +29 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +16 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +165 -26
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +5 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListEndAlignmentTest.kt +34 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListScrollPositionTrackerTest.kt +28 -0
- package/docs/DESIGN.md +4 -2
- package/docs/ROW_TEMPLATES.md +45 -0
- package/docs/SPEC.md +241 -1
- package/docs/STYLE_SPEC.md +8 -0
- package/ios/HybridNativeList.swift +30 -0
- package/ios/NativeListActivityDetailsView.swift +229 -0
- package/ios/NativeListActivityRenderer.swift +24 -2
- package/ios/NativeListMediaPreviewSlot.swift +199 -0
- package/ios/NativeListMediaTileRenderer.swift +10 -4
- package/ios/NativeListModels.swift +4 -1
- package/ios/NativeListScrollPositionTracker.swift +30 -0
- package/ios/NativeListSectionHeaderRenderer.swift +27 -1
- package/ios/RNCNativeListView.swift +159 -4
- package/ios/tests/NativeListActivityPresentationTests.swift +43 -0
- package/ios/tests/NativeListLifecycleTests.swift +123 -0
- package/ios/tests/NativeListMediaVisibilityTests.swift +42 -0
- package/ios/tests/NativeListScrollPositionTrackerTests.swift +25 -0
- package/lib/module/NativeList.js +56 -3
- package/lib/module/NativeList.web.js +5 -0
- package/lib/module/validation.js +64 -4
- package/lib/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/lib/typescript/src/NativeList.nitro.d.ts +3 -0
- package/lib/typescript/src/NativeList.types.d.ts +14 -1
- package/lib/typescript/src/models.d.ts +33 -5
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/package.json +6 -5
- package/scripts/enable-container-slots.mjs +58 -0
- package/scripts/enable-container-slots.test.mjs +58 -0
- package/src/NativeList.nitro.ts +3 -0
- package/src/NativeList.tsx +84 -2
- package/src/NativeList.types.ts +14 -1
- package/src/NativeList.web.tsx +5 -0
- package/src/models.ts +38 -1
- package/src/validation.ts +146 -4
|
@@ -4,9 +4,11 @@ final class NativeListActivityCell: NativeListRendererCell {
|
|
|
4
4
|
override class func measure(
|
|
5
5
|
_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
|
|
6
6
|
) -> CGFloat? {
|
|
7
|
+
if item.data["amounts"] != nil { return NativeListActivityDetailsView.measure(item.data, width: width) }
|
|
7
8
|
return item.data.dictionaries("footerActions").isEmpty ? 60 : 100
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
private let details = NativeListActivityDetailsView()
|
|
10
12
|
private let visual = NativeListLeadingVisual()
|
|
11
13
|
private let column = UIStackView()
|
|
12
14
|
private let titleLine = UIStackView()
|
|
@@ -22,7 +24,7 @@ final class NativeListActivityCell: NativeListRendererCell {
|
|
|
22
24
|
private let buttons = (0..<3).map { _ in UIButton(type: .system) }
|
|
23
25
|
private var actionKeys: [String] = []
|
|
24
26
|
private var dimensions: [NSLayoutConstraint] = []
|
|
25
|
-
override var assetFields: [String] { ["leading", "secondaryLeading"] }
|
|
27
|
+
override var assetFields: [String] { ["leading", "secondaryLeading", "amounts"] }
|
|
26
28
|
|
|
27
29
|
override init(frame: CGRect) {
|
|
28
30
|
super.init(frame: frame)
|
|
@@ -60,7 +62,11 @@ final class NativeListActivityCell: NativeListRendererCell {
|
|
|
60
62
|
button.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
|
|
61
63
|
actions.addArrangedSubview(button)
|
|
62
64
|
}
|
|
63
|
-
[visual, column, amounts].forEach(root.addArrangedSubview)
|
|
65
|
+
[visual, column, amounts, details].forEach(root.addArrangedSubview)
|
|
66
|
+
details.isHidden = true
|
|
67
|
+
details.onAction = { [weak self] key, view, source, slot in
|
|
68
|
+
self?.emitAction(key, from: view, source: source, slot: slot)
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
66
72
|
override func bindContent(
|
|
@@ -68,6 +74,21 @@ final class NativeListActivityCell: NativeListRendererCell {
|
|
|
68
74
|
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
69
75
|
) {
|
|
70
76
|
let data = item.data
|
|
77
|
+
let rich = data["amounts"] != nil
|
|
78
|
+
details.isHidden = !rich
|
|
79
|
+
[visual, column, amounts].forEach { $0.isHidden = rich }
|
|
80
|
+
root.axis = rich ? .vertical : .horizontal
|
|
81
|
+
root.alignment = rich ? .fill : .center
|
|
82
|
+
if rich {
|
|
83
|
+
let style = data.dictionary("style") ?? [:]
|
|
84
|
+
let hp = CGFloat(style.double("horizontalPadding", default: data.string("presentation") == "table" ? 16 : 12))
|
|
85
|
+
let vp = CGFloat(style.double("verticalPadding", default: 8))
|
|
86
|
+
contentInsets = UIEdgeInsets(top: vp, left: hp, bottom: vp, right: hp)
|
|
87
|
+
visual.recycle()
|
|
88
|
+
details.bind(item, theme: theme)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
details.recycle()
|
|
71
92
|
let style = data.dictionary("style") ?? [:]
|
|
72
93
|
let image = style.dictionary("image") ?? [:]
|
|
73
94
|
root.spacing = 12
|
|
@@ -173,6 +194,7 @@ final class NativeListActivityCell: NativeListRendererCell {
|
|
|
173
194
|
}
|
|
174
195
|
override func recycleContent() {
|
|
175
196
|
visual.recycle()
|
|
197
|
+
details.recycle()
|
|
176
198
|
actionKeys = []
|
|
177
199
|
buttons.forEach { $0.isHidden = true }
|
|
178
200
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
private final class NativeListPausedVideoView: UIView {
|
|
5
|
+
var onWindowChanged: (() -> Void)?
|
|
6
|
+
var onLayoutChanged: (() -> Void)?
|
|
7
|
+
override class var layerClass: AnyClass { AVPlayerLayer.self }
|
|
8
|
+
var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
|
|
9
|
+
override func didMoveToWindow() { super.didMoveToWindow(); onWindowChanged?() }
|
|
10
|
+
override func layoutSubviews() { super.layoutSubviews(); onLayoutChanged?() }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Uses the same AVPlayer source semantics as native Video, always paused and muted.
|
|
14
|
+
final class NativeListMediaPreviewSlot {
|
|
15
|
+
let view = UIView()
|
|
16
|
+
private let image = NativeListImageSlot()
|
|
17
|
+
private let video = NativeListPausedVideoView()
|
|
18
|
+
private var identity: String?
|
|
19
|
+
private var epoch = 0
|
|
20
|
+
private var candidateGeneration = 0
|
|
21
|
+
private var playerGeneration = 0
|
|
22
|
+
private var result: Bool?
|
|
23
|
+
private var completion: ((Bool) -> Void)?
|
|
24
|
+
private var player: AVPlayer?
|
|
25
|
+
private var itemObservation: NSKeyValueObservation?
|
|
26
|
+
private var readyObservation: NSKeyValueObservation?
|
|
27
|
+
private var resumeVideo: (() -> Void)?
|
|
28
|
+
private var lifecycleObservers: [NSObjectProtocol] = []
|
|
29
|
+
private var viewportObservations: [NSKeyValueObservation] = []
|
|
30
|
+
|
|
31
|
+
init() {
|
|
32
|
+
view.isUserInteractionEnabled = false
|
|
33
|
+
for child in [image.view, video] {
|
|
34
|
+
child.frame = view.bounds
|
|
35
|
+
child.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
36
|
+
view.addSubview(child)
|
|
37
|
+
}
|
|
38
|
+
video.clipsToBounds = true
|
|
39
|
+
video.isHidden = true
|
|
40
|
+
video.onWindowChanged = { [weak self] in self?.observeViewport(); self?.updateVisibility() }
|
|
41
|
+
video.onLayoutChanged = { [weak self] in self?.updateVisibility() }
|
|
42
|
+
lifecycleObservers = [
|
|
43
|
+
NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in self?.releasePlayer() },
|
|
44
|
+
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in self?.updateVisibility() },
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func bind(
|
|
49
|
+
_ source: [String: Any], key: String, fit: String? = nil,
|
|
50
|
+
placeholder: String = "#0000000F", probeOrder: [String] = ["image"],
|
|
51
|
+
completion: ((Bool) -> Void)? = nil
|
|
52
|
+
) {
|
|
53
|
+
let next = NativeListImageSlot.signature([
|
|
54
|
+
"source": source, "key": key, "fit": fit ?? "", "order": probeOrder,
|
|
55
|
+
"placeholder": placeholder,
|
|
56
|
+
])
|
|
57
|
+
self.completion = completion
|
|
58
|
+
if identity == next {
|
|
59
|
+
if let result { completion?(result) }
|
|
60
|
+
updateVisibility()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
recycle()
|
|
64
|
+
identity = next
|
|
65
|
+
self.completion = completion
|
|
66
|
+
video.playerLayer.videoGravity = (fit ?? source.string("contentFit")) == "cover" ? .resizeAspectFill : .resizeAspect
|
|
67
|
+
probe(source, key: key, fit: fit, placeholder: placeholder, order: probeOrder, attempt: 0, token: epoch)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private func probe(
|
|
71
|
+
_ source: [String: Any], key: String, fit: String?, placeholder: String,
|
|
72
|
+
order: [String], attempt: Int, token: Int
|
|
73
|
+
) {
|
|
74
|
+
guard token == epoch else { return }
|
|
75
|
+
candidateGeneration &+= 1
|
|
76
|
+
let candidate = candidateGeneration
|
|
77
|
+
resumeVideo = nil
|
|
78
|
+
viewportObservations.removeAll()
|
|
79
|
+
releasePlayer()
|
|
80
|
+
guard attempt < order.count else {
|
|
81
|
+
result = false
|
|
82
|
+
completion?(false)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
let finished: (Bool) -> Void = { [weak self] loaded in
|
|
86
|
+
guard let self, self.epoch == token, self.candidateGeneration == candidate else { return }
|
|
87
|
+
if loaded {
|
|
88
|
+
guard self.result != true else { return }
|
|
89
|
+
self.result = true
|
|
90
|
+
self.completion?(true)
|
|
91
|
+
} else {
|
|
92
|
+
self.probe(source, key: key, fit: fit, placeholder: placeholder, order: order, attempt: attempt + 1, token: token)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if order[attempt] == "image" {
|
|
96
|
+
video.isHidden = true
|
|
97
|
+
image.view.isHidden = false
|
|
98
|
+
image.bind(source, key: key, fit: fit, placeholder: placeholder, completion: finished)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
image.recycle()
|
|
102
|
+
image.view.isHidden = true
|
|
103
|
+
video.isHidden = false
|
|
104
|
+
guard let url = URL(string: source.string("uri")), !source.string("uri").isEmpty else { finished(false); return }
|
|
105
|
+
resumeVideo = { [weak self] in
|
|
106
|
+
guard let self, self.epoch == token, self.candidateGeneration == candidate, self.player == nil else { return }
|
|
107
|
+
self.playerGeneration &+= 1
|
|
108
|
+
let generation = self.playerGeneration
|
|
109
|
+
let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": source.dictionary("headers") ?? [:]])
|
|
110
|
+
let item = AVPlayerItem(asset: asset)
|
|
111
|
+
item.preferredForwardBufferDuration = 1
|
|
112
|
+
let player = AVPlayer(playerItem: item)
|
|
113
|
+
player.isMuted = true
|
|
114
|
+
player.pause()
|
|
115
|
+
self.player = player
|
|
116
|
+
self.video.playerLayer.player = player
|
|
117
|
+
self.itemObservation = item.observe(\.status, options: [.initial, .new]) { [weak self] item, _ in
|
|
118
|
+
DispatchQueue.main.async {
|
|
119
|
+
guard let self, self.playerGeneration == generation else { return }
|
|
120
|
+
if item.status == .failed { finished(false) }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
self.readyObservation = self.video.playerLayer.observe(\.isReadyForDisplay, options: [.initial, .new]) { [weak self] layer, _ in
|
|
124
|
+
DispatchQueue.main.async {
|
|
125
|
+
guard let self, self.playerGeneration == generation else { return }
|
|
126
|
+
if layer.isReadyForDisplay { finished(true) }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
observeViewport()
|
|
131
|
+
updateVisibility()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func observeViewport() {
|
|
135
|
+
viewportObservations.removeAll()
|
|
136
|
+
guard resumeVideo != nil, video.window != nil else { return }
|
|
137
|
+
var ancestor = video.superview
|
|
138
|
+
while let current = ancestor {
|
|
139
|
+
if let scrollView = current as? UIScrollView {
|
|
140
|
+
viewportObservations.append(scrollView.observe(\.contentOffset, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
141
|
+
viewportObservations.append(scrollView.observe(\.bounds, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
142
|
+
}
|
|
143
|
+
// Pager or host layouts may translate a retained page without scrolling it.
|
|
144
|
+
viewportObservations.append(current.layer.observe(\.transform, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
145
|
+
viewportObservations.append(current.layer.observe(\.position, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
146
|
+
viewportObservations.append(current.layer.observe(\.isHidden, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
147
|
+
viewportObservations.append(current.layer.observe(\.opacity, options: [.new]) { [weak self] _, _ in self?.updateVisibility() })
|
|
148
|
+
ancestor = current.superview
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Retained pager pages can have a window while lying outside its clipped viewport.
|
|
153
|
+
static func intersectsViewport(_ view: UIView) -> Bool {
|
|
154
|
+
guard let window = view.window, !view.bounds.isEmpty else { return false }
|
|
155
|
+
var visible = view.convert(view.bounds, to: window).intersection(window.bounds)
|
|
156
|
+
var ancestor: UIView? = view
|
|
157
|
+
while let current = ancestor {
|
|
158
|
+
if current.isHidden || current.alpha <= 0.01 { return false }
|
|
159
|
+
if current.clipsToBounds || current is UIScrollView {
|
|
160
|
+
visible = visible.intersection(current.convert(current.bounds, to: window))
|
|
161
|
+
}
|
|
162
|
+
if visible.isNull || visible.isEmpty { return false }
|
|
163
|
+
ancestor = current.superview
|
|
164
|
+
}
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private func updateVisibility() {
|
|
169
|
+
if Self.intersectsViewport(video) && UIApplication.shared.applicationState == .active { resumeVideo?() }
|
|
170
|
+
else { releasePlayer() }
|
|
171
|
+
}
|
|
172
|
+
private func releasePlayer() {
|
|
173
|
+
playerGeneration &+= 1
|
|
174
|
+
itemObservation = nil
|
|
175
|
+
readyObservation = nil
|
|
176
|
+
player?.pause()
|
|
177
|
+
video.playerLayer.player = nil
|
|
178
|
+
player?.replaceCurrentItem(with: nil)
|
|
179
|
+
player = nil
|
|
180
|
+
}
|
|
181
|
+
func recycle() {
|
|
182
|
+
epoch &+= 1
|
|
183
|
+
candidateGeneration &+= 1
|
|
184
|
+
resumeVideo = nil
|
|
185
|
+
viewportObservations.removeAll()
|
|
186
|
+
releasePlayer()
|
|
187
|
+
image.recycle()
|
|
188
|
+
image.view.isHidden = false
|
|
189
|
+
video.isHidden = true
|
|
190
|
+
identity = nil
|
|
191
|
+
result = nil
|
|
192
|
+
completion = nil
|
|
193
|
+
}
|
|
194
|
+
deinit {
|
|
195
|
+
lifecycleObservers.forEach(NotificationCenter.default.removeObserver)
|
|
196
|
+
player?.pause()
|
|
197
|
+
player?.replaceCurrentItem(with: nil)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -8,7 +8,7 @@ final class NativeListMediaTileCell: NativeListRendererCell {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
private let picture = UIView()
|
|
11
|
-
private let image =
|
|
11
|
+
private let image = NativeListMediaPreviewSlot()
|
|
12
12
|
private let network = NativeListImageSlot()
|
|
13
13
|
private let errorIcon = UIImageView()
|
|
14
14
|
private let title = NativeListTextLabel()
|
|
@@ -27,7 +27,7 @@ final class NativeListMediaTileCell: NativeListRendererCell {
|
|
|
27
27
|
override var defaultCornerRadius: CGFloat { 16 }
|
|
28
28
|
override var showsSelection: Bool { false }
|
|
29
29
|
override var pressChangesBackground: Bool { false }
|
|
30
|
-
override var assetFields: [String] { ["image", "networkImage"] }
|
|
30
|
+
override var assetFields: [String] { ["image", "media", "networkImage"] }
|
|
31
31
|
override var isHighlighted: Bool { didSet { picture.alpha = isHighlighted ? 0.8 : 1 } }
|
|
32
32
|
|
|
33
33
|
override init(frame: CGRect) {
|
|
@@ -120,7 +120,8 @@ final class NativeListMediaTileCell: NativeListRendererCell {
|
|
|
120
120
|
errorIcon.isHidden = state != "error"
|
|
121
121
|
errorIcon.image = nativeListIcon(named: "ImageSquareWavesOutline")
|
|
122
122
|
errorIcon.tintColor = UIColor(nativeListHex: "#00000044", fallback: .lightGray)
|
|
123
|
-
|
|
123
|
+
let media = item.data.dictionary("media")
|
|
124
|
+
if state != "empty", state != "error", let source = media?.dictionary("source") ?? item.data.dictionary("image") {
|
|
124
125
|
// Legacy source-backed media had a transparent container. Clear only a fill this
|
|
125
126
|
// renderer applied, so OneKeyImage keeps ownership of its own placeholder color.
|
|
126
127
|
if ownsImageFill {
|
|
@@ -129,7 +130,12 @@ final class NativeListMediaTileCell: NativeListRendererCell {
|
|
|
129
130
|
}
|
|
130
131
|
image.bind(
|
|
131
132
|
source, key: "\(item.key):media", fit: imageStyle["contentFit"] as? String,
|
|
132
|
-
placeholder: placeholder
|
|
133
|
+
placeholder: placeholder, probeOrder: media?["probeOrder"] as? [String] ?? ["image"]
|
|
134
|
+
) { [weak self] loaded in
|
|
135
|
+
self?.errorIcon.isHidden = loaded
|
|
136
|
+
self?.ownsImageFill = !loaded
|
|
137
|
+
self?.image.view.backgroundColor = loaded ? .clear : UIColor(nativeListHex: placeholder, fallback: .lightGray)
|
|
138
|
+
}
|
|
133
139
|
} else {
|
|
134
140
|
image.recycle()
|
|
135
141
|
image.view.backgroundColor =
|
|
@@ -93,6 +93,7 @@ struct NativeListConfig {
|
|
|
93
93
|
var selectedKeys: Set<String>
|
|
94
94
|
let reorderable: Bool
|
|
95
95
|
let pullToRefresh: Bool
|
|
96
|
+
let refreshTriggerDistance: CGFloat?
|
|
96
97
|
var refreshing: Bool
|
|
97
98
|
let loadMore: Bool
|
|
98
99
|
let endReachedThreshold: Double
|
|
@@ -144,7 +145,7 @@ struct NativeListConfig {
|
|
|
144
145
|
generation: generation,
|
|
145
146
|
layout: layout,
|
|
146
147
|
orientation: layoutData["orientation"] as? String ?? "vertical",
|
|
147
|
-
gridColumns: min(
|
|
148
|
+
gridColumns: min(7, max(1, layoutData["gridColumns"] as? Int ?? 1)),
|
|
148
149
|
stickyHeaders: layoutData["stickyHeaders"] as? Bool ?? false,
|
|
149
150
|
contentPadding: contentPadding,
|
|
150
151
|
contentPaddingHorizontal: contentPaddingHorizontal,
|
|
@@ -156,6 +157,8 @@ struct NativeListConfig {
|
|
|
156
157
|
selectedKeys: selectedKeys,
|
|
157
158
|
reorderable: capabilities?["reorderable"] as? Bool ?? false,
|
|
158
159
|
pullToRefresh: capabilities?["pullToRefresh"] as? Bool ?? false,
|
|
160
|
+
refreshTriggerDistance: (capabilities?["refreshTriggerDistance"] as? Double)
|
|
161
|
+
.flatMap { $0.isFinite && $0 > 0 ? CGFloat($0) : nil },
|
|
159
162
|
refreshing: capabilities?["refreshing"] as? Bool ?? false,
|
|
160
163
|
loadMore: capabilities?["loadMore"] as? Bool ?? false,
|
|
161
164
|
endReachedThreshold: capabilities?["endReachedThreshold"] as? Double ?? 0.2,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// A per-view hysteresis observer. A nil result means no bridge event is needed.
|
|
4
|
+
struct NativeListScrollPositionTracker {
|
|
5
|
+
private var start: Double?
|
|
6
|
+
private var end: Double?
|
|
7
|
+
private var previous: Bool?
|
|
8
|
+
|
|
9
|
+
mutating func configure(json: String) {
|
|
10
|
+
start = nil
|
|
11
|
+
end = nil
|
|
12
|
+
previous = nil
|
|
13
|
+
guard let data = json.data(using: .utf8),
|
|
14
|
+
let values = try? JSONSerialization.jsonObject(with: data) as? [String: Double],
|
|
15
|
+
let start = values["start"], let end = values["end"],
|
|
16
|
+
start.isFinite, end.isFinite, start >= 0, end > start else { return }
|
|
17
|
+
self.start = start
|
|
18
|
+
self.end = end
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
mutating func resetDelivery() { previous = nil }
|
|
22
|
+
|
|
23
|
+
mutating func update(offset: Double) -> Bool? {
|
|
24
|
+
guard let start, let end, offset.isFinite else { return nil }
|
|
25
|
+
let next = previous == true ? offset > start : offset >= end
|
|
26
|
+
guard previous != next else { return nil }
|
|
27
|
+
previous = next
|
|
28
|
+
return next
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -99,7 +99,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
99
99
|
if layout == "table" { return 28 }
|
|
100
100
|
if item.data.string("presentation") == "networkSelector" { return 47 }
|
|
101
101
|
if item.data.string("variant") == "history" {
|
|
102
|
-
return 16
|
|
102
|
+
return item.data.bool("titleLoading") ? 20 : 16
|
|
103
103
|
}
|
|
104
104
|
if item.data.string("variant") == "summary" { return 68 }
|
|
105
105
|
if item.data.string("variant") == "gallery" { return 32 }
|
|
@@ -110,6 +110,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
110
110
|
private let mainStack = UIStackView()
|
|
111
111
|
private let titleRowStack = UIStackView()
|
|
112
112
|
private let titleLabel = NativeListDottedUnderlineLabel()
|
|
113
|
+
private let titleSpinner = UIActivityIndicatorView(style: .medium)
|
|
113
114
|
private let subtitleLabel = NativeListTextLabel()
|
|
114
115
|
private let trailingStack = UIStackView()
|
|
115
116
|
private let accessoryButtons = [NativeListAccessoryButton(type: .system)]
|
|
@@ -132,6 +133,12 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
132
133
|
root.axis = .horizontal
|
|
133
134
|
root.alignment = .center
|
|
134
135
|
root.spacing = 12
|
|
136
|
+
titleSpinner.translatesAutoresizingMaskIntoConstraints = false
|
|
137
|
+
titleSpinner.isAccessibilityElement = false
|
|
138
|
+
NSLayoutConstraint.activate([
|
|
139
|
+
titleSpinner.widthAnchor.constraint(equalToConstant: 20),
|
|
140
|
+
titleSpinner.heightAnchor.constraint(equalToConstant: 20),
|
|
141
|
+
])
|
|
135
142
|
mainStack.axis = .vertical
|
|
136
143
|
mainStack.alignment = .fill
|
|
137
144
|
mainStack.spacing = 2
|
|
@@ -174,6 +181,7 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
174
181
|
selectorTypographyRestorers = []
|
|
175
182
|
currentItem = item
|
|
176
183
|
currentTheme = theme
|
|
184
|
+
titleSpinner.stopAnimating()
|
|
177
185
|
NSLayoutConstraint.deactivate(selectorConstraints)
|
|
178
186
|
selectorConstraints = []
|
|
179
187
|
root.arrangedSubviews.forEach {
|
|
@@ -244,6 +252,8 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
244
252
|
if style["lineGap"] != nil { mainStack.spacing = CGFloat(style.double("lineGap")) }
|
|
245
253
|
if style["trailingGap"] != nil { trailingStack.spacing = CGFloat(style.double("trailingGap")) }
|
|
246
254
|
applyTextStyle(item)
|
|
255
|
+
titleSpinner.color = titleLabel.textColor
|
|
256
|
+
updateTitleSpinner()
|
|
247
257
|
contentInsets = UIEdgeInsets(
|
|
248
258
|
top: topInset, left: leftInset, bottom: -bottomInset, right: -rightInset)
|
|
249
259
|
root.alignment =
|
|
@@ -301,6 +311,18 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
301
311
|
override func recycleContent() {
|
|
302
312
|
checkboxControl.reset()
|
|
303
313
|
currentItem = nil
|
|
314
|
+
titleSpinner.stopAnimating()
|
|
315
|
+
}
|
|
316
|
+
override func didMoveToWindow() {
|
|
317
|
+
super.didMoveToWindow()
|
|
318
|
+
updateTitleSpinner()
|
|
319
|
+
}
|
|
320
|
+
private func updateTitleSpinner() {
|
|
321
|
+
if window != nil, currentItem?.data.bool("titleLoading") == true {
|
|
322
|
+
titleSpinner.startAnimating()
|
|
323
|
+
} else {
|
|
324
|
+
titleSpinner.stopAnimating()
|
|
325
|
+
}
|
|
304
326
|
}
|
|
305
327
|
@objc private func titlePressed() {
|
|
306
328
|
if let item = currentItem {
|
|
@@ -324,6 +346,10 @@ final class NativeListSectionHeaderCell: NativeListRendererCell {
|
|
|
324
346
|
layout: String,
|
|
325
347
|
_ checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
326
348
|
) {
|
|
349
|
+
if item.data.bool("titleLoading") {
|
|
350
|
+
root.addArrangedSubview(titleSpinner)
|
|
351
|
+
root.setCustomSpacing(8, after: titleSpinner)
|
|
352
|
+
}
|
|
327
353
|
root.addArrangedSubview(mainStack)
|
|
328
354
|
let variant = item.data.string("variant")
|
|
329
355
|
// OneKey patch: title help is a separate target from checkbox and value actions.
|