@onekeyfe/react-native-image-crop-picker 3.0.147 → 3.0.148
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 +48 -5
- package/ReactNativeImageCropPicker.podspec +5 -3
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerImageProcessor.kt +8 -18
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerSession.kt +21 -46
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperActivity.kt +636 -0
- package/android/src/main/java/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperTheme.kt +188 -0
- package/android/src/main/res/drawable/image_crop_picker_rotate.xml +11 -0
- package/ios/ImageCropPickerImageProcessor.swift +8 -4
- package/ios/ImageCropPickerSession.swift +43 -46
- package/ios/ImageCropperTheme.swift +155 -0
- package/ios/ImageCropperViewController.swift +462 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.h +9 -0
- package/ios/TOCropViewController/Views/TOCropOverlayView.m +47 -3
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts +17 -11
- package/lib/typescript/src/ReactNativeImageCropPicker.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridReactNativeImageCropPickerSpec.cpp +8 -0
- package/nitrogen/generated/android/c++/JImageCropPickerOptions.hpp +9 -45
- package/nitrogen/generated/android/c++/JImageCropperAppearance.hpp +108 -0
- package/nitrogen/generated/android/c++/JImageCropperColorScheme.hpp +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropPickerOptions.kt +5 -55
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperAppearance.kt +111 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativeimagecroppicker/ImageCropperColorScheme.kt +23 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Bridge.hpp +36 -0
- package/nitrogen/generated/ios/ReactNativeImageCropPicker-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridReactNativeImageCropPickerSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/ImageCropPickerOptions.swift +6 -193
- package/nitrogen/generated/ios/swift/ImageCropperAppearance.swift +251 -0
- package/nitrogen/generated/ios/swift/ImageCropperColorScheme.swift +40 -0
- package/nitrogen/generated/shared/c++/ImageCropPickerOptions.hpp +8 -46
- package/nitrogen/generated/shared/c++/ImageCropperAppearance.hpp +134 -0
- package/nitrogen/generated/shared/c++/ImageCropperColorScheme.hpp +76 -0
- package/package.json +1 -1
- package/src/ReactNativeImageCropPicker.nitro.ts +29 -13
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
protocol ImageCropperViewControllerDelegate: AnyObject {
|
|
4
|
+
// `cropFrame` is in the coordinate space of `image`, rotated by `angle`.
|
|
5
|
+
func imageCropperViewController(
|
|
6
|
+
_ controller: ImageCropperViewController,
|
|
7
|
+
didCropWithFrame cropFrame: CGRect,
|
|
8
|
+
angle: Int
|
|
9
|
+
)
|
|
10
|
+
func imageCropperViewControllerDidCancel(_ controller: ImageCropperViewController)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Full-screen cropper laid out like a OneKey page: a header with the title and
|
|
14
|
+
// a rotate button, the crop area, and a Cancel / Confirm footer. Android's
|
|
15
|
+
// ImageCropperActivity draws the same screen with the same metrics.
|
|
16
|
+
final class ImageCropperViewController: UIViewController {
|
|
17
|
+
// The area outside the crop box shows the page background at this opacity.
|
|
18
|
+
static let dimmedAlpha: CGFloat = 0.7
|
|
19
|
+
|
|
20
|
+
weak var delegate: ImageCropperViewControllerDelegate?
|
|
21
|
+
let image: UIImage
|
|
22
|
+
|
|
23
|
+
private let config: ImageCropPickerConfig
|
|
24
|
+
private let theme: ImageCropperTheme
|
|
25
|
+
private let cropView: TOCropView
|
|
26
|
+
private let titleLabel = UILabel()
|
|
27
|
+
private let rotateButton: ImageCropperIconButton
|
|
28
|
+
private let cancelButton: ImageCropperButton
|
|
29
|
+
private let confirmButton: ImageCropperButton
|
|
30
|
+
private var didPerformInitialSetup = false
|
|
31
|
+
private var isRotating = false
|
|
32
|
+
|
|
33
|
+
// Set while the session crops and saves the image.
|
|
34
|
+
var isProcessing = false {
|
|
35
|
+
didSet { updateControls() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init(image: UIImage, config: ImageCropPickerConfig, theme: ImageCropperTheme) {
|
|
39
|
+
self.image = image
|
|
40
|
+
self.config = config
|
|
41
|
+
self.theme = theme
|
|
42
|
+
let cropView = TOCropView(
|
|
43
|
+
croppingStyle: config.cropperCircleOverlay ? .circular : .default,
|
|
44
|
+
image: image
|
|
45
|
+
)
|
|
46
|
+
self.cropView = cropView
|
|
47
|
+
|
|
48
|
+
// TOCropViewController's own translations, for callers that pass no text.
|
|
49
|
+
let strings = TO_CROP_VIEW_RESOURCE_BUNDLE_FOR_OBJECT(cropView) ?? .main
|
|
50
|
+
func localized(_ key: String) -> String {
|
|
51
|
+
return strings.localizedString(forKey: key, value: key, table: "TOCropViewControllerLocalizable")
|
|
52
|
+
}
|
|
53
|
+
// Large Button: 12pt + 1pt border vertically, 20pt + 1pt horizontally.
|
|
54
|
+
let buttonPadding = theme.metric(20) + 1
|
|
55
|
+
let spinnerSpacing = theme.metric(8)
|
|
56
|
+
cancelButton = ImageCropperButton(
|
|
57
|
+
title: Self.nonEmpty(config.cropperCancelText) ?? localized("Cancel"),
|
|
58
|
+
horizontalPadding: buttonPadding,
|
|
59
|
+
spinnerSpacing: spinnerSpacing,
|
|
60
|
+
font: theme.buttonFont,
|
|
61
|
+
textColor: theme.cancelButtonTextColor,
|
|
62
|
+
backgroundColor: theme.cancelButtonColor,
|
|
63
|
+
pressedBackgroundColor: theme.cancelButtonPressedColor
|
|
64
|
+
)
|
|
65
|
+
confirmButton = ImageCropperButton(
|
|
66
|
+
title: Self.nonEmpty(config.cropperChooseText) ?? localized("Done"),
|
|
67
|
+
horizontalPadding: buttonPadding,
|
|
68
|
+
spinnerSpacing: spinnerSpacing,
|
|
69
|
+
font: theme.buttonFont,
|
|
70
|
+
textColor: theme.confirmButtonTextColor,
|
|
71
|
+
backgroundColor: theme.confirmButtonColor,
|
|
72
|
+
pressedBackgroundColor: theme.confirmButtonPressedColor
|
|
73
|
+
)
|
|
74
|
+
rotateButton = ImageCropperIconButton(
|
|
75
|
+
image: ImageCropperIcons.rotateCounterclockwise(size: theme.metric(24)),
|
|
76
|
+
tintColor: theme.iconColor,
|
|
77
|
+
pressedBackgroundColor: theme.cancelButtonColor
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
super.init(nibName: nil, bundle: nil)
|
|
81
|
+
modalPresentationStyle = .fullScreen
|
|
82
|
+
modalTransitionStyle = .coverVertical
|
|
83
|
+
overrideUserInterfaceStyle = theme.isDark ? .dark : .light
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@available(*, unavailable)
|
|
87
|
+
required init?(coder: NSCoder) {
|
|
88
|
+
fatalError("init(coder:) has not been implemented")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
92
|
+
return theme.isDark ? .lightContent : .darkContent
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override func viewDidLoad() {
|
|
96
|
+
super.viewDidLoad()
|
|
97
|
+
view.backgroundColor = theme.backgroundColor
|
|
98
|
+
|
|
99
|
+
configureCropView()
|
|
100
|
+
view.addSubview(cropView)
|
|
101
|
+
|
|
102
|
+
titleLabel.text = config.cropperToolbarTitle
|
|
103
|
+
titleLabel.font = theme.titleFont
|
|
104
|
+
titleLabel.textColor = theme.titleColor
|
|
105
|
+
titleLabel.textAlignment = .center
|
|
106
|
+
titleLabel.lineBreakMode = .byTruncatingTail
|
|
107
|
+
titleLabel.accessibilityTraits = .header
|
|
108
|
+
view.addSubview(titleLabel)
|
|
109
|
+
|
|
110
|
+
rotateButton.isHidden = config.cropperRotateButtonsHidden
|
|
111
|
+
rotateButton.accessibilityLabel = "Rotate"
|
|
112
|
+
rotateButton.addTarget(self, action: #selector(rotateTapped), for: .touchUpInside)
|
|
113
|
+
view.addSubview(rotateButton)
|
|
114
|
+
|
|
115
|
+
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
|
|
116
|
+
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
|
|
117
|
+
view.addSubview(cancelButton)
|
|
118
|
+
view.addSubview(confirmButton)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func configureCropView() {
|
|
122
|
+
cropView.backgroundColor = theme.backgroundColor
|
|
123
|
+
cropView.overlayView.backgroundColor = theme.backgroundColor.withAlphaComponent(Self.dimmedAlpha)
|
|
124
|
+
// The built-in dark blur ignores the theme, so rely on the dimmed overlay alone.
|
|
125
|
+
cropView.translucencyAlwaysHidden = true
|
|
126
|
+
cropView.cropViewPadding = theme.metric(20)
|
|
127
|
+
|
|
128
|
+
let isCircular = config.cropperCircleOverlay
|
|
129
|
+
let isFreeStyle = config.freeStyleCropEnabled && !isCircular
|
|
130
|
+
if !isCircular, let targetSize = config.targetSize {
|
|
131
|
+
// Applied by performInitialSetup, which sizes the first crop box from it.
|
|
132
|
+
cropView.aspectRatio = targetSize
|
|
133
|
+
}
|
|
134
|
+
cropView.aspectRatioLockEnabled = !isFreeStyle
|
|
135
|
+
cropView.resetAspectRatioEnabled = isFreeStyle
|
|
136
|
+
cropView.cropBoxResizeEnabled = isFreeStyle
|
|
137
|
+
|
|
138
|
+
if let overlay = cropView.gridOverlayView {
|
|
139
|
+
overlay.frameColor = theme.titleColor
|
|
140
|
+
overlay.gridColor = UIColor.white.withAlphaComponent(0.8)
|
|
141
|
+
overlay.cornerHandlesHidden = !isFreeStyle
|
|
142
|
+
if !config.showCropGuidelines {
|
|
143
|
+
overlay.displayHorizontalGridLines = false
|
|
144
|
+
overlay.displayVerticalGridLines = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override func viewDidLayoutSubviews() {
|
|
150
|
+
super.viewDidLayoutSubviews()
|
|
151
|
+
let cropFrame = layoutChrome(in: view.bounds.size)
|
|
152
|
+
guard cropFrame.width > 0, cropFrame.height > 0 else {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
cropView.frame = cropFrame
|
|
156
|
+
cropView.moveCroppedContentToCenter(animated: false)
|
|
157
|
+
if !didPerformInitialSetup {
|
|
158
|
+
didPerformInitialSetup = true
|
|
159
|
+
cropView.performInitialSetup()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override func viewWillTransition(
|
|
164
|
+
to size: CGSize,
|
|
165
|
+
with coordinator: UIViewControllerTransitionCoordinator
|
|
166
|
+
) {
|
|
167
|
+
super.viewWillTransition(to: size, with: coordinator)
|
|
168
|
+
guard didPerformInitialSetup, size != view.bounds.size else {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
// Same sequence as TOCropViewController, so the crop box keeps its content.
|
|
172
|
+
cropView.prepareforRotation()
|
|
173
|
+
cropView.simpleRenderMode = true
|
|
174
|
+
cropView.internalLayoutDisabled = true
|
|
175
|
+
coordinator.animate(alongsideTransition: { [self] _ in
|
|
176
|
+
cropView.frame = layoutChrome(in: size)
|
|
177
|
+
cropView.performRelayoutForRotation()
|
|
178
|
+
}, completion: { [self] _ in
|
|
179
|
+
cropView.setSimpleRenderMode(false, animated: true)
|
|
180
|
+
cropView.internalLayoutDisabled = false
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Lays out the header and footer, and returns the frame left for the crop view.
|
|
185
|
+
@discardableResult
|
|
186
|
+
private func layoutChrome(in size: CGSize) -> CGRect {
|
|
187
|
+
let insets = view.safeAreaInsets
|
|
188
|
+
let spacing = theme.metric(20)
|
|
189
|
+
let headerHeight = theme.metric(56)
|
|
190
|
+
let iconButtonSize = theme.metric(40)
|
|
191
|
+
let buttonHeight = theme.metric(50)
|
|
192
|
+
let buttonGap = theme.metric(10)
|
|
193
|
+
// Same as OneKey's page footer: 20pt of padding, plus the home indicator
|
|
194
|
+
// inset less 10pt.
|
|
195
|
+
let bottomInset = insets.bottom > 10 ? insets.bottom - 10 : insets.bottom
|
|
196
|
+
|
|
197
|
+
let contentMinX = insets.left
|
|
198
|
+
let contentWidth = max(size.width - insets.left - insets.right, 0)
|
|
199
|
+
|
|
200
|
+
let headerMidY = insets.top + headerHeight / 2
|
|
201
|
+
rotateButton.frame = CGRect(
|
|
202
|
+
x: contentMinX + contentWidth - spacing + theme.metric(8) - iconButtonSize,
|
|
203
|
+
y: headerMidY - iconButtonSize / 2,
|
|
204
|
+
width: iconButtonSize,
|
|
205
|
+
height: iconButtonSize
|
|
206
|
+
)
|
|
207
|
+
// Centered, clear of the rotate button on both sides.
|
|
208
|
+
let titleInset = spacing + iconButtonSize
|
|
209
|
+
titleLabel.frame = CGRect(
|
|
210
|
+
x: contentMinX + titleInset,
|
|
211
|
+
y: insets.top,
|
|
212
|
+
width: max(contentWidth - titleInset * 2, 0),
|
|
213
|
+
height: headerHeight
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
let buttonY = size.height - bottomInset - spacing - buttonHeight
|
|
217
|
+
let buttonWidth = max((contentWidth - spacing * 2 - buttonGap) / 2, 0)
|
|
218
|
+
cancelButton.frame = CGRect(
|
|
219
|
+
x: contentMinX + spacing,
|
|
220
|
+
y: buttonY,
|
|
221
|
+
width: buttonWidth,
|
|
222
|
+
height: buttonHeight
|
|
223
|
+
)
|
|
224
|
+
confirmButton.frame = CGRect(
|
|
225
|
+
x: cancelButton.frame.maxX + buttonGap,
|
|
226
|
+
y: buttonY,
|
|
227
|
+
width: buttonWidth,
|
|
228
|
+
height: buttonHeight
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// The crop view pads the crop box by `spacing` on every side.
|
|
232
|
+
let cropMinY = insets.top + headerHeight
|
|
233
|
+
return CGRect(
|
|
234
|
+
x: contentMinX,
|
|
235
|
+
y: cropMinY,
|
|
236
|
+
width: contentWidth,
|
|
237
|
+
height: max(buttonY - cropMinY, 0)
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private func updateControls() {
|
|
242
|
+
cropView.isUserInteractionEnabled = !isProcessing
|
|
243
|
+
rotateButton.isEnabled = !isProcessing
|
|
244
|
+
confirmButton.isLoading = isProcessing
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@objc private func rotateTapped() {
|
|
248
|
+
guard !isRotating, !isProcessing else {
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
isRotating = true
|
|
252
|
+
cropView.rotateImageNinetyDegrees(animated: true, clockwise: false) { [weak self] _ in
|
|
253
|
+
self?.isRotating = false
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@objc private func cancelTapped() {
|
|
258
|
+
delegate?.imageCropperViewControllerDidCancel(self)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@objc private func confirmTapped() {
|
|
262
|
+
guard !isProcessing, !isRotating else {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
delegate?.imageCropperViewController(
|
|
266
|
+
self,
|
|
267
|
+
didCropWithFrame: cropView.imageCropFrame,
|
|
268
|
+
angle: cropView.angle
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private static func nonEmpty(_ value: String?) -> String? {
|
|
273
|
+
guard let value, !value.isEmpty else {
|
|
274
|
+
return nil
|
|
275
|
+
}
|
|
276
|
+
return value
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// OneKey's large Button: a capsule with a 16pt medium label, a pressed color,
|
|
281
|
+
// and a spinner next to the label while loading.
|
|
282
|
+
final class ImageCropperButton: UIControl {
|
|
283
|
+
private let label = UILabel()
|
|
284
|
+
private let spinner = UIActivityIndicatorView(style: .medium)
|
|
285
|
+
private let horizontalPadding: CGFloat
|
|
286
|
+
private let spinnerSpacing: CGFloat
|
|
287
|
+
private let normalBackgroundColor: UIColor
|
|
288
|
+
private let pressedBackgroundColor: UIColor
|
|
289
|
+
|
|
290
|
+
var isLoading = false {
|
|
291
|
+
didSet {
|
|
292
|
+
guard isLoading != oldValue else {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if isLoading {
|
|
296
|
+
spinner.startAnimating()
|
|
297
|
+
} else {
|
|
298
|
+
spinner.stopAnimating()
|
|
299
|
+
}
|
|
300
|
+
updateAppearance()
|
|
301
|
+
setNeedsLayout()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override var isHighlighted: Bool {
|
|
306
|
+
didSet { updateAppearance() }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override var isEnabled: Bool {
|
|
310
|
+
didSet { updateAppearance() }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
init(
|
|
314
|
+
title: String,
|
|
315
|
+
horizontalPadding: CGFloat,
|
|
316
|
+
spinnerSpacing: CGFloat,
|
|
317
|
+
font: UIFont,
|
|
318
|
+
textColor: UIColor,
|
|
319
|
+
backgroundColor: UIColor,
|
|
320
|
+
pressedBackgroundColor: UIColor
|
|
321
|
+
) {
|
|
322
|
+
self.horizontalPadding = horizontalPadding
|
|
323
|
+
self.spinnerSpacing = spinnerSpacing
|
|
324
|
+
normalBackgroundColor = backgroundColor
|
|
325
|
+
self.pressedBackgroundColor = pressedBackgroundColor
|
|
326
|
+
super.init(frame: .zero)
|
|
327
|
+
|
|
328
|
+
layer.cornerCurve = .continuous
|
|
329
|
+
label.text = title
|
|
330
|
+
label.font = font
|
|
331
|
+
label.textColor = textColor
|
|
332
|
+
label.textAlignment = .center
|
|
333
|
+
label.lineBreakMode = .byTruncatingTail
|
|
334
|
+
label.isUserInteractionEnabled = false
|
|
335
|
+
addSubview(label)
|
|
336
|
+
|
|
337
|
+
spinner.color = textColor
|
|
338
|
+
spinner.hidesWhenStopped = true
|
|
339
|
+
spinner.isUserInteractionEnabled = false
|
|
340
|
+
addSubview(spinner)
|
|
341
|
+
|
|
342
|
+
isAccessibilityElement = true
|
|
343
|
+
accessibilityTraits = .button
|
|
344
|
+
accessibilityLabel = title
|
|
345
|
+
updateAppearance()
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@available(*, unavailable)
|
|
349
|
+
required init?(coder: NSCoder) {
|
|
350
|
+
fatalError("init(coder:) has not been implemented")
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
override func layoutSubviews() {
|
|
354
|
+
super.layoutSubviews()
|
|
355
|
+
layer.cornerRadius = bounds.height / 2
|
|
356
|
+
|
|
357
|
+
let spinnerSize = spinner.intrinsicContentSize
|
|
358
|
+
let leadingWidth = isLoading ? spinnerSize.width + spinnerSpacing : 0
|
|
359
|
+
let maxLabelWidth = max(bounds.width - horizontalPadding * 2 - leadingWidth, 0)
|
|
360
|
+
let labelWidth = min(ceil(label.intrinsicContentSize.width), maxLabelWidth)
|
|
361
|
+
let startX = (bounds.width - labelWidth - leadingWidth) / 2
|
|
362
|
+
|
|
363
|
+
spinner.frame = CGRect(
|
|
364
|
+
x: startX,
|
|
365
|
+
y: (bounds.height - spinnerSize.height) / 2,
|
|
366
|
+
width: spinnerSize.width,
|
|
367
|
+
height: spinnerSize.height
|
|
368
|
+
)
|
|
369
|
+
label.frame = CGRect(
|
|
370
|
+
x: startX + leadingWidth,
|
|
371
|
+
y: 0,
|
|
372
|
+
width: labelWidth,
|
|
373
|
+
height: bounds.height
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private func updateAppearance() {
|
|
378
|
+
backgroundColor = isHighlighted ? pressedBackgroundColor : normalBackgroundColor
|
|
379
|
+
// OneKey dims disabled and loading buttons alike.
|
|
380
|
+
alpha = isEnabled && !isLoading ? 1 : 0.4
|
|
381
|
+
accessibilityTraits = isEnabled ? .button : [.button, .notEnabled]
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// OneKey's header icon button: a 24pt icon with a round pressed background.
|
|
386
|
+
final class ImageCropperIconButton: UIControl {
|
|
387
|
+
private let imageView = UIImageView()
|
|
388
|
+
private let pressedBackgroundColor: UIColor
|
|
389
|
+
|
|
390
|
+
override var isHighlighted: Bool {
|
|
391
|
+
didSet { updateAppearance() }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
override var isEnabled: Bool {
|
|
395
|
+
didSet { updateAppearance() }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
init(image: UIImage, tintColor: UIColor, pressedBackgroundColor: UIColor) {
|
|
399
|
+
self.pressedBackgroundColor = pressedBackgroundColor
|
|
400
|
+
super.init(frame: .zero)
|
|
401
|
+
imageView.image = image
|
|
402
|
+
imageView.tintColor = tintColor
|
|
403
|
+
imageView.contentMode = .center
|
|
404
|
+
imageView.isUserInteractionEnabled = false
|
|
405
|
+
addSubview(imageView)
|
|
406
|
+
isAccessibilityElement = true
|
|
407
|
+
accessibilityTraits = .button
|
|
408
|
+
updateAppearance()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@available(*, unavailable)
|
|
412
|
+
required init?(coder: NSCoder) {
|
|
413
|
+
fatalError("init(coder:) has not been implemented")
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
override func layoutSubviews() {
|
|
417
|
+
super.layoutSubviews()
|
|
418
|
+
layer.cornerRadius = bounds.height / 2
|
|
419
|
+
imageView.frame = bounds
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private func updateAppearance() {
|
|
423
|
+
backgroundColor = isHighlighted ? pressedBackgroundColor : .clear
|
|
424
|
+
imageView.alpha = isEnabled ? 1 : 0.4
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
enum ImageCropperIcons {
|
|
429
|
+
// OneKey's RotateCounterclockwise icon (24x24 viewBox). Android uses the
|
|
430
|
+
// same path in res/drawable/image_crop_picker_rotate.xml.
|
|
431
|
+
static func rotateCounterclockwise(size: CGFloat) -> UIImage {
|
|
432
|
+
let path = UIBezierPath()
|
|
433
|
+
path.move(to: CGPoint(x: 6, y: 5.426))
|
|
434
|
+
path.addCurve(to: CGPoint(x: 12.028, y: 3), controlPoint1: CGPoint(x: 7.628, y: 3.919), controlPoint2: CGPoint(x: 9.484, y: 3))
|
|
435
|
+
path.addCurve(to: CGPoint(x: 20.969, y: 10.984), controlPoint1: CGPoint(x: 16.605, y: 3.001), controlPoint2: CGPoint(x: 20.452, y: 6.436))
|
|
436
|
+
path.addCurve(to: CGPoint(x: 14.047, y: 20.77), controlPoint1: CGPoint(x: 21.485, y: 15.531), controlPoint2: CGPoint(x: 18.507, y: 19.742))
|
|
437
|
+
path.addCurve(to: CGPoint(x: 3.541, y: 15), controlPoint1: CGPoint(x: 9.588, y: 21.798), controlPoint2: CGPoint(x: 5.067, y: 19.315))
|
|
438
|
+
path.addLine(to: CGPoint(x: 3.207, y: 14.057))
|
|
439
|
+
path.addLine(to: CGPoint(x: 5.093, y: 13.391))
|
|
440
|
+
path.addLine(to: CGPoint(x: 5.426, y: 14.333))
|
|
441
|
+
path.addCurve(to: CGPoint(x: 13.597, y: 18.821), controlPoint1: CGPoint(x: 6.612, y: 17.689), controlPoint2: CGPoint(x: 10.128, y: 19.62))
|
|
442
|
+
path.addCurve(to: CGPoint(x: 18.981, y: 11.211), controlPoint1: CGPoint(x: 17.066, y: 18.023), controlPoint2: CGPoint(x: 19.382, y: 14.748))
|
|
443
|
+
path.addCurve(to: CGPoint(x: 12.029, y: 5), controlPoint1: CGPoint(x: 18.58, y: 7.674), controlPoint2: CGPoint(x: 15.588, y: 5.002))
|
|
444
|
+
path.addCurve(to: CGPoint(x: 7.244, y: 7), controlPoint1: CGPoint(x: 10.047, y: 5), controlPoint2: CGPoint(x: 8.622, y: 5.686))
|
|
445
|
+
path.addLine(to: CGPoint(x: 10, y: 7))
|
|
446
|
+
path.addLine(to: CGPoint(x: 10, y: 9))
|
|
447
|
+
path.addLine(to: CGPoint(x: 4, y: 9))
|
|
448
|
+
path.addLine(to: CGPoint(x: 4, y: 3))
|
|
449
|
+
path.addLine(to: CGPoint(x: 6, y: 3))
|
|
450
|
+
path.close()
|
|
451
|
+
path.apply(CGAffineTransform(scaleX: size / 24, y: size / 24))
|
|
452
|
+
|
|
453
|
+
let format = UIGraphicsImageRendererFormat.preferred()
|
|
454
|
+
format.opaque = false
|
|
455
|
+
let image = UIGraphicsImageRenderer(size: CGSize(width: size, height: size), format: format)
|
|
456
|
+
.image { _ in
|
|
457
|
+
UIColor.black.setFill()
|
|
458
|
+
path.fill()
|
|
459
|
+
}
|
|
460
|
+
return image.withRenderingMode(.alwaysTemplate)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -38,6 +38,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
38
38
|
/** Shows and hides the interior grid lines with an optional crossfade animation. */
|
|
39
39
|
- (void)setGridHidden:(BOOL)hidden animated:(BOOL)animated;
|
|
40
40
|
|
|
41
|
+
/** OneKey: color of the crop box border and corner handles. Default is white. */
|
|
42
|
+
@property (nonatomic, strong) UIColor *frameColor;
|
|
43
|
+
|
|
44
|
+
/** OneKey: color of the interior grid lines. Default is white. */
|
|
45
|
+
@property (nonatomic, strong) UIColor *gridColor;
|
|
46
|
+
|
|
47
|
+
/** OneKey: hides the corner handles, for crop boxes that cannot be resized. */
|
|
48
|
+
@property (nonatomic, assign) BOOL cornerHandlesHidden;
|
|
49
|
+
|
|
41
50
|
@end
|
|
42
51
|
|
|
43
52
|
NS_ASSUME_NONNULL_END
|
|
@@ -50,6 +50,9 @@ static const CGFloat kTOCropOverLayerCornerWidth = 20.0f;
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
- (void)setup {
|
|
53
|
+
_frameColor = [UIColor whiteColor];
|
|
54
|
+
_gridColor = [UIColor whiteColor];
|
|
55
|
+
|
|
53
56
|
UIView * (^newLineView)(void) = ^UIView *(void) {
|
|
54
57
|
return [self createNewLineView];
|
|
55
58
|
};
|
|
@@ -188,6 +191,41 @@ static const CGFloat kTOCropOverLayerCornerWidth = 20.0f;
|
|
|
188
191
|
|
|
189
192
|
#pragma mark - Property methods
|
|
190
193
|
|
|
194
|
+
- (NSArray<UIView *> *)cornerHandleViews {
|
|
195
|
+
NSMutableArray<UIView *> *views = [NSMutableArray array];
|
|
196
|
+
for (NSArray *lines in @[self.topLeftLineViews, self.topRightLineViews, self.bottomRightLineViews, self.bottomLeftLineViews]) {
|
|
197
|
+
[views addObjectsFromArray:lines];
|
|
198
|
+
}
|
|
199
|
+
return views;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
- (void)setFrameColor:(UIColor *)frameColor {
|
|
203
|
+
_frameColor = frameColor ?: [UIColor whiteColor];
|
|
204
|
+
for (UIView *lineView in self.outerLineViews) {
|
|
205
|
+
lineView.backgroundColor = _frameColor;
|
|
206
|
+
}
|
|
207
|
+
for (UIView *lineView in [self cornerHandleViews]) {
|
|
208
|
+
lineView.backgroundColor = _frameColor;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
- (void)setGridColor:(UIColor *)gridColor {
|
|
213
|
+
_gridColor = gridColor ?: [UIColor whiteColor];
|
|
214
|
+
for (UIView *lineView in self.horizontalGridLines) {
|
|
215
|
+
lineView.backgroundColor = _gridColor;
|
|
216
|
+
}
|
|
217
|
+
for (UIView *lineView in self.verticalGridLines) {
|
|
218
|
+
lineView.backgroundColor = _gridColor;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
- (void)setCornerHandlesHidden:(BOOL)cornerHandlesHidden {
|
|
223
|
+
_cornerHandlesHidden = cornerHandlesHidden;
|
|
224
|
+
for (UIView *lineView in [self cornerHandleViews]) {
|
|
225
|
+
lineView.hidden = cornerHandlesHidden;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
- (void)setDisplayHorizontalGridLines:(BOOL)displayHorizontalGridLines {
|
|
192
230
|
_displayHorizontalGridLines = displayHorizontalGridLines;
|
|
193
231
|
|
|
@@ -196,7 +234,7 @@ static const CGFloat kTOCropOverLayerCornerWidth = 20.0f;
|
|
|
196
234
|
}];
|
|
197
235
|
|
|
198
236
|
if (_displayHorizontalGridLines) {
|
|
199
|
-
self.horizontalGridLines = @[[self
|
|
237
|
+
self.horizontalGridLines = @[[self createGridLineView], [self createGridLineView]];
|
|
200
238
|
} else {
|
|
201
239
|
self.horizontalGridLines = @[];
|
|
202
240
|
}
|
|
@@ -214,7 +252,7 @@ static const CGFloat kTOCropOverLayerCornerWidth = 20.0f;
|
|
|
214
252
|
}];
|
|
215
253
|
|
|
216
254
|
if (_displayVerticalGridLines) {
|
|
217
|
-
self.verticalGridLines = @[[self
|
|
255
|
+
self.verticalGridLines = @[[self createGridLineView], [self createGridLineView]];
|
|
218
256
|
} else {
|
|
219
257
|
self.verticalGridLines = @[];
|
|
220
258
|
}
|
|
@@ -232,9 +270,15 @@ static const CGFloat kTOCropOverLayerCornerWidth = 20.0f;
|
|
|
232
270
|
|
|
233
271
|
- (nonnull UIView *)createNewLineView {
|
|
234
272
|
UIView *newLine = [[UIView alloc] initWithFrame:CGRectZero];
|
|
235
|
-
newLine.backgroundColor = [UIColor whiteColor];
|
|
273
|
+
newLine.backgroundColor = self.frameColor ?: [UIColor whiteColor];
|
|
236
274
|
[self addSubview:newLine];
|
|
237
275
|
return newLine;
|
|
238
276
|
}
|
|
239
277
|
|
|
278
|
+
- (nonnull UIView *)createGridLineView {
|
|
279
|
+
UIView *newLine = [self createNewLineView];
|
|
280
|
+
newLine.backgroundColor = self.gridColor ?: [UIColor whiteColor];
|
|
281
|
+
return newLine;
|
|
282
|
+
}
|
|
283
|
+
|
|
240
284
|
@end
|
|
@@ -15,6 +15,22 @@ export interface PickedImage {
|
|
|
15
15
|
cropRect?: CropRect;
|
|
16
16
|
filename?: string;
|
|
17
17
|
}
|
|
18
|
+
export type ImageCropperColorScheme = 'light' | 'dark';
|
|
19
|
+
export interface ImageCropperAppearance {
|
|
20
|
+
colorScheme?: ImageCropperColorScheme;
|
|
21
|
+
backgroundColor?: string;
|
|
22
|
+
titleColor?: string;
|
|
23
|
+
iconColor?: string;
|
|
24
|
+
cancelButtonColor?: string;
|
|
25
|
+
cancelButtonPressedColor?: string;
|
|
26
|
+
cancelButtonTextColor?: string;
|
|
27
|
+
confirmButtonColor?: string;
|
|
28
|
+
confirmButtonPressedColor?: string;
|
|
29
|
+
confirmButtonTextColor?: string;
|
|
30
|
+
titleFontFamily?: string;
|
|
31
|
+
buttonFontFamily?: string;
|
|
32
|
+
scale?: number;
|
|
33
|
+
}
|
|
18
34
|
export interface ImageCropPickerOptions {
|
|
19
35
|
width?: number;
|
|
20
36
|
height?: number;
|
|
@@ -28,19 +44,9 @@ export interface ImageCropPickerOptions {
|
|
|
28
44
|
cropperToolbarTitle?: string;
|
|
29
45
|
cropperChooseText?: string;
|
|
30
46
|
cropperCancelText?: string;
|
|
31
|
-
cropperChooseColor?: string;
|
|
32
|
-
cropperCancelColor?: string;
|
|
33
47
|
cropperRotateButtonsHidden?: boolean;
|
|
34
|
-
cropperActiveWidgetColor?: string;
|
|
35
|
-
cropperToolbarColor?: string;
|
|
36
|
-
cropperToolbarWidgetColor?: string;
|
|
37
|
-
cropperStatusBarLight?: boolean;
|
|
38
|
-
cropperNavigationBarLight?: boolean;
|
|
39
48
|
showCropGuidelines?: boolean;
|
|
40
|
-
|
|
41
|
-
enableRotationGesture?: boolean;
|
|
42
|
-
hideBottomControls?: boolean;
|
|
43
|
-
disableCropperColorSetters?: boolean;
|
|
49
|
+
cropperAppearance?: ImageCropperAppearance;
|
|
44
50
|
}
|
|
45
51
|
export interface ReactNativeImageCropPicker extends HybridObject<{
|
|
46
52
|
ios: 'swift';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativeImageCropPicker.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativeImageCropPicker.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAE1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IAEb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IAErC,
|
|
1
|
+
{"version":3,"file":"ReactNativeImageCropPicker.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativeImageCropPicker.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAE1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IAEb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,MAAM,CAAC;AAKvD,MAAM,WAAW,sBAAsB;IAErC,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IAErC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IAErC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;CAC5C;AAED,MAAM,WAAW,0BACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IAGzD,UAAU,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAIlE,WAAW,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,WAAW,CAAC,CAAC;IAGxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAGvB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
|
|
@@ -13,6 +13,10 @@ namespace margelo::nitro::reactnativeimagecroppicker { struct PickedImage; }
|
|
|
13
13
|
namespace margelo::nitro::reactnativeimagecroppicker { struct CropRect; }
|
|
14
14
|
// Forward declaration of `ImageCropPickerOptions` to properly resolve imports.
|
|
15
15
|
namespace margelo::nitro::reactnativeimagecroppicker { struct ImageCropPickerOptions; }
|
|
16
|
+
// Forward declaration of `ImageCropperAppearance` to properly resolve imports.
|
|
17
|
+
namespace margelo::nitro::reactnativeimagecroppicker { struct ImageCropperAppearance; }
|
|
18
|
+
// Forward declaration of `ImageCropperColorScheme` to properly resolve imports.
|
|
19
|
+
namespace margelo::nitro::reactnativeimagecroppicker { enum class ImageCropperColorScheme; }
|
|
16
20
|
|
|
17
21
|
#include "PickedImage.hpp"
|
|
18
22
|
#include <NitroModules/Promise.hpp>
|
|
@@ -25,6 +29,10 @@ namespace margelo::nitro::reactnativeimagecroppicker { struct ImageCropPickerOpt
|
|
|
25
29
|
#include <NitroModules/JUnit.hpp>
|
|
26
30
|
#include "ImageCropPickerOptions.hpp"
|
|
27
31
|
#include "JImageCropPickerOptions.hpp"
|
|
32
|
+
#include "ImageCropperAppearance.hpp"
|
|
33
|
+
#include "JImageCropperAppearance.hpp"
|
|
34
|
+
#include "ImageCropperColorScheme.hpp"
|
|
35
|
+
#include "JImageCropperColorScheme.hpp"
|
|
28
36
|
|
|
29
37
|
namespace margelo::nitro::reactnativeimagecroppicker {
|
|
30
38
|
|