@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- package/ios/NativeListCell.swift +0 -4300
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// Shared chrome and binding lifetime; the subclass owns its template views.
|
|
4
|
+
enum NativeListRendererUpdate { case unchanged, content, assets, replace }
|
|
5
|
+
|
|
6
|
+
class NativeListRendererCell: NativeListRowHost {
|
|
7
|
+
let root = UIStackView()
|
|
8
|
+
var contentInsets = UIEdgeInsets.zero
|
|
9
|
+
var usesIntrinsicContentHeight: Bool { false }
|
|
10
|
+
var defaultBorderWidth: CGFloat { 0 }
|
|
11
|
+
func defaultBorderColor(_ theme: [String: Any]?) -> UIColor { .clear }
|
|
12
|
+
var defaultCornerRadius: CGFloat { 0 }
|
|
13
|
+
/// Legacy pressed rows round all corners (12pt) while highlighted; nil opts out.
|
|
14
|
+
var pressedCornerRadius: CGFloat? { pressChangesBackground ? 12 : nil }
|
|
15
|
+
/// Radius for `groupPosition` rows when listStyle does not set one (legacy 12).
|
|
16
|
+
var groupCornerRadius: CGFloat { 12 }
|
|
17
|
+
var defaultCornerCurve: CALayerCornerCurve { .circular }
|
|
18
|
+
var pressChangesBackground: Bool { true }
|
|
19
|
+
func pressedBackground(_ theme: [String: Any]?) -> UIColor {
|
|
20
|
+
nativeListColor(theme, "rowPressedBackground", "#E8E8E8")
|
|
21
|
+
}
|
|
22
|
+
var showsSelection: Bool { true }
|
|
23
|
+
/// Whether the legacy row-level `backgroundColor` paints this row (resting and full-width
|
|
24
|
+
/// fills). `style.container.backgroundColor` always applies.
|
|
25
|
+
var honorsRowBackgroundColor: Bool { true }
|
|
26
|
+
var defaultVerticalAlignment: String? { nil }
|
|
27
|
+
var defaultSeparatorInset: CGFloat { 12 }
|
|
28
|
+
func bindContent(
|
|
29
|
+
_ item: NativeListItem, theme: [String: Any]?, layout: String,
|
|
30
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
31
|
+
) {}
|
|
32
|
+
func recycleContent() {}
|
|
33
|
+
func bindSelectionContent(
|
|
34
|
+
_ item: NativeListItem,
|
|
35
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
36
|
+
) {}
|
|
37
|
+
var assetFields: [String] { [] }
|
|
38
|
+
private func update(from old: NativeListItem?, to new: NativeListItem) -> NativeListRendererUpdate
|
|
39
|
+
{
|
|
40
|
+
guard let old, old.key == new.key, old.rendererKey == new.rendererKey else { return .replace }
|
|
41
|
+
if old.content == new.content { return .unchanged }
|
|
42
|
+
for field in assetFields where !nativeListValuesEqual(old.data[field], new.data[field]) {
|
|
43
|
+
return .assets
|
|
44
|
+
}
|
|
45
|
+
return .content
|
|
46
|
+
}
|
|
47
|
+
private let separator = UIView()
|
|
48
|
+
private let fullWidthBackground = CALayer()
|
|
49
|
+
private var contentPosition: NSLayoutConstraint?
|
|
50
|
+
private var rootConstraints: [NSLayoutConstraint] = []
|
|
51
|
+
private var item: NativeListItem?
|
|
52
|
+
private var theme: [String: Any]?
|
|
53
|
+
private var layout = "linear"
|
|
54
|
+
private var itemIndex: Int?
|
|
55
|
+
func unselectedBackground(
|
|
56
|
+
_ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
|
|
57
|
+
) -> UIColor { nativeListColor(theme, "rowBackground", "#FFFFFF") }
|
|
58
|
+
private(set) var selectedState = false
|
|
59
|
+
// Last applied list-level inputs; compared structurally (no serialization/hashing per bind).
|
|
60
|
+
// The layout direction is an input because renderers resolve start/end alignment at bind.
|
|
61
|
+
private var boundInputs:
|
|
62
|
+
(
|
|
63
|
+
theme: NSDictionary?, layout: String, listStyle: NSDictionary?,
|
|
64
|
+
direction: UIUserInterfaceLayoutDirection
|
|
65
|
+
)?
|
|
66
|
+
|
|
67
|
+
override init(frame: CGRect) {
|
|
68
|
+
super.init(frame: frame)
|
|
69
|
+
root.translatesAutoresizingMaskIntoConstraints = false
|
|
70
|
+
contentView.addSubview(root)
|
|
71
|
+
contentView.addSubview(separator)
|
|
72
|
+
rootConstraints = [
|
|
73
|
+
root.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
74
|
+
root.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
75
|
+
root.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
76
|
+
root.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
|
77
|
+
]
|
|
78
|
+
NSLayoutConstraint.activate(rootConstraints)
|
|
79
|
+
}
|
|
80
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
81
|
+
|
|
82
|
+
func accessibilityText(_ item: NativeListItem) -> String {
|
|
83
|
+
item.data.string("accessibilityLabel", default: item.data.string("title"))
|
|
84
|
+
}
|
|
85
|
+
@discardableResult func retainBoundItem(_ item: NativeListItem) -> Bool {
|
|
86
|
+
guard self.item?.key == item.key else { return false }
|
|
87
|
+
self.item = item
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override func bind(
|
|
92
|
+
item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int? = nil,
|
|
93
|
+
selected: Bool, checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
94
|
+
) {
|
|
95
|
+
let update = update(from: self.item, to: item)
|
|
96
|
+
let inputs = (
|
|
97
|
+
theme: theme.map { $0 as NSDictionary }, layout: layout,
|
|
98
|
+
listStyle: listStyle.map { $0 as NSDictionary },
|
|
99
|
+
direction: effectiveUserInterfaceLayoutDirection
|
|
100
|
+
)
|
|
101
|
+
let inputsChanged =
|
|
102
|
+
boundInputs.map {
|
|
103
|
+
// A missing theme/listStyle is the empty object, as in the former `?? [:]` JSON digest.
|
|
104
|
+
$0.layout != inputs.layout || $0.direction != inputs.direction
|
|
105
|
+
|| !nativeListValuesEqual($0.theme ?? [:], inputs.theme ?? [:])
|
|
106
|
+
|| !nativeListValuesEqual($0.listStyle ?? [:], inputs.listStyle ?? [:])
|
|
107
|
+
} ?? true
|
|
108
|
+
if update != .unchanged || inputsChanged {
|
|
109
|
+
if self.item != nil { onBindingInvalidated?(self, bindingEpoch) }
|
|
110
|
+
bindingEpoch &+= 1
|
|
111
|
+
if update == .replace {
|
|
112
|
+
recycleContent()
|
|
113
|
+
isHighlighted = false
|
|
114
|
+
}
|
|
115
|
+
contentPosition?.isActive = false
|
|
116
|
+
contentPosition = nil
|
|
117
|
+
NSLayoutConstraint.activate(Array(rootConstraints.suffix(2)))
|
|
118
|
+
bindContent(item, theme: theme, layout: layout, checkboxState: checkboxState)
|
|
119
|
+
rootConstraints[0].constant = contentInsets.left
|
|
120
|
+
rootConstraints[1].constant = -contentInsets.right
|
|
121
|
+
rootConstraints[2].constant = contentInsets.top
|
|
122
|
+
rootConstraints[3].constant = -contentInsets.bottom
|
|
123
|
+
if root.axis == .vertical,
|
|
124
|
+
let alignment = item.data.dictionary("style")?.dictionary("container")?[
|
|
125
|
+
"contentVerticalAlignment"] as? String
|
|
126
|
+
?? (item.styledHeight != nil || usesIntrinsicContentHeight
|
|
127
|
+
? (defaultVerticalAlignment ?? "top") : nil)
|
|
128
|
+
{
|
|
129
|
+
NSLayoutConstraint.deactivate(Array(rootConstraints.suffix(2)))
|
|
130
|
+
contentPosition =
|
|
131
|
+
alignment == "bottom"
|
|
132
|
+
? root.bottomAnchor.constraint(
|
|
133
|
+
equalTo: contentView.bottomAnchor, constant: -contentInsets.bottom)
|
|
134
|
+
: alignment == "center"
|
|
135
|
+
? root.centerYAnchor.constraint(
|
|
136
|
+
equalTo: contentView.centerYAnchor,
|
|
137
|
+
constant: (contentInsets.top - contentInsets.bottom) / 2)
|
|
138
|
+
: root.topAnchor.constraint(equalTo: contentView.topAnchor, constant: contentInsets.top)
|
|
139
|
+
contentPosition?.isActive = true
|
|
140
|
+
}
|
|
141
|
+
boundInputs = inputs
|
|
142
|
+
}
|
|
143
|
+
self.item = item
|
|
144
|
+
self.theme = theme
|
|
145
|
+
self.layout = layout
|
|
146
|
+
self.itemIndex = itemIndex
|
|
147
|
+
self.selectedState = selected
|
|
148
|
+
bindSelectionContent(item, checkboxState: checkboxState)
|
|
149
|
+
accessibilityLabel = accessibilityText(item)
|
|
150
|
+
accessibilityIdentifier = item.data["testID"] as? String
|
|
151
|
+
isUserInteractionEnabled = !item.data.bool("disabled")
|
|
152
|
+
if !isUserInteractionEnabled { isHighlighted = false }
|
|
153
|
+
applyAppearance()
|
|
154
|
+
setNeedsLayout()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
override func updateSelection(
|
|
158
|
+
item: NativeListItem, selected: Bool,
|
|
159
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
160
|
+
) {
|
|
161
|
+
guard self.item?.key == item.key else { return }
|
|
162
|
+
self.item = item
|
|
163
|
+
self.selectedState = selected
|
|
164
|
+
bindSelectionContent(item, checkboxState: checkboxState)
|
|
165
|
+
applyAppearance()
|
|
166
|
+
}
|
|
167
|
+
override var isHighlighted: Bool { didSet { applyAppearance() } }
|
|
168
|
+
|
|
169
|
+
func emitAction(
|
|
170
|
+
_ key: String, from view: UIView, source: String, slot: Int? = nil,
|
|
171
|
+
target: NativeSelectionTarget? = nil, anchorInset: CGFloat = 0
|
|
172
|
+
) {
|
|
173
|
+
guard let item, !key.isEmpty, !item.data.bool("disabled") else { return }
|
|
174
|
+
onAction?(
|
|
175
|
+
item, key, target,
|
|
176
|
+
NativeListActionOrigin(
|
|
177
|
+
sourceView: view, ownerCell: self,
|
|
178
|
+
bindingEpoch: bindingEpoch, source: source, slot: slot, anchorInset: anchorInset))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func applyAppearance() {
|
|
182
|
+
guard let item else { return }
|
|
183
|
+
let container = item.data.dictionary("style")?.dictionary("container") ?? [:]
|
|
184
|
+
let showSelection =
|
|
185
|
+
showsSelection && selectedState && (layout != "sectioned" || item.data.bool("selected"))
|
|
186
|
+
var background =
|
|
187
|
+
showSelection
|
|
188
|
+
? nativeListColor(theme, "rowSelectedBackground", "#F0F0F0")
|
|
189
|
+
: unselectedBackground(item, theme: theme, layout: layout, itemIndex: itemIndex)
|
|
190
|
+
let rowFill =
|
|
191
|
+
container["backgroundColor"]
|
|
192
|
+
?? (honorsRowBackgroundColor ? item.data["backgroundColor"] : nil)
|
|
193
|
+
if let color = rowFill as? String {
|
|
194
|
+
background = UIColor(nativeListHex: color, fallback: background)
|
|
195
|
+
}
|
|
196
|
+
contentView.backgroundColor =
|
|
197
|
+
isHighlighted && pressChangesBackground
|
|
198
|
+
? pressedBackground(theme) : background
|
|
199
|
+
contentView.alpha =
|
|
200
|
+
CGFloat(container.double("opacity", default: item.data.double("opacity", default: 1)))
|
|
201
|
+
* (item.data.bool("disabled") ? 0.5 : 1)
|
|
202
|
+
let position = item.data.string("groupPosition")
|
|
203
|
+
let grouped = ["first", "last", "single"].contains(position)
|
|
204
|
+
let pressedRadius =
|
|
205
|
+
isHighlighted && isUserInteractionEnabled ? pressedCornerRadius.map(Double.init) : nil
|
|
206
|
+
let fallbackRadius =
|
|
207
|
+
defaultCornerRadius > 0
|
|
208
|
+
? Double(defaultCornerRadius)
|
|
209
|
+
: grouped
|
|
210
|
+
? listStyle?.double("groupCornerRadius", default: Double(groupCornerRadius))
|
|
211
|
+
?? Double(groupCornerRadius) : 0
|
|
212
|
+
let radius = CGFloat(
|
|
213
|
+
container.double(
|
|
214
|
+
"cornerRadius", default: pressedRadius ?? fallbackRadius))
|
|
215
|
+
let top: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
216
|
+
let bottom: CACornerMask = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
217
|
+
layer.maskedCorners =
|
|
218
|
+
container["cornerRadius"] != nil || position == "single" || defaultCornerRadius > 0
|
|
219
|
+
|| pressedRadius != nil
|
|
220
|
+
? top.union(bottom) : position == "first" ? top : position == "last" ? bottom : []
|
|
221
|
+
layer.cornerCurve = defaultCornerCurve
|
|
222
|
+
contentView.layer.cornerCurve = defaultCornerCurve
|
|
223
|
+
layer.cornerRadius = radius
|
|
224
|
+
layer.masksToBounds = radius > 0
|
|
225
|
+
contentView.layer.cornerRadius = radius
|
|
226
|
+
contentView.layer.maskedCorners = layer.maskedCorners
|
|
227
|
+
contentView.layer.borderWidth = CGFloat(
|
|
228
|
+
container.double("borderWidth", default: Double(defaultBorderWidth)))
|
|
229
|
+
contentView.layer.borderColor =
|
|
230
|
+
UIColor(
|
|
231
|
+
nativeListHex: container.string("borderColor"), fallback: defaultBorderColor(theme)
|
|
232
|
+
).cgColor
|
|
233
|
+
if item.data.bool("backgroundFullWidth"),
|
|
234
|
+
let fill = rowFill as? String
|
|
235
|
+
{
|
|
236
|
+
fullWidthBackground.backgroundColor = UIColor(nativeListHex: fill, fallback: .clear).cgColor
|
|
237
|
+
contentView.layer.insertSublayer(fullWidthBackground, at: 0)
|
|
238
|
+
clipsToBounds = false
|
|
239
|
+
} else {
|
|
240
|
+
fullWidthBackground.removeFromSuperlayer()
|
|
241
|
+
}
|
|
242
|
+
separator.isHidden = !item.data.bool("separator")
|
|
243
|
+
separator.backgroundColor = UIColor(
|
|
244
|
+
nativeListHex: listStyle?.dictionary("separator")?.string(
|
|
245
|
+
"color", default: theme?.string("separator", default: "#E0E0E0") ?? "#E0E0E0") ?? theme?
|
|
246
|
+
.string("separator", default: "#E0E0E0") ?? "#E0E0E0", fallback: .lightGray)
|
|
247
|
+
}
|
|
248
|
+
override func layoutSubviews() {
|
|
249
|
+
super.layoutSubviews()
|
|
250
|
+
fullWidthBackground.frame = CGRect(
|
|
251
|
+
x: -frame.minX, y: 0, width: superview?.bounds.width ?? bounds.width, height: bounds.height)
|
|
252
|
+
let inset = CGFloat(
|
|
253
|
+
listStyle?.dictionary("separator")?.double("inset", default: Double(defaultSeparatorInset))
|
|
254
|
+
?? Double(defaultSeparatorInset))
|
|
255
|
+
separator.frame = CGRect(
|
|
256
|
+
x: effectiveUserInterfaceLayoutDirection == .rightToLeft ? 0 : inset,
|
|
257
|
+
y: contentView.bounds.height - 1 / UIScreen.main.scale,
|
|
258
|
+
width: max(0, contentView.bounds.width - inset), height: 1 / UIScreen.main.scale)
|
|
259
|
+
}
|
|
260
|
+
override func prepareForReuse() {
|
|
261
|
+
super.prepareForReuse()
|
|
262
|
+
if item != nil { onBindingInvalidated?(self, bindingEpoch) }
|
|
263
|
+
bindingEpoch &+= 1
|
|
264
|
+
item = nil
|
|
265
|
+
boundInputs = nil
|
|
266
|
+
recycleContent()
|
|
267
|
+
fullWidthBackground.removeFromSuperlayer()
|
|
268
|
+
isHighlighted = false
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Type-aware structural equality for bridged JSON values, matching the former
|
|
273
|
+
/// sorted-keys JSON digest without serializing: `true` != `1` (CFBoolean vs NSNumber),
|
|
274
|
+
/// numbers compare by value (`1` == `1.0`), strings by UTF-16 code units, and inside objects
|
|
275
|
+
/// an explicit `null` differs from a missing key. At the top level a missing value equals
|
|
276
|
+
/// `NSNull` (the digest used `value ?? NSNull()`).
|
|
277
|
+
func nativeListValuesEqual(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
|
278
|
+
switch (lhs.flatMap { $0 is NSNull ? nil : $0 }, rhs.flatMap { $0 is NSNull ? nil : $0 }) {
|
|
279
|
+
case (nil, nil): return true
|
|
280
|
+
case let (lhs?, rhs?): return nativeListJSONEqual(lhs as AnyObject, rhs as AnyObject)
|
|
281
|
+
default: return false
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private func nativeListJSONEqual(_ lhs: AnyObject, _ rhs: AnyObject) -> Bool {
|
|
286
|
+
if lhs === rhs { return true }
|
|
287
|
+
if let lhs = lhs as? NSNumber, let rhs = rhs as? NSNumber {
|
|
288
|
+
let boolean = CFBooleanGetTypeID()
|
|
289
|
+
return (CFGetTypeID(lhs) == boolean) == (CFGetTypeID(rhs) == boolean) && lhs.isEqual(to: rhs)
|
|
290
|
+
}
|
|
291
|
+
if let lhs = lhs as? NSString, let rhs = rhs as? NSString { return lhs.isEqual(to: rhs as String) }
|
|
292
|
+
if let lhs = lhs as? NSDictionary, let rhs = rhs as? NSDictionary {
|
|
293
|
+
guard lhs.count == rhs.count else { return false }
|
|
294
|
+
for (key, value) in lhs {
|
|
295
|
+
guard let other = rhs.object(forKey: key),
|
|
296
|
+
nativeListJSONEqual(value as AnyObject, other as AnyObject)
|
|
297
|
+
else { return false }
|
|
298
|
+
}
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
if let lhs = lhs as? NSArray, let rhs = rhs as? NSArray {
|
|
302
|
+
guard lhs.count == rhs.count else { return false }
|
|
303
|
+
for index in 0..<lhs.count
|
|
304
|
+
where !nativeListJSONEqual(lhs[index] as AnyObject, rhs[index] as AnyObject) {
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
if lhs is NSNull || rhs is NSNull { return lhs is NSNull && rhs is NSNull }
|
|
310
|
+
return lhs.isEqual(rhs)
|
|
311
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
// Closed internal registry; no public plugin or bridge surface.
|
|
4
|
+
enum NativeListRendererRegistry {
|
|
5
|
+
private static let hosts:
|
|
6
|
+
[NativeListRendererKey: (type: NativeListRowHost.Type, create: () -> NativeListRowHost)] = [
|
|
7
|
+
.sectionHeader: (
|
|
8
|
+
NativeListSectionHeaderCell.self, { NativeListSectionHeaderCell(frame: .zero) }
|
|
9
|
+
),
|
|
10
|
+
.walletGroup: (NativeListWalletGroupCell.self, { NativeListWalletGroupCell(frame: .zero) }),
|
|
11
|
+
.identity: (NativeListIdentityCell.self, { NativeListIdentityCell(frame: .zero) }),
|
|
12
|
+
.market: (NativeListMarketCell.self, { NativeListMarketCell(frame: .zero) }),
|
|
13
|
+
.message: (NativeListMessageCell.self, { NativeListMessageCell(frame: .zero) }),
|
|
14
|
+
.rail: (NativeListRailCell.self, { NativeListRailCell(frame: .zero) }),
|
|
15
|
+
.mediaTile: (NativeListMediaTileCell.self, { NativeListMediaTileCell(frame: .zero) }),
|
|
16
|
+
.metricCard: (NativeListMetricCardCell.self, { NativeListMetricCardCell(frame: .zero) }),
|
|
17
|
+
.dataRow: (NativeListDataRowCell.self, { NativeListDataRowCell(frame: .zero) }),
|
|
18
|
+
.activity: (NativeListActivityCell.self, { NativeListActivityCell(frame: .zero) }),
|
|
19
|
+
.system: (NativeListSystemCell.self, { NativeListSystemCell(frame: .zero) }),
|
|
20
|
+
.action: (NativeListActionCell.self, { NativeListActionCell(frame: .zero) }),
|
|
21
|
+
.unsupported: (
|
|
22
|
+
NativeListUnsupportedCell.self, { NativeListUnsupportedCell(frame: .zero) }
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
static func create(_ key: NativeListRendererKey) -> NativeListRowHost { hosts[key]!.create() }
|
|
26
|
+
static func reuseIdentifier(for key: NativeListRendererKey) -> String {
|
|
27
|
+
"NativeList.\(key.rawValue)"
|
|
28
|
+
}
|
|
29
|
+
static func register(in collectionView: UICollectionView) {
|
|
30
|
+
for (key, host) in hosts {
|
|
31
|
+
collectionView.register(host.type, forCellWithReuseIdentifier: reuseIdentifier(for: key))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
static func appliesSizePreset(_ item: NativeListItem) -> Bool {
|
|
35
|
+
hosts[item.rendererKey]!.type.appliesSizePreset(item)
|
|
36
|
+
}
|
|
37
|
+
static func measure(_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String)
|
|
38
|
+
-> CGFloat?
|
|
39
|
+
{
|
|
40
|
+
hosts[item.rendererKey]!.type.measure(item, width: width, theme: theme, layout: layout)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Legacy fallback for unknown row types: shared chrome only, 56-point default height
|
|
45
|
+
/// (plus the size preset), no template content.
|
|
46
|
+
final class NativeListUnsupportedCell: NativeListRendererCell {
|
|
47
|
+
override class func measure(
|
|
48
|
+
_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
|
|
49
|
+
) -> CGFloat? { 56 }
|
|
50
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct NativeListResolvedText {
|
|
4
|
+
let text: String
|
|
5
|
+
let font: UIFont
|
|
6
|
+
let color: UIColor
|
|
7
|
+
let lineHeight: CGFloat
|
|
8
|
+
let lines: Int
|
|
9
|
+
/// Semantic `start | center | end`; nil keeps the legacy `.natural` default.
|
|
10
|
+
let alignmentName: String?
|
|
11
|
+
let breakMode: NSLineBreakMode
|
|
12
|
+
let verticalAlignment: String?
|
|
13
|
+
let offsetY: CGFloat
|
|
14
|
+
let explicitLineHeight: Bool
|
|
15
|
+
let explicitTruncation: Bool
|
|
16
|
+
let tabular: Bool
|
|
17
|
+
/// Keep the label's break mode in the attributed paragraph (legacy plain-text labels kept
|
|
18
|
+
/// their tail ellipsis; attributed paragraphs otherwise default to word wrapping).
|
|
19
|
+
let preservesBreakMode: Bool
|
|
20
|
+
|
|
21
|
+
init(
|
|
22
|
+
_ value: String, style: [String: Any]?, size: CGFloat, weight: NativeListFontWeight = .regular,
|
|
23
|
+
color: UIColor, lineHeight: CGFloat, lines: Int, breakMode: NSLineBreakMode = .byTruncatingTail,
|
|
24
|
+
tabular: Bool = false, preservesBreakMode: Bool = false
|
|
25
|
+
) {
|
|
26
|
+
let style = style ?? [:]
|
|
27
|
+
self.tabular = tabular
|
|
28
|
+
self.preservesBreakMode = preservesBreakMode
|
|
29
|
+
self.lines = min(3, max(1, style.int("lines", default: lines)))
|
|
30
|
+
explicitTruncation = style["lines"] != nil || style["truncate"] != nil
|
|
31
|
+
text =
|
|
32
|
+
self.lines == 1 && explicitTruncation
|
|
33
|
+
? value.replacingOccurrences(of: "\r\n|[\r\n]", with: " ", options: .regularExpression)
|
|
34
|
+
: value
|
|
35
|
+
let resolvedWeight: NativeListFontWeight
|
|
36
|
+
switch style.string("fontWeight") {
|
|
37
|
+
case "regular": resolvedWeight = .regular
|
|
38
|
+
case "medium": resolvedWeight = .medium
|
|
39
|
+
case "semibold": resolvedWeight = .semibold
|
|
40
|
+
case "bold": resolvedWeight = .bold
|
|
41
|
+
default: resolvedWeight = weight
|
|
42
|
+
}
|
|
43
|
+
let resolvedSize = CGFloat(style.double("fontSize", default: Double(size)))
|
|
44
|
+
font =
|
|
45
|
+
tabular
|
|
46
|
+
? nativeListTabularFont(ofSize: resolvedSize, weight: resolvedWeight)
|
|
47
|
+
: nativeListFont(ofSize: resolvedSize, weight: resolvedWeight)
|
|
48
|
+
self.color =
|
|
49
|
+
(style["color"] as? String).map { UIColor(nativeListHex: $0, fallback: color) } ?? color
|
|
50
|
+
self.lineHeight = CGFloat(style.double("lineHeight", default: Double(lineHeight)))
|
|
51
|
+
explicitLineHeight = style["lineHeight"] != nil
|
|
52
|
+
alignmentName = style["alignment"] as? String
|
|
53
|
+
self.breakMode =
|
|
54
|
+
explicitTruncation
|
|
55
|
+
? (style.string("truncate", default: "tail") == "clip"
|
|
56
|
+
? (self.lines == 1 ? .byClipping : .byWordWrapping) : .byTruncatingTail) : breakMode
|
|
57
|
+
verticalAlignment = style["verticalAlignment"] as? String
|
|
58
|
+
offsetY = CGFloat(style.double("offsetY"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Start/end follow the host view's layout direction, as legacy `marketTextAlignment` did.
|
|
62
|
+
func alignment(for direction: UIUserInterfaceLayoutDirection) -> NSTextAlignment {
|
|
63
|
+
guard let alignmentName else { return .natural }
|
|
64
|
+
return NativeListTextStyles.marketTextAlignment(alignmentName, direction: direction)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func attributed(direction: UIUserInterfaceLayoutDirection) -> NSAttributedString {
|
|
68
|
+
let paragraph = NSMutableParagraphStyle()
|
|
69
|
+
paragraph.minimumLineHeight = lineHeight
|
|
70
|
+
paragraph.maximumLineHeight = lineHeight
|
|
71
|
+
paragraph.alignment = alignment(for: direction)
|
|
72
|
+
if explicitTruncation || preservesBreakMode { paragraph.lineBreakMode = breakMode }
|
|
73
|
+
var attributes: [NSAttributedString.Key: Any] = [
|
|
74
|
+
.font: font, .foregroundColor: color, .paragraphStyle: paragraph,
|
|
75
|
+
]
|
|
76
|
+
if explicitLineHeight {
|
|
77
|
+
attributes[.baselineOffset] = max(0, (lineHeight - font.lineHeight) / 2)
|
|
78
|
+
}
|
|
79
|
+
return NSAttributedString(string: text, attributes: attributes)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Resolves start/end once from the label's current direction; the list rebinds bound rows
|
|
83
|
+
/// when its effective layout direction changes (NativeListRendererCell bound inputs).
|
|
84
|
+
func bind(_ label: NativeListTextLabel) {
|
|
85
|
+
let direction = label.effectiveUserInterfaceLayoutDirection
|
|
86
|
+
label.font = font
|
|
87
|
+
label.textColor = color
|
|
88
|
+
label.numberOfLines = lines
|
|
89
|
+
label.textAlignment = alignment(for: direction)
|
|
90
|
+
label.lineBreakMode = breakMode
|
|
91
|
+
label.rowVerticalAlignment = verticalAlignment
|
|
92
|
+
label.rowOffsetY = offsetY
|
|
93
|
+
label.attributedText = attributed(direction: direction)
|
|
94
|
+
label.isHidden = text.isEmpty
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Measurement runs for every row on every snapshot. It reuses one sizing label (the same
|
|
98
|
+
// UILabel metrics, line cap and truncation as the bound view) and memoizes by the inputs
|
|
99
|
+
// that affect height; alignment and color do not.
|
|
100
|
+
private static let sizingLabel = NativeListTextLabel()
|
|
101
|
+
private static let heightCache: NSCache<NSString, NSNumber> = {
|
|
102
|
+
let cache = NSCache<NSString, NSNumber>()
|
|
103
|
+
cache.countLimit = 4096
|
|
104
|
+
return cache
|
|
105
|
+
}()
|
|
106
|
+
|
|
107
|
+
func measure(width: CGFloat) -> CGFloat {
|
|
108
|
+
guard !text.isEmpty else { return 0 }
|
|
109
|
+
let width = max(1, width)
|
|
110
|
+
let key =
|
|
111
|
+
"\(width)|\(lines)|\(lineHeight)|\(explicitLineHeight)|\(explicitTruncation)|\(breakMode.rawValue)|\(preservesBreakMode)|\(font.fontName)|\(font.pointSize)|\(tabular)|\(text)"
|
|
112
|
+
as NSString
|
|
113
|
+
if let cached = Self.heightCache.object(forKey: key) { return CGFloat(cached.doubleValue) }
|
|
114
|
+
let label = Thread.isMainThread ? Self.sizingLabel : NativeListTextLabel()
|
|
115
|
+
bind(label)
|
|
116
|
+
let height = ceil(
|
|
117
|
+
label.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height)
|
|
118
|
+
if label === Self.sizingLabel { label.attributedText = nil }
|
|
119
|
+
Self.heightCache.setObject(NSNumber(value: Double(height)), forKey: key)
|
|
120
|
+
return height
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
final class NativeListActionOrigin {
|
|
4
|
+
weak var sourceView: UIView?
|
|
5
|
+
weak var ownerCell: NativeListRowHost?
|
|
6
|
+
let bindingEpoch: Int
|
|
7
|
+
let source: String
|
|
8
|
+
let slot: Int?
|
|
9
|
+
// OneKey patch: expose the layout slot while preserving the larger hit target.
|
|
10
|
+
let anchorInset: CGFloat
|
|
11
|
+
var windowPoint: CGPoint?
|
|
12
|
+
|
|
13
|
+
init(
|
|
14
|
+
sourceView: UIView,
|
|
15
|
+
ownerCell: NativeListRowHost,
|
|
16
|
+
bindingEpoch: Int,
|
|
17
|
+
source: String,
|
|
18
|
+
slot: Int? = nil,
|
|
19
|
+
anchorInset: CGFloat = 0
|
|
20
|
+
) {
|
|
21
|
+
self.sourceView = sourceView
|
|
22
|
+
self.ownerCell = ownerCell
|
|
23
|
+
self.bindingEpoch = bindingEpoch
|
|
24
|
+
self.source = source
|
|
25
|
+
self.slot = slot
|
|
26
|
+
self.anchorInset = anchorInset
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Common list-facing lifecycle; each renderer owns its view allocation and reset.
|
|
31
|
+
class NativeListRowHost: UICollectionViewCell {
|
|
32
|
+
class func measure(_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String)
|
|
33
|
+
-> CGFloat?
|
|
34
|
+
{ nil }
|
|
35
|
+
class func appliesSizePreset(_ item: NativeListItem) -> Bool { true }
|
|
36
|
+
|
|
37
|
+
var bindingEpoch = 0
|
|
38
|
+
var listStyle: [String: Any]?
|
|
39
|
+
var onAction: ((NativeListItem, String, NativeSelectionTarget?, NativeListActionOrigin?) -> Void)?
|
|
40
|
+
var onBindingInvalidated: ((NativeListRowHost, Int) -> Void)?
|
|
41
|
+
|
|
42
|
+
func bind(
|
|
43
|
+
item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int? = nil,
|
|
44
|
+
selected: Bool, checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
45
|
+
) {
|
|
46
|
+
preconditionFailure("A row host must implement binding")
|
|
47
|
+
}
|
|
48
|
+
func updateSelection(
|
|
49
|
+
item: NativeListItem, selected: Bool,
|
|
50
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
51
|
+
) {}
|
|
52
|
+
func updateMarketQuote(_ item: NativeListItem, theme: [String: Any]?) {}
|
|
53
|
+
func setPressed(_ pressed: Bool) { isHighlighted = pressed && isUserInteractionEnabled }
|
|
54
|
+
func canStartWalletGroupReorder(at point: CGPoint) -> Bool { true }
|
|
55
|
+
func setWalletGroupReorderCompact(_ compact: Bool) {}
|
|
56
|
+
func prepareWalletGroupReorderExpansion() {}
|
|
57
|
+
func animateWalletGroupReorderExpansion() {}
|
|
58
|
+
func finishWalletGroupReorderExpansion() {}
|
|
59
|
+
func rowActionOrigin() -> NativeListActionOrigin {
|
|
60
|
+
NativeListActionOrigin(
|
|
61
|
+
sourceView: contentView, ownerCell: self, bindingEpoch: bindingEpoch, source: "row")
|
|
62
|
+
}
|
|
63
|
+
}
|