@codebymanan/react-native-photo-editor 0.1.1 → 0.2.0
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/PhotoEditor.podspec +2 -1
- package/README.md +81 -3
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/photoeditor/PhotoEditorModule.kt +1 -1
- package/android/src/main/java/com/photoeditor/editor/EditImageActivity.kt +19 -2
- package/ios/PhotoEditorManager.swift +128 -9
- package/ios/StickerContainerView.swift +179 -0
- package/jest/mock.js +7 -0
- package/lib/typescript/src/types.d.ts +5 -3
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/types.ts +5 -3
package/PhotoEditor.podspec
CHANGED
package/README.md
CHANGED
|
@@ -20,15 +20,51 @@ const editedPath = await open({
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
`open()` resolves with the file path of the edited image, or rejects with
|
|
23
|
-
`E_CANCELLED` if the user backs out.
|
|
23
|
+
`E_CANCELLED` if the user backs out (see [Errors](#errors)).
|
|
24
24
|
|
|
25
25
|
| Option | Type | Description |
|
|
26
26
|
| --- | --- | --- |
|
|
27
|
-
| `path` | `string` | **Required.** Image to edit. Accepts a local file path, a `file://`
|
|
28
|
-
| `stickers` | `string[]` | Sticker
|
|
27
|
+
| `path` | `string` | **Required.** Image to edit. Accepts a local file path, a `file://` URI, a remote `http(s)` URL, a `content://` URI (Android) or a `ph://` photo library URI (iOS, see below). |
|
|
28
|
+
| `stickers` | `string[]` | Sticker images shown in the sticker picker, as remote URLs or local paths. On iOS the sticker tool only appears when this is non-empty. |
|
|
29
29
|
| `language` | `string` | BCP-47 tag (`'fr'`, `'pt-BR'`, `'zh-Hans'`) for the editor UI. Defaults to the device language. See [Localization](#localization). |
|
|
30
30
|
| `translations` | `Record<string, string>` | Per-key string overrides. See [Localization](#localization). |
|
|
31
31
|
|
|
32
|
+
### Stickers
|
|
33
|
+
|
|
34
|
+
Pass the images you want in the sticker picker as `stickers`. Each entry is a
|
|
35
|
+
remote `http(s)` URL or a path to a file on the device; transparent PNGs work
|
|
36
|
+
best.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const STICKERS = [
|
|
40
|
+
'https://example.com/stickers/heart.png',
|
|
41
|
+
'https://example.com/stickers/star.png',
|
|
42
|
+
`${RNFS.DocumentDirectoryPath}/stickers/smile.png`, // a file you saved earlier
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
await open({ path, stickers: STICKERS });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The user opens the sticker tool, taps a sticker to drop it onto the photo, then
|
|
49
|
+
drags, pinches and rotates it into place. Stickers load in the background, so
|
|
50
|
+
remote ones appear in the picker as they arrive.
|
|
51
|
+
|
|
52
|
+
Images bundled with `require('./sticker.png')` are not file paths in release
|
|
53
|
+
builds, so they cannot be passed directly. Host them remotely or copy them to
|
|
54
|
+
the device first.
|
|
55
|
+
|
|
56
|
+
When `stickers` is omitted or empty the platforms differ: **iOS** hides the
|
|
57
|
+
sticker tool, while **Android** shows a small built-in set of sample stickers.
|
|
58
|
+
Pass your own list if you want the same behaviour on both.
|
|
59
|
+
|
|
60
|
+
### Photo library URIs on iOS
|
|
61
|
+
|
|
62
|
+
`ph://` URIs, as returned by pickers such as
|
|
63
|
+
`@react-native-camera-roll/camera-roll`, are loaded through the Photos
|
|
64
|
+
framework. iCloud-only photos are downloaded on demand. Your app needs an
|
|
65
|
+
`NSPhotoLibraryUsageDescription` entry in its `Info.plist`; if the user has
|
|
66
|
+
never been asked, the editor asks for access before loading the photo.
|
|
67
|
+
|
|
32
68
|
## Localization
|
|
33
69
|
|
|
34
70
|
By default the editor follows the device language on both platforms. Pass
|
|
@@ -185,6 +221,48 @@ underlying editor never sets `semanticContentAttribute`. So the iOS editor lays
|
|
|
185
221
|
out right-to-left when your app itself is running in an RTL language, and not
|
|
186
222
|
because of this option.
|
|
187
223
|
|
|
224
|
+
## Errors
|
|
225
|
+
|
|
226
|
+
`open()` rejects with one of these codes on `error.code`:
|
|
227
|
+
|
|
228
|
+
| Code | When |
|
|
229
|
+
| --- | --- |
|
|
230
|
+
| `E_CANCELLED` | The user closed the editor without saving. Usually not an error worth showing. |
|
|
231
|
+
| `E_IN_PROGRESS` | `open()` was called while another editor session was still open. |
|
|
232
|
+
| `E_INVALID_OPTIONS` | `path` was missing or empty. |
|
|
233
|
+
| `E_NO_ACTIVITY` | There was no screen to present the editor from, e.g. the app was in the background. |
|
|
234
|
+
| `E_FAILED` | The image could not be loaded or saved. `error.message` says which, including a missing photo library permission on iOS. |
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
try {
|
|
238
|
+
const edited = await open({ path });
|
|
239
|
+
} catch (e) {
|
|
240
|
+
if (e.code !== 'E_CANCELLED') {
|
|
241
|
+
console.warn(e.code, e.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Testing with Jest
|
|
247
|
+
|
|
248
|
+
The native module is not available under Jest. Mock it with the bundled stub,
|
|
249
|
+
whose `open` resolves with a fake path and can be overridden per test:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
// jest.setup.js
|
|
253
|
+
jest.mock('@codebymanan/react-native-photo-editor', () =>
|
|
254
|
+
require('@codebymanan/react-native-photo-editor/jest/mock')
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
import { open } from '@codebymanan/react-native-photo-editor';
|
|
260
|
+
|
|
261
|
+
open.mockRejectedValueOnce(
|
|
262
|
+
Object.assign(new Error('cancelled'), { code: 'E_CANCELLED' })
|
|
263
|
+
);
|
|
264
|
+
```
|
|
265
|
+
|
|
188
266
|
## Contributing
|
|
189
267
|
|
|
190
268
|
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
package/android/build.gradle
CHANGED
|
@@ -50,7 +50,7 @@ dependencies {
|
|
|
50
50
|
implementation "com.facebook.react:react-android"
|
|
51
51
|
|
|
52
52
|
// Editing engine
|
|
53
|
-
implementation "com.burhanrashid52:photoeditor:3.
|
|
53
|
+
implementation "com.burhanrashid52:photoeditor:3.1.1"
|
|
54
54
|
|
|
55
55
|
// Editor screen UI
|
|
56
56
|
implementation "androidx.appcompat:appcompat:1.7.0"
|
|
@@ -55,7 +55,7 @@ class PhotoEditorModule(reactContext: ReactApplicationContext) :
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
override fun open(options: ReadableMap, promise: Promise) {
|
|
58
|
-
val activity = currentActivity
|
|
58
|
+
val activity = reactApplicationContext.currentActivity
|
|
59
59
|
if (activity == null) {
|
|
60
60
|
promise.reject(E_NO_ACTIVITY, "No activity attached to React context")
|
|
61
61
|
return
|
|
@@ -128,6 +128,7 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis
|
|
|
128
128
|
.build()
|
|
129
129
|
|
|
130
130
|
mPhotoEditor.setOnPhotoEditorListener(this)
|
|
131
|
+
updateUndoRedoState()
|
|
131
132
|
|
|
132
133
|
loadSourceImage()
|
|
133
134
|
}
|
|
@@ -195,10 +196,12 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis
|
|
|
195
196
|
|
|
196
197
|
override fun onAddViewListener(viewType: ViewType, numberOfAddedViews: Int) {
|
|
197
198
|
Log.d(TAG, "onAddViewListener() called with: viewType = [$viewType]")
|
|
199
|
+
updateUndoRedoState()
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
override fun onRemoveViewListener(viewType: ViewType, numberOfAddedViews: Int) {
|
|
201
203
|
Log.d(TAG, "onRemoveViewListener() called with: viewType = [$viewType]")
|
|
204
|
+
updateUndoRedoState()
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
override fun onStartViewChangeListener(viewType: ViewType) {
|
|
@@ -207,6 +210,14 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis
|
|
|
207
210
|
|
|
208
211
|
override fun onStopViewChangeListener(viewType: ViewType) {
|
|
209
212
|
Log.d(TAG, "onStopViewChangeListener() called with: viewType = [$viewType]")
|
|
213
|
+
// A finished brush stroke clears the redo stack without firing add/remove
|
|
214
|
+
updateUndoRedoState()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Greys out undo/redo when there is nothing to step through, via the pe_undo/pe_redo selectors. */
|
|
218
|
+
private fun updateUndoRedoState() {
|
|
219
|
+
mImgUndo.isEnabled = mPhotoEditor.isUndoAvailable
|
|
220
|
+
mImgRedo.isEnabled = mPhotoEditor.isRedoAvailable
|
|
210
221
|
}
|
|
211
222
|
|
|
212
223
|
override fun onTouchSourceImage(event: MotionEvent) {
|
|
@@ -216,8 +227,14 @@ class EditImageActivity : BaseActivity(), OnPhotoEditorListener, View.OnClickLis
|
|
|
216
227
|
@SuppressLint("NonConstantResourceId")
|
|
217
228
|
override fun onClick(view: View) {
|
|
218
229
|
when (view.id) {
|
|
219
|
-
R.id.imgUndo ->
|
|
220
|
-
|
|
230
|
+
R.id.imgUndo -> {
|
|
231
|
+
mPhotoEditor.undo()
|
|
232
|
+
updateUndoRedoState()
|
|
233
|
+
}
|
|
234
|
+
R.id.imgRedo -> {
|
|
235
|
+
mPhotoEditor.redo()
|
|
236
|
+
updateUndoRedoState()
|
|
237
|
+
}
|
|
221
238
|
|
|
222
239
|
R.id.imgSave -> saveImage()
|
|
223
240
|
R.id.imgClose -> onBackPressed()
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import Photos
|
|
1
2
|
import UIKit
|
|
2
3
|
import ZLImageEditor
|
|
3
4
|
|
|
4
5
|
@objc(PhotoEditorManager)
|
|
5
6
|
public class PhotoEditorManager: NSObject {
|
|
6
7
|
|
|
8
|
+
/// True from the moment `open()` accepts a call until its promise settles,
|
|
9
|
+
/// which spans the image load as well as the editor itself. Main thread only.
|
|
10
|
+
private static var isSessionActive = false
|
|
11
|
+
|
|
7
12
|
@objc public static func open(
|
|
8
13
|
_ path: String,
|
|
9
14
|
stickers: [String],
|
|
@@ -12,10 +17,46 @@ public class PhotoEditorManager: NSObject {
|
|
|
12
17
|
resolve: @escaping (String) -> Void,
|
|
13
18
|
reject: @escaping (String, String) -> Void
|
|
14
19
|
) {
|
|
15
|
-
|
|
20
|
+
DispatchQueue.main.async {
|
|
21
|
+
guard !isSessionActive else {
|
|
22
|
+
reject("E_IN_PROGRESS", "Another editor session is already open")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
isSessionActive = true
|
|
26
|
+
|
|
27
|
+
// Every settlement below already happens on the main thread.
|
|
28
|
+
startSession(
|
|
29
|
+
path,
|
|
30
|
+
stickers: stickers,
|
|
31
|
+
language: language,
|
|
32
|
+
translations: translations,
|
|
33
|
+
resolve: { outputPath in
|
|
34
|
+
isSessionActive = false
|
|
35
|
+
resolve(outputPath)
|
|
36
|
+
},
|
|
37
|
+
reject: { code, message in
|
|
38
|
+
isSessionActive = false
|
|
39
|
+
reject(code, message)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private static func startSession(
|
|
45
|
+
_ path: String,
|
|
46
|
+
stickers: [String],
|
|
47
|
+
language: String?,
|
|
48
|
+
translations: [String: Any]?,
|
|
49
|
+
resolve: @escaping (String) -> Void,
|
|
50
|
+
reject: @escaping (String, String) -> Void
|
|
51
|
+
) {
|
|
52
|
+
loadImage(path) { result in
|
|
16
53
|
DispatchQueue.main.async {
|
|
17
|
-
|
|
18
|
-
|
|
54
|
+
let image: UIImage
|
|
55
|
+
switch result {
|
|
56
|
+
case .success(let loaded):
|
|
57
|
+
image = loaded
|
|
58
|
+
case .failure(let error):
|
|
59
|
+
reject("E_FAILED", error.message)
|
|
19
60
|
return
|
|
20
61
|
}
|
|
21
62
|
guard let presenter = topViewController() else {
|
|
@@ -27,9 +68,12 @@ public class PhotoEditorManager: NSObject {
|
|
|
27
68
|
.languageType(languageType(for: language))
|
|
28
69
|
.customLanguageConfig(customLanguage(from: translations))
|
|
29
70
|
|
|
71
|
+
// Assigned on every call, nil included: the configuration is a shared
|
|
72
|
+
// singleton, and the editor hides the sticker tool when this is nil.
|
|
30
73
|
ZLImageEditorConfiguration.default()
|
|
31
|
-
.editImageTools([.draw, .clip, .textSticker, .mosaic, .filter, .adjust])
|
|
74
|
+
.editImageTools([.draw, .clip, .imageSticker, .textSticker, .mosaic, .filter, .adjust])
|
|
32
75
|
.adjustTools([.brightness, .contrast, .saturation])
|
|
76
|
+
.imageStickerContainerView(stickers.isEmpty ? nil : StickerContainerView(urls: stickers))
|
|
33
77
|
|
|
34
78
|
let editor = ZLEditImageViewController(image: image)
|
|
35
79
|
editor.modalPresentationStyle = .fullScreen
|
|
@@ -112,18 +156,93 @@ public class PhotoEditorManager: NSObject {
|
|
|
112
156
|
}
|
|
113
157
|
}
|
|
114
158
|
|
|
115
|
-
|
|
116
|
-
|
|
159
|
+
struct LoadError: Error {
|
|
160
|
+
let message: String
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Loads the editor image or a sticker. `completion` may run on any queue.
|
|
164
|
+
static func loadImage(_ path: String, completion: @escaping (Result<UIImage, LoadError>) -> Void) {
|
|
165
|
+
let failed = LoadError(message: "Could not load image: \(path)")
|
|
166
|
+
|
|
167
|
+
if path.hasPrefix("ph://") {
|
|
168
|
+
loadPhotoLibraryImage(localIdentifier: String(path.dropFirst(5)), path: path, completion: completion)
|
|
169
|
+
} else if path.hasPrefix("http://") || path.hasPrefix("https://") {
|
|
117
170
|
guard let url = URL(string: path) else {
|
|
118
|
-
completion(
|
|
171
|
+
completion(.failure(failed))
|
|
119
172
|
return
|
|
120
173
|
}
|
|
121
174
|
URLSession.shared.dataTask(with: url) { data, _, _ in
|
|
122
|
-
|
|
175
|
+
guard let image = data.flatMap(UIImage.init(data:)) else {
|
|
176
|
+
completion(.failure(failed))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
completion(.success(image))
|
|
123
180
|
}.resume()
|
|
124
181
|
} else {
|
|
125
182
|
let filePath = path.hasPrefix("file://") ? String(path.dropFirst(7)) : path
|
|
126
|
-
|
|
183
|
+
guard let image = UIImage(contentsOfFile: filePath) else {
|
|
184
|
+
completion(.failure(failed))
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
completion(.success(image))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Resolves a `ph://` URI, as returned by photo pickers such as
|
|
192
|
+
/// @react-native-camera-roll/camera-roll, through the Photos framework.
|
|
193
|
+
private static func loadPhotoLibraryImage(
|
|
194
|
+
localIdentifier: String,
|
|
195
|
+
path: String,
|
|
196
|
+
completion: @escaping (Result<UIImage, LoadError>) -> Void
|
|
197
|
+
) {
|
|
198
|
+
withPhotoLibraryAccess { granted in
|
|
199
|
+
guard granted else {
|
|
200
|
+
completion(.failure(LoadError(
|
|
201
|
+
message: "Photo library access is needed to load \(path)")))
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Some pickers hand out only the asset UUID, without the "/L0/001"
|
|
206
|
+
// suffix that a full local identifier carries; try both.
|
|
207
|
+
var identifiers = [localIdentifier]
|
|
208
|
+
if !localIdentifier.contains("/") {
|
|
209
|
+
identifiers.append("\(localIdentifier)/L0/001")
|
|
210
|
+
}
|
|
211
|
+
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil).firstObject
|
|
212
|
+
else {
|
|
213
|
+
completion(.failure(LoadError(message: "No photo library asset found for \(path)")))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let options = PHImageRequestOptions()
|
|
218
|
+
// Lets iCloud-only photos download instead of failing.
|
|
219
|
+
options.isNetworkAccessAllowed = true
|
|
220
|
+
options.deliveryMode = .highQualityFormat
|
|
221
|
+
options.version = .current
|
|
222
|
+
|
|
223
|
+
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) {
|
|
224
|
+
data, _, _, _ in
|
|
225
|
+
guard let image = data.flatMap(UIImage.init(data:)) else {
|
|
226
|
+
completion(.failure(LoadError(message: "Could not load image: \(path)")))
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
completion(.success(image))
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// Only prompts when the user has never been asked. Limited access counts as
|
|
235
|
+
/// granted: the asset is reachable if the user included it in the selection.
|
|
236
|
+
private static func withPhotoLibraryAccess(_ completion: @escaping (Bool) -> Void) {
|
|
237
|
+
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
|
|
238
|
+
case .authorized, .limited:
|
|
239
|
+
completion(true)
|
|
240
|
+
case .notDetermined:
|
|
241
|
+
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
|
242
|
+
completion(status == .authorized || status == .limited)
|
|
243
|
+
}
|
|
244
|
+
default:
|
|
245
|
+
completion(false)
|
|
127
246
|
}
|
|
128
247
|
}
|
|
129
248
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import ZLImageEditor
|
|
3
|
+
|
|
4
|
+
/// Bottom sheet listing the `stickers` passed to `open()`.
|
|
5
|
+
///
|
|
6
|
+
/// ZLImageEditor only offers its image sticker tool when a view conforming to
|
|
7
|
+
/// `ZLImageStickerContainerDelegate` is configured, and leaves showing, hiding
|
|
8
|
+
/// and populating it entirely to that view.
|
|
9
|
+
final class StickerContainerView: UIView, ZLImageStickerContainerDelegate {
|
|
10
|
+
|
|
11
|
+
var selectImageBlock: ((UIImage) -> Void)?
|
|
12
|
+
var hideBlock: (() -> Void)?
|
|
13
|
+
|
|
14
|
+
private static let sheetHeightRatio: CGFloat = 0.45
|
|
15
|
+
private static let cellSpacing: CGFloat = 12
|
|
16
|
+
private static let columns: CGFloat = 4
|
|
17
|
+
|
|
18
|
+
private let urls: [String]
|
|
19
|
+
/// Indexed like `urls`; nil until that sticker has loaded, and stays nil if it fails.
|
|
20
|
+
private var images: [UIImage?]
|
|
21
|
+
|
|
22
|
+
private let sheet = UIView()
|
|
23
|
+
private let collectionView: UICollectionView
|
|
24
|
+
|
|
25
|
+
init(urls: [String]) {
|
|
26
|
+
self.urls = urls
|
|
27
|
+
self.images = Array(repeating: nil, count: urls.count)
|
|
28
|
+
|
|
29
|
+
let layout = UICollectionViewFlowLayout()
|
|
30
|
+
layout.minimumInteritemSpacing = Self.cellSpacing
|
|
31
|
+
layout.minimumLineSpacing = Self.cellSpacing
|
|
32
|
+
layout.sectionInset = UIEdgeInsets(
|
|
33
|
+
top: Self.cellSpacing, left: Self.cellSpacing, bottom: Self.cellSpacing, right: Self.cellSpacing)
|
|
34
|
+
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
35
|
+
|
|
36
|
+
super.init(frame: .zero)
|
|
37
|
+
|
|
38
|
+
// Tapping the dimmed area above the sheet dismisses it; the editor does not.
|
|
39
|
+
let dismissTap = UITapGestureRecognizer(target: self, action: #selector(hide))
|
|
40
|
+
dismissTap.delegate = self
|
|
41
|
+
addGestureRecognizer(dismissTap)
|
|
42
|
+
|
|
43
|
+
sheet.backgroundColor = UIColor(white: 0.1, alpha: 0.95)
|
|
44
|
+
sheet.layer.cornerRadius = 12
|
|
45
|
+
sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
46
|
+
addSubview(sheet)
|
|
47
|
+
|
|
48
|
+
collectionView.backgroundColor = .clear
|
|
49
|
+
collectionView.dataSource = self
|
|
50
|
+
collectionView.delegate = self
|
|
51
|
+
collectionView.register(StickerCell.self, forCellWithReuseIdentifier: StickerCell.reuseIdentifier)
|
|
52
|
+
sheet.addSubview(collectionView)
|
|
53
|
+
|
|
54
|
+
loadStickers()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@available(*, unavailable)
|
|
58
|
+
required init?(coder: NSCoder) {
|
|
59
|
+
fatalError("init(coder:) has not been implemented")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// MARK: - ZLImageStickerContainerDelegate
|
|
63
|
+
|
|
64
|
+
func show(in view: UIView) {
|
|
65
|
+
if superview !== view {
|
|
66
|
+
removeFromSuperview()
|
|
67
|
+
view.addSubview(self)
|
|
68
|
+
}
|
|
69
|
+
frame = view.bounds
|
|
70
|
+
autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
71
|
+
isHidden = false
|
|
72
|
+
layoutIfNeeded()
|
|
73
|
+
|
|
74
|
+
sheet.transform = CGAffineTransform(translationX: 0, y: sheet.bounds.height)
|
|
75
|
+
backgroundColor = .clear
|
|
76
|
+
UIView.animate(withDuration: 0.25) {
|
|
77
|
+
self.sheet.transform = .identity
|
|
78
|
+
self.backgroundColor = UIColor(white: 0, alpha: 0.3)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc private func hide() {
|
|
83
|
+
UIView.animate(withDuration: 0.25, animations: {
|
|
84
|
+
self.sheet.transform = CGAffineTransform(translationX: 0, y: self.sheet.bounds.height)
|
|
85
|
+
self.backgroundColor = .clear
|
|
86
|
+
}, completion: { _ in
|
|
87
|
+
self.isHidden = true
|
|
88
|
+
self.sheet.transform = .identity
|
|
89
|
+
})
|
|
90
|
+
hideBlock?()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MARK: - Layout
|
|
94
|
+
|
|
95
|
+
override func layoutSubviews() {
|
|
96
|
+
super.layoutSubviews()
|
|
97
|
+
let height = bounds.height * Self.sheetHeightRatio
|
|
98
|
+
sheet.frame = CGRect(x: 0, y: bounds.height - height, width: bounds.width, height: height)
|
|
99
|
+
collectionView.frame = sheet.bounds
|
|
100
|
+
collectionView.contentInset.bottom = safeAreaInsets.bottom
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - Loading
|
|
104
|
+
|
|
105
|
+
private func loadStickers() {
|
|
106
|
+
for (index, url) in urls.enumerated() {
|
|
107
|
+
PhotoEditorManager.loadImage(url) { [weak self] result in
|
|
108
|
+
guard case .success(let image) = result else { return }
|
|
109
|
+
DispatchQueue.main.async {
|
|
110
|
+
guard let self else { return }
|
|
111
|
+
self.images[index] = image
|
|
112
|
+
self.collectionView.reloadItems(at: [IndexPath(item: index, section: 0)])
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
extension StickerContainerView: UIGestureRecognizerDelegate {
|
|
120
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
121
|
+
!sheet.frame.contains(touch.location(in: self))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
extension StickerContainerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
|
126
|
+
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
127
|
+
urls.count
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func collectionView(
|
|
131
|
+
_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
|
|
132
|
+
) -> UICollectionViewCell {
|
|
133
|
+
let cell = collectionView.dequeueReusableCell(
|
|
134
|
+
withReuseIdentifier: StickerCell.reuseIdentifier, for: indexPath) as! StickerCell
|
|
135
|
+
cell.imageView.image = images[indexPath.item]
|
|
136
|
+
return cell
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func collectionView(
|
|
140
|
+
_ collectionView: UICollectionView,
|
|
141
|
+
layout collectionViewLayout: UICollectionViewLayout,
|
|
142
|
+
sizeForItemAt indexPath: IndexPath
|
|
143
|
+
) -> CGSize {
|
|
144
|
+
let gaps = Self.cellSpacing * (Self.columns + 1)
|
|
145
|
+
let side = floor((collectionView.bounds.width - gaps) / Self.columns)
|
|
146
|
+
return CGSize(width: side, height: side)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
150
|
+
// A sticker that has not loaded (or failed to) has nothing to add.
|
|
151
|
+
guard let image = images[indexPath.item] else { return }
|
|
152
|
+
selectImageBlock?(image)
|
|
153
|
+
hide()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private final class StickerCell: UICollectionViewCell {
|
|
158
|
+
static let reuseIdentifier = "StickerCell"
|
|
159
|
+
|
|
160
|
+
let imageView = UIImageView()
|
|
161
|
+
|
|
162
|
+
override init(frame: CGRect) {
|
|
163
|
+
super.init(frame: frame)
|
|
164
|
+
imageView.contentMode = .scaleAspectFit
|
|
165
|
+
imageView.frame = contentView.bounds
|
|
166
|
+
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
167
|
+
contentView.addSubview(imageView)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@available(*, unavailable)
|
|
171
|
+
required init?(coder: NSCoder) {
|
|
172
|
+
fatalError("init(coder:) has not been implemented")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
override func prepareForReuse() {
|
|
176
|
+
super.prepareForReuse()
|
|
177
|
+
imageView.image = nil
|
|
178
|
+
}
|
|
179
|
+
}
|
package/jest/mock.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
// Stands in for the native module in Jest, where TurboModules are unavailable.
|
|
3
|
+
// Usage: jest.mock('@codebymanan/react-native-photo-editor', () =>
|
|
4
|
+
// require('@codebymanan/react-native-photo-editor/jest/mock'));
|
|
5
|
+
module.exports = {
|
|
6
|
+
open: jest.fn(() => Promise.resolve('/tmp/photo_editor/edited.png')),
|
|
7
|
+
};
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export interface PhotoEditorOptions {
|
|
2
2
|
/**
|
|
3
|
-
* The image to edit. Accepts a local file path, `file://` or
|
|
4
|
-
*
|
|
3
|
+
* The image to edit. Accepts a local file path, `file://` URI, or a remote
|
|
4
|
+
* `http(s)` URL, plus `content://` URIs on Android and `ph://` photo library
|
|
5
|
+
* URIs on iOS.
|
|
5
6
|
*/
|
|
6
7
|
path: string;
|
|
7
8
|
/**
|
|
8
|
-
* Optional list of sticker
|
|
9
|
+
* Optional list of sticker images (remote URLs or local paths) shown in the
|
|
10
|
+
* sticker picker. On iOS the sticker tool only appears when this is set.
|
|
9
11
|
*/
|
|
10
12
|
stickers?: string[];
|
|
11
13
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CAC1C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codebymanan/react-native-photo-editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A React Native photo editor plugin wrapping native editors for Android and iOS",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"types": "./lib/typescript/src/index.d.ts",
|
|
11
11
|
"default": "./lib/module/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./jest/mock": "./jest/mock.js",
|
|
13
14
|
"./package.json": "./package.json"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"cpp",
|
|
21
22
|
"*.podspec",
|
|
22
23
|
"react-native.config.js",
|
|
24
|
+
"jest",
|
|
23
25
|
"!ios/build",
|
|
24
26
|
"!android/build",
|
|
25
27
|
"!android/gradle",
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export interface PhotoEditorOptions {
|
|
2
2
|
/**
|
|
3
|
-
* The image to edit. Accepts a local file path, `file://` or
|
|
4
|
-
*
|
|
3
|
+
* The image to edit. Accepts a local file path, `file://` URI, or a remote
|
|
4
|
+
* `http(s)` URL, plus `content://` URIs on Android and `ph://` photo library
|
|
5
|
+
* URIs on iOS.
|
|
5
6
|
*/
|
|
6
7
|
path: string;
|
|
7
8
|
/**
|
|
8
|
-
* Optional list of sticker
|
|
9
|
+
* Optional list of sticker images (remote URLs or local paths) shown in the
|
|
10
|
+
* sticker picker. On iOS the sticker tool only appears when this is set.
|
|
9
11
|
*/
|
|
10
12
|
stickers?: string[];
|
|
11
13
|
/**
|