Browse Source

drop deprecated, introduce ViewShot component, rename APIs

Gaëtan Renaudeau 6 years ago
parent
commit
fe74b433c8

+ 108
- 61
README.md View File

@@ -5,15 +5,84 @@ Capture a React Native view to an image.
5 5
 
6 6
 <img src="https://github.com/gre/react-native-view-shot-example/raw/master/docs/recursive.gif" width=300 />
7 7
 
8
-> iOS: For React Native version between `0.30.x` and `0.39.x`, you should use `react-native-view-shot@1.5.1`.
8
+## Install
9 9
 
10
-## Usage
10
+```bash
11
+yarn add react-native-view-shot
12
+react-native link react-native-view-shot
13
+```
14
+
15
+## Recommended High Level API
16
+
17
+```js
18
+import { ViewShot } from "react-native-view-shot";
19
+
20
+class ExampleCaptureOnMountManually extends Component {
21
+  componentDidMount () {
22
+    this.refs.viewShot.capture().then(uri => {
23
+      console.log("do something with ", uri);
24
+    });
25
+  }
26
+  render() {
27
+    return (
28
+      <ViewShot ref="viewShot" options={{ format: "jpg", quality: 0.9 }}>
29
+        <Text>...Something to rasterize...</Text>
30
+      </ViewShot>
31
+    );
32
+  }
33
+}
34
+
35
+// alternative
36
+class ExampleCaptureOnMountSimpler extends Component {
37
+  onCapture = uri => {
38
+    console.log("do something with ", uri);
39
+  }
40
+  render() {
41
+    return (
42
+      <ViewShot onCapture={this.onCapture} captureMode="mount">
43
+        <Text>...Something to rasterize...</Text>
44
+      </ViewShot>
45
+    );
46
+  }
47
+}
48
+
49
+// waiting an image
50
+class ExampleWaitingCapture extends Component {
51
+  onImageLoad = () => {
52
+    this.refs.viewShot.capture().then(uri => {
53
+      console.log("do something with ", uri);
54
+    })
55
+  };
56
+  render() {
57
+    return (
58
+      <ViewShot ref="viewShot">
59
+        <Text>...Something to rasterize...</Text>
60
+        <Image ... onLoad={this.onImageLoad} />
61
+      </ViewShot>
62
+    );
63
+  }
64
+}
65
+```
66
+
67
+**Props:**
68
+
69
+- **`children`**: the actual content to rasterize.
70
+- **`options`**: the same options as in `captureRef` method.
71
+- **`captureMode`** (string):
72
+  - if not defined (default). the capture is not automatic and you need to use the ref and call `capture()` yourself.
73
+  - `"mount"`. Capture the view once at mount. (It is important to understand image loading won't be waited, in such case you want to use `"none"` with `viewShotRef.capture()` after `Image#onLoad`.)
74
+  - `"continuous"` EXPERIMENTAL, this will capture A LOT of images continuously. For very specific use-cases.
75
+  - `"update"` EXPERIMENTAL, this will capture images each time React redraw (on did update). For very specific use-cases.
76
+- **`onCapture`**: when a `captureMode` is defined, this callback will be called with the capture result.
77
+- **`onCaptureFailure`**: when a `captureMode` is defined, this callback will be called when a capture fails.
78
+
79
+## `captureRef(view, options)` lower level imperative API
11 80
 
12 81
 ```js
13
-import { takeSnapshot } from "react-native-view-shot";
82
+import { captureRef } from "react-native-view-shot";
14 83
 
15
-takeSnapshot(viewRef, {
16
-  format: "jpeg",
84
+captureRef(viewRef, {
85
+  format: "jpg",
17 86
   quality: 0.8
18 87
 })
19 88
 .then(
@@ -22,39 +91,33 @@ takeSnapshot(viewRef, {
22 91
 );
23 92
 ```
24 93
 
25
-### Example
26
-
27
-[Checkout react-native-view-shot-example](https://github.com/gre/react-native-view-shot-example)
28
-
29
-## Full API
30
-
31
-### `takeSnapshot(view, options)`
32
-
33 94
 Returns a Promise of the image URI.
34 95
 
35 96
 - **`view`** is a reference to a React Native component.
36 97
 - **`options`** may include:
37 98
   - **`width`** / **`height`** *(number)*: the width and height of the final image (resized from the View bound. don't provide it if you want the original pixel size).
38
-  - **`format`** *(string)*: either `png` or `jpg`/`jpeg` or `webm` (Android). Defaults to `png`.
39
-  - **`quality`** *(number)*: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpeg)
99
+  - **`format`** *(string)*: either `png` or `jpg` or `webm` (Android). Defaults to `png`.
100
+  - **`quality`** *(number)*: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
40 101
   - **`result`** *(string)*, the method you want to use to save the snapshot, one of:
41
-    - `"file"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
102
+    - `"tmpfile"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
42 103
     - `"base64"`: encode as base64 and returns the raw string. Use only with small images as this may result of lags (the string is sent over the bridge). *N.B. This is not a data uri, use `data-uri` instead*.
43 104
     - `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
44 105
  - **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height.
45 106
 
46
-### DEPRECATED `path` option and `dirs` constants
107
+## `releaseCapture(uri)`
47 108
 
48
-> A feature used to allow to set an arbitrary file path. This has become tricky to maintain because all the edge cases and use-cases of file management so we have decided to drop it, making this library focusing more on solving snapshotting and not file system.
109
+This method release a previously captured `uri`. For tmpfile it will clean them out, for other result types it just won't do anything.
49 110
 
50
-To migrate from this old feature, you have a few solutions:
111
+NB: the tmpfile captures are automatically cleaned out after the app closes, so you might not have to worry about this unless advanced usecases. The `ViewShot` component will use it each time you capture more than once (useful for continuous capture to not leak files).
51 112
 
52
-- If you want to save the snapshotted image result to the CameraRoll, just use https://facebook.github.io/react-native/docs/cameraroll.html#savetocameraroll
53
-- If you want to save it to an arbitrary file path, use something like https://github.com/itinance/react-native-fs
54
-- For any more advanced needs, you can write your own (or find another) native module that would solve your use-case.
113
+### Advanced Examples
114
+
115
+[Checkout react-native-view-shot-example](https://github.com/gre/react-native-view-shot-example)
55 116
 
56 117
 ## Interoperability Table
57 118
 
119
+> Snapshots are not guaranteed to be pixel perfect. It also depends on the platform. Here is some difference we have noticed and how to workaround.
120
+
58 121
 Model tested: iPhone 6 (iOS), Nexus 5 (Android).
59 122
 
60 123
 | System             | iOS                | Android           | Windows           |
@@ -71,60 +134,44 @@ Model tested: iPhone 6 (iOS), Nexus 5 (Android).
71 134
 3. Component itself lacks platform support.
72 135
 4. But you can just use the react-native-maps snapshot function: https://github.com/airbnb/react-native-maps#take-snapshot-of-map
73 136
 
74
-## Caveats
137
+## Troubleshooting / FAQ
75 138
 
76
-Snapshots are not guaranteed to be pixel perfect. It also depends on the platform. Here is some difference we have noticed and how to workaround.
139
+### Saving to a file?
77 140
 
78
-- Support of special components like Video / GL views is not guaranteed to work. In case of failure, the `takeSnapshot` promise gets rejected (the library won't crash).
79
-- It's preferable to **use a background color on the view you rasterize** to avoid transparent pixels and potential weirdness that some border appear around texts.
141
+- If you want to save the snapshotted image result to the CameraRoll, just use https://facebook.github.io/react-native/docs/cameraroll.html#savetocameraroll
142
+- If you want to save it to an arbitrary file path, use something like https://github.com/itinance/react-native-fs
143
+- For any more advanced needs, you can write your own (or find another) native module that would solve your use-case.
80 144
 
81
-### specific to Android implementation
145
+### The snapshot is rejected with an error?
82 146
 
83
-- you need to make sure `collapsable` is set to `false` if you want to snapshot a **View**. Some content might even need to be wrapped into such `<View collapsable={false}>` to actually make them snapshotable! Otherwise that view won't reflect any UI View. ([found by @gaguirre](https://github.com/gre/react-native-view-shot/issues/7#issuecomment-245302844))
84
--  if you implement a third party library and want to get back a File, you must first resolve the `Uri`. (the `file` result returns an `Uri` so it's consistent with iOS and can be given to APIs like `Image.getSize`)
147
+- Support of special components like Video / GL views is not guaranteed to work. In case of failure, the `captureRef` promise gets rejected (the library won't crash).
85 148
 
86
-## Getting started
149
+### get a black or blank result or still have an error with simple views?
87 150
 
88
-```
89
-npm install --save react-native-view-shot
90
-```
151
+Check the **Interoperability Table** above. Some special components are unfortunately not supported. If you have a View that contains one of an unsupported component, the whole snapshot might be compromised as well.
91 152
 
92
-### Mostly automatic installation
153
+### black background instead of transparency / weird border appear around texts?
93 154
 
94
-```
95
-react-native link react-native-view-shot
96
-```
155
+- It's preferable to **use a background color on the view you rasterize** to avoid transparent pixels and potential weirdness that some border appear around texts.
156
+
157
+### on Android, getting "Trying to resolve view with tag '{tagID}' which doesn't exist"
158
+
159
+> you need to make sure `collapsable` is set to `false` if you want to snapshot a **View**. Some content might even need to be wrapped into such `<View collapsable={false}>` to actually make them snapshotable! Otherwise that view won't reflect any UI View. ([found by @gaguirre](https://github.com/gre/react-native-view-shot/issues/7#issuecomment-245302844))
160
+
161
+Alternatively, you can use the `ViewShot` component that will have `collapsable={false}` set to solve this problem.
97 162
 
98
-### Manual installation
163
+### Getting "The content size must not be zero or negative."
99 164
 
100
-#### iOS
165
+> Make sure you don't snapshot instantly, you need to wait at least there is a first `onLayout` event, or after a timeout, otherwise the View might not be ready yet. (It should also be safe to just wait Image `onLoad` if you have one). If you still have the problem, make sure your view actually have a width and height > 0.
101 166
 
102
-1. In XCode, in the project navigator, right click `Libraries` ➜ `Add Files to [your project's name]`
103
-2. Go to `node_modules` ➜ `react-native-view-shot` and add `RNViewShot.xcodeproj`
104
-3. In XCode, in the project navigator, select your project. Add `libRNViewShot.a` to your project's `Build Phases` ➜ `Link Binary With Libraries`
105
-4. Run your project (`Cmd+R`)<
167
+Alternatively, you can use the `ViewShot` component that will wait the first `onLayout`.
106 168
 
107
-#### Android
169
+### Snapshotted image does not match my width and height but is twice/3-times bigger
108 170
 
109
-1. Open up `android/app/src/main/java/[...]/MainActivity.java`
110
- - Add `import fr.greweb.reactnativeviewshot.RNViewShotPackage;` to the imports at the top of the file
111
- - Add `new RNViewShotPackage()` to the list returned by the `getPackages()` method
112
-2. Append the following lines to `android/settings.gradle`:
113
- 	```
114
- 	include ':react-native-view-shot'
115
- 	project(':react-native-view-shot').projectDir = new File(rootProject.projectDir, 	'../node_modules/react-native-view-shot/android')
116
- 	```
117
-3. Insert the following lines inside the dependencies block in `android/app/build.gradle`:
118
- 	```
119
-     compile project(':react-native-view-shot')
120
- 	```
171
+This is because the snapshot image result is in real pixel size where the width/height defined in a React Native style are defined in "point" unit. You might want to set width and height option to force a resize. (might affect image quality)
121 172
 
122
-#### Windows
123 173
 
124
-1. In Visual Studio, in the solution explorer, right click on your solution then select `Add` ➜ `ExisitingProject`
125
-2. Go to `node_modules` ➜ `react-native-view-shot` and add `RNViewShot.csproj` (UWP) or optionally `RNViewShot.Net46.csproj` (WPF)
126
-3. In Visual Studio, in the solution explorer, right click on your Application project then select `Add` ➜ `Reference`
127
-4. Under the projects tab select `RNViewShot` (UWP) or `RNViewShot.Net46` (WPF)
174
+---
128 175
 
129 176
 ## Thanks
130 177
 

+ 25
- 48
android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java View File

@@ -3,6 +3,7 @@ package fr.greweb.reactnativeviewshot;
3 3
 
4 4
 import android.content.Context;
5 5
 import android.graphics.Bitmap;
6
+import android.net.Uri;
6 7
 import android.os.AsyncTask;
7 8
 import android.os.Environment;
8 9
 import android.util.DisplayMetrics;
@@ -23,6 +24,7 @@ import com.facebook.react.uimanager.UIManagerModule;
23 24
 import java.io.File;
24 25
 import java.io.FilenameFilter;
25 26
 import java.io.IOException;
27
+import java.util.Collections;
26 28
 import java.util.HashMap;
27 29
 import java.util.Map;
28 30
 
@@ -42,7 +44,7 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
42 44
 
43 45
     @Override
44 46
     public Map<String, Object> getConstants() {
45
-        return getSystemFolders(this.getReactApplicationContext());
47
+        return Collections.emptyMap();
46 48
     }
47 49
 
48 50
     @Override
@@ -52,41 +54,35 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
52 54
     }
53 55
 
54 56
     @ReactMethod
55
-    public void takeSnapshot(int tag, ReadableMap options, Promise promise) {
57
+    public void releaseCapture(String uri) {
58
+        File file = new File(Uri.parse(uri).getPath());
59
+        if (!file.exists()) return;
60
+        File parent = file.getParentFile();
61
+        if (parent.equals(reactContext.getExternalCacheDir()) || parent.equals(reactContext.getCacheDir())) {
62
+            file.delete();
63
+        }
64
+    }
65
+
66
+    @ReactMethod
67
+    public void captureRef(int tag, ReadableMap options, Promise promise) {
56 68
         ReactApplicationContext context = getReactApplicationContext();
57
-        String format = options.hasKey("format") ? options.getString("format") : "png";
69
+        String format = options.getString("format");
58 70
         Bitmap.CompressFormat compressFormat =
59
-                format.equals("png")
60
-                        ? Bitmap.CompressFormat.PNG
61
-                        : format.equals("jpg")||format.equals("jpeg")
62
-                        ? Bitmap.CompressFormat.JPEG
63
-                        : format.equals("webm")
64
-                        ? Bitmap.CompressFormat.WEBP
65
-                        : null;
66
-        if (compressFormat == null) {
67
-            promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Unsupported image format: "+format+". Try one of: png | jpg | jpeg");
68
-            return;
69
-        }
70
-        double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0;
71
+          format.equals("jpg")
72
+          ? Bitmap.CompressFormat.JPEG
73
+          : format.equals("webm")
74
+          ? Bitmap.CompressFormat.WEBP
75
+          : Bitmap.CompressFormat.PNG;
76
+        double quality = options.getDouble("quality");
71 77
         DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
72 78
         Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
73 79
         Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null;
74
-        String result = options.hasKey("result") ? options.getString("result") : "tmpfile";
75
-        Boolean snapshotContentContainer = options.hasKey("snapshotContentContainer") ? options.getBoolean("snapshotContentContainer") : false;
80
+        String result = options.getString("result");
81
+        Boolean snapshotContentContainer = options.getBoolean("snapshotContentContainer");
76 82
         try {
77 83
             File file = null;
78
-            if ("file".equals(result)) {
79
-                if (options.hasKey("path")) {
80
-                    file = new File(options.getString("path"));
81
-                    file.getParentFile().mkdirs();
82
-                    file.createNewFile();
83
-                }
84
-                else {
85
-                    file = createTempFile(getReactApplicationContext(), format);
86
-                }
87
-            }
88
-            else if ("tmpfile".equals(result)) {
89
-                file = createTempFile(getReactApplicationContext(), format);
84
+            if ("tmpfile".equals(result)) {
85
+              file = createTempFile(getReactApplicationContext(), format);
90 86
             }
91 87
             UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
92 88
             uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer,reactContext, promise));
@@ -136,25 +132,6 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
136 132
         }
137 133
     }
138 134
 
139
-    static private Map<String, Object> getSystemFolders(ReactApplicationContext ctx) {
140
-        Map<String, Object> res = new HashMap<>();
141
-        res.put("CacheDir", ctx.getCacheDir().getAbsolutePath());
142
-        res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
143
-        res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath());
144
-        res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
145
-        res.put("MainBundleDir", ctx.getApplicationInfo().dataDir);
146
-        res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath());
147
-        res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
148
-        res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath());
149
-        res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath());
150
-        String state;
151
-        state = Environment.getExternalStorageState();
152
-        if (state.equals(Environment.MEDIA_MOUNTED)) {
153
-            res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath());
154
-        }
155
-        return res;
156
-    }
157
-
158 135
     /**
159 136
      * Create a temporary file in the cache directory on either internal or external storage,
160 137
      * whichever is available and has more free space.

+ 1
- 5
android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java View File

@@ -79,11 +79,10 @@ public class ViewShot implements UIBlock {
79 79
             return;
80 80
         }
81 81
         try {
82
-            if ("file".equals(result) || "tmpfile".equals(result)) {
82
+            if ("tmpfile".equals(result)) {
83 83
                 os = new FileOutputStream(output);
84 84
                 captureView(view, os);
85 85
                 String uri = Uri.fromFile(output).toString();
86
-                reactContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(uri)));
87 86
                 promise.resolve(uri);
88 87
             }
89 88
             else if ("base64".equals(result)) {
@@ -101,9 +100,6 @@ public class ViewShot implements UIBlock {
101 100
                 data = "data:image/"+extension+";base64," + data;
102 101
                 promise.resolve(data);
103 102
             }
104
-            else {
105
-                promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Unsupported result: "+result+". Try one of: file | base64 | data-uri");
106
-            }
107 103
         }
108 104
         catch (Exception e) {
109 105
             e.printStackTrace();

+ 0
- 52
index.js View File

@@ -1,52 +0,0 @@
1
-//@flow
2
-import { NativeModules, findNodeHandle } from "react-native";
3
-const { RNViewShot } = NativeModules;
4
-
5
-export const dirs = {
6
-  // cross platform
7
-  CacheDir: RNViewShot.CacheDir,
8
-  DocumentDir: RNViewShot.DocumentDir,
9
-  MainBundleDir: RNViewShot.MainBundleDir,
10
-  MovieDir: RNViewShot.MovieDir,
11
-  MusicDir: RNViewShot.MusicDir,
12
-  PictureDir: RNViewShot.PictureDir,
13
-  // only Android
14
-  DCIMDir: RNViewShot.DCIMDir,
15
-  DownloadDir: RNViewShot.DownloadDir,
16
-  RingtoneDir: RNViewShot.RingtoneDir,
17
-  SDCardDir: RNViewShot.SDCardDir
18
-};
19
-
20
-export function takeSnapshot(
21
-  view: number | ReactElement<any>,
22
-  options?: {
23
-    width?: number,
24
-    height?: number,
25
-    path?: string,
26
-    format?: "png" | "jpg" | "jpeg" | "webm",
27
-    quality?: number,
28
-    result?: "tmpfile" | "file" | "base64" | "data-uri",
29
-    snapshotContentContainer?: boolean
30
-  } = {}
31
-): Promise<string> {
32
-  if (options.result === "file") {
33
-    console.warn(
34
-      "react-native-view-shot: result='file' is deprecated, has been renamed to 'tmpfile' and no longer support any 'path' option. See README for more information"
35
-    );
36
-  } else if ("path" in options) {
37
-    console.warn(
38
-      "react-native-view-shot: path option is deprecated. See README for more information"
39
-    );
40
-  }
41
-  if (typeof view !== "number") {
42
-    const node = findNodeHandle(view);
43
-    if (!node)
44
-      return Promise.reject(
45
-        new Error("findNodeHandle failed to resolve view=" + String(view))
46
-      );
47
-    view = node;
48
-  }
49
-  return RNViewShot.takeSnapshot(view, options);
50
-}
51
-
52
-export default { takeSnapshot, dirs };

+ 28
- 49
ios/RNViewShot.m View File

@@ -1,4 +1,3 @@
1
-
2 1
 #import "RNViewShot.h"
3 2
 #import <AVFoundation/AVFoundation.h>
4 3
 #import <React/RCTLog.h>
@@ -9,7 +8,6 @@
9 8
 #import <React/RCTUIManager.h>
10 9
 #import <React/RCTBridge.h>
11 10
 
12
-
13 11
 @implementation RNViewShot
14 12
 
15 13
 RCT_EXPORT_MODULE()
@@ -21,22 +19,20 @@ RCT_EXPORT_MODULE()
21 19
   return RCTGetUIManagerQueue();
22 20
 }
23 21
 
24
-- (NSDictionary *)constantsToExport
22
+
23
+RCT_EXPORT_METHOD(releaseCapture:(nonnull NSString *)uri)
25 24
 {
26
-  return @{
27
-           @"CacheDir" : [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject],
28
-           @"DocumentDir": [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject],
29
-           @"MainBundleDir" : [[NSBundle mainBundle] bundlePath],
30
-           @"MovieDir": [NSSearchPathForDirectoriesInDomains(NSMoviesDirectory, NSUserDomainMask, YES) firstObject],
31
-           @"MusicDir": [NSSearchPathForDirectoriesInDomains(NSMusicDirectory, NSUserDomainMask, YES) firstObject],
32
-           @"PictureDir": [NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES) firstObject],
33
-           };
25
+  NSString *directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ReactNative"];
26
+  // Ensure it's a valid file in the tmp directory
27
+  if ([uri hasPrefix:directory] && ![uri isEqualToString:directory]) {
28
+    NSFileManager *fileManager = [NSFileManager new];
29
+    if ([fileManager fileExistsAtPath:uri]) {
30
+      [fileManager removeItemAtPath:uri error:NULL];
31
+    }
32
+  }
34 33
 }
35 34
 
36
-// forked from RN implementation
37
-// https://github.com/facebook/react-native/blob/f35b372883a76b5666b016131d59268b42f3c40d/React/Modules/RCTUIManager.m#L1367
38
-
39
-RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
35
+RCT_EXPORT_METHOD(captureRef:(nonnull NSNumber *)target
40 36
                   withOptions:(NSDictionary *)options
41 37
                   resolve:(RCTPromiseResolveBlock)resolve
42 38
                   reject:(RCTPromiseRejectBlock)reject)
@@ -53,9 +49,9 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
53 49
 
54 50
     // Get options
55 51
     CGSize size = [RCTConvert CGSize:options];
56
-    NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
57
-    NSString *result = [RCTConvert NSString:options[@"result"] ?: @"tmpfile"];
58
-    BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"] ?: @"false"];
52
+    NSString *format = [RCTConvert NSString:options[@"format"]];
53
+    NSString *result = [RCTConvert NSString:options[@"result"]];
54
+    BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"]];
59 55
 
60 56
     // Capture image
61 57
     BOOL success;
@@ -117,40 +113,17 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
117 113
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
118 114
 
119 115
       NSData *data;
120
-      if ([format isEqualToString:@"png"]) {
121
-        data = UIImagePNGRepresentation(image);
122
-      } else if ([format isEqualToString:@"jpeg"] || [format isEqualToString:@"jpg"]) {
123
-        CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
116
+      if ([format isEqualToString:@"jpg"]) {
117
+        CGFloat quality = [RCTConvert CGFloat:options[@"quality"]];
124 118
         data = UIImageJPEGRepresentation(image, quality);
125
-      } else {
126
-        reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported image format: %@. Try one of: png | jpg | jpeg", format], nil);
127
-        return;
119
+      }
120
+      else {
121
+        data = UIImagePNGRepresentation(image);
128 122
       }
129 123
 
130 124
       NSError *error = nil;
131 125
       NSString *res = nil;
132
-      if ([result isEqualToString:@"file"] || [result isEqualToString:@"tmpfile"] ) {
133
-        // Save to a temp file
134
-        NSString *path;
135
-        if (options[@"path"] && [result isEqualToString:@"file"]) {
136
-          path = options[@"path"];
137
-          NSString * folder = [path stringByDeletingLastPathComponent];
138
-          NSFileManager * fm = [NSFileManager defaultManager];
139
-          if(![fm fileExistsAtPath:folder]) {
140
-            [fm createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:NULL error:&error];
141
-            [fm createFileAtPath:path contents:nil attributes:nil];
142
-          }
143
-        }
144
-        else {
145
-          path = RCTTempFilePath(format, &error);
146
-        }
147
-        if (path && !error) {
148
-          if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) {
149
-            res = path;
150
-          }
151
-        }
152
-      }
153
-      else if ([result isEqualToString:@"base64"]) {
126
+      if ([result isEqualToString:@"base64"]) {
154 127
         // Return as a base64 raw string
155 128
         res = [data base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
156 129
       }
@@ -160,9 +133,15 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
160 133
         res = [NSString stringWithFormat:@"data:image/%@;base64,%@", format, base64];
161 134
       }
162 135
       else {
163
-        reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported result: %@. Try one of: file | base64 | data-uri", result], nil);
164
-        return;
136
+        // Save to a temp file
137
+        NSString *path = RCTTempFilePath(format, &error);
138
+        if (path && !error) {
139
+          if ([data writeToFile:path options:(NSDataWritingOptions)0 error:&error]) {
140
+            res = path;
141
+          }
142
+        }
165 143
       }
144
+
166 145
       if (res && !error) {
167 146
         resolve(res);
168 147
         return;

+ 2
- 1
package.json View File

@@ -2,7 +2,7 @@
2 2
   "name": "react-native-view-shot",
3 3
   "version": "1.12.0",
4 4
   "description": "Capture a React Native view to an image",
5
-  "main": "index.js",
5
+  "main": "src/index.js",
6 6
   "keywords": [
7 7
     "react-native",
8 8
     "screenshot",
@@ -18,6 +18,7 @@
18 18
     "url": "https://github.com/gre/react-native-view-shot.git"
19 19
   },
20 20
   "peerDependencies": {
21
+    "react": "*",
21 22
     "react-native": "*"
22 23
   }
23 24
 }

+ 254
- 0
src/index.js View File

@@ -0,0 +1,254 @@
1
+//@flow
2
+import React, { Component } from "react";
3
+import { View, NativeModules, Platform, findNodeHandle } from "react-native";
4
+const { RNViewShot } = NativeModules;
5
+
6
+const neverEndingPromise = new Promise(() => {});
7
+
8
+type Options = {
9
+  width?: number,
10
+  height?: number,
11
+  format: "png" | "jpg" | "webm",
12
+  quality: number,
13
+  result: "tmpfile" | "base64" | "data-uri",
14
+  snapshotContentContainer: boolean
15
+};
16
+
17
+if (!RNViewShot) {
18
+  console.warn(
19
+    "NativeModules.RNViewShot is undefined. Make sure the library is linked on the native side."
20
+  );
21
+}
22
+
23
+const acceptedFormats = ["png", "jpg"].concat(
24
+  Platform.OS === "android" ? ["webm"] : []
25
+);
26
+
27
+const acceptedResults = ["tmpfile", "base64", "data-uri"];
28
+
29
+const defaultOptions = {
30
+  format: "png",
31
+  quality: 1,
32
+  result: "tmpfile",
33
+  snapshotContentContainer: false
34
+};
35
+
36
+// validate and coerce options
37
+function validateOptions(
38
+  options: ?Object
39
+): { options: Options, errors: Array<string> } {
40
+  options = {
41
+    ...defaultOptions,
42
+    ...options
43
+  };
44
+  const errors = [];
45
+  if (
46
+    "width" in options &&
47
+    (typeof options.width !== "number" || options.width <= 0)
48
+  ) {
49
+    errors.push("option width should be a positive number");
50
+    delete options.width;
51
+  }
52
+  if (
53
+    "height" in options &&
54
+    (typeof options.height !== "number" || options.height <= 0)
55
+  ) {
56
+    errors.push("option height should be a positive number");
57
+    delete options.height;
58
+  }
59
+  if (
60
+    typeof options.quality !== "number" ||
61
+    options.quality < 0 ||
62
+    options.quality > 1
63
+  ) {
64
+    errors.push("option quality should be a number between 0.0 and 1.0");
65
+    options.quality = defaultOptions.quality;
66
+  }
67
+  if (typeof options.snapshotContentContainer !== "boolean") {
68
+    errors.push("option snapshotContentContainer should be a boolean");
69
+  }
70
+  if (acceptedFormats.indexOf(options.format) === -1) {
71
+    options.format = defaultOptions.format;
72
+    errors.push(
73
+      "option format is not in valid formats: " + acceptedFormats.join(" | ")
74
+    );
75
+  }
76
+  if (acceptedResults.indexOf(options.result) === -1) {
77
+    options.result = defaultOptions.result;
78
+    errors.push(
79
+      "option result is not in valid formats: " + acceptedResults.join(" | ")
80
+    );
81
+  }
82
+  return { options, errors };
83
+}
84
+
85
+export function captureRef(
86
+  view: number | ReactElement<*>,
87
+  optionsObject?: Object
88
+): Promise<string> {
89
+  if (typeof view !== "number") {
90
+    const node = findNodeHandle(view);
91
+    if (!node)
92
+      return Promise.reject(
93
+        new Error("findNodeHandle failed to resolve view=" + String(view))
94
+      );
95
+    view = node;
96
+  }
97
+  const { options, errors } = validateOptions(optionsObject);
98
+  if (__DEV__ && errors.length > 0) {
99
+    console.warn(
100
+      "react-native-view-shot: bad options:\n" +
101
+        errors.map(e => `- ${e}`).join("\n")
102
+    );
103
+  }
104
+  return RNViewShot.captureRef(view, options);
105
+}
106
+
107
+export function releaseCapture(uri: string): void {
108
+  if (typeof uri !== "string") {
109
+    if (__DEV__) {
110
+      console.warn("Invalid argument to releaseCapture. Got: " + uri);
111
+    }
112
+  } else {
113
+    RNViewShot.releaseCapture(uri);
114
+  }
115
+}
116
+
117
+type Props = {
118
+  options?: Object,
119
+  captureMode?: "mount" | "continuous" | "update",
120
+  children: React.Element<*>,
121
+  onLayout?: (e: *) => void,
122
+  onCapture: (uri: string) => void,
123
+  onCaptureFailure: (e: Error) => void
124
+};
125
+
126
+function checkCompatibleProps(props: Props) {
127
+  if (!props.captureMode && props.onCapture) {
128
+    console.warn(
129
+      "react-native-view-shot: a captureMode prop must be provided for `onCapture`"
130
+    );
131
+  } else if (props.captureMode && !props.onCapture) {
132
+    console.warn(
133
+      "react-native-view-shot: captureMode prop is defined but onCapture prop callback is missing"
134
+    );
135
+  } else if (
136
+    (props.captureMode === "continuous" || props.captureMode === "update") &&
137
+    props.options &&
138
+    props.options.result &&
139
+    props.options.result !== "tmpfile"
140
+  ) {
141
+    console.warn(
142
+      "react-native-view-shot: result=tmpfile is recommended for captureMode=" +
143
+        props.captureMode
144
+    );
145
+  }
146
+}
147
+
148
+export default class ViewShot extends Component {
149
+  static captureRef = captureRef;
150
+  static releaseCapture = releaseCapture;
151
+  props: Props;
152
+  root: ?View;
153
+
154
+  _raf: *;
155
+  lastCapturedURI: ?string;
156
+
157
+  resolveFirstLayout: (layout: Object) => void;
158
+  firstLayoutPromise = new Promise(resolve => {
159
+    this.resolveFirstLayout = resolve;
160
+  });
161
+
162
+  capture = (): Promise<string> =>
163
+    this.firstLayoutPromise
164
+      .then(() => {
165
+        const { root } = this;
166
+        if (!root) return neverEndingPromise; // component is unmounted, you never want to hear back from the promise
167
+        return captureRef(root, this.props.options);
168
+      })
169
+      .then(
170
+        (uri: string) => {
171
+          this.onCapture(uri);
172
+          return uri;
173
+        },
174
+        (e: Error) => {
175
+          this.onCaptureFailure(e);
176
+          throw e;
177
+        }
178
+      );
179
+
180
+  onCapture = (uri: string) => {
181
+    if (!this.root) return;
182
+    if (this.lastCapturedURI) {
183
+      // schedule releasing the previous capture
184
+      setTimeout(releaseCapture, 500, this.lastCapturedURI);
185
+    }
186
+    this.lastCapturedURI = uri;
187
+    const { onCapture } = this.props;
188
+    if (onCapture) onCapture(uri);
189
+  };
190
+
191
+  onCaptureFailure = (e: Error) => {
192
+    if (!this.root) return;
193
+    const { onCaptureFailure } = this.props;
194
+    if (onCaptureFailure) onCaptureFailure(e);
195
+  };
196
+
197
+  syncCaptureLoop = (captureMode: ?string) => {
198
+    cancelAnimationFrame(this._raf);
199
+    if (captureMode === "continuous") {
200
+      let previousCaptureURI = "-"; // needs to capture at least once at first, so we use "-" arbitrary string
201
+      const loop = () => {
202
+        this._raf = requestAnimationFrame(loop);
203
+        if (previousCaptureURI === this.lastCapturedURI) return; // previous capture has not finished, don't capture yet
204
+        previousCaptureURI = this.lastCapturedURI;
205
+        this.capture();
206
+      };
207
+      this._raf = requestAnimationFrame(loop);
208
+    }
209
+  };
210
+
211
+  onRef = (ref: View) => {
212
+    this.root = ref;
213
+  };
214
+
215
+  onLayout = (e: { nativeEvent: { layout: Object } }) => {
216
+    const { onLayout } = this.props;
217
+    this.resolveFirstLayout(e.nativeEvent.layout);
218
+    if (onLayout) onLayout(e);
219
+  };
220
+
221
+  componentDidMount() {
222
+    if (__DEV__) checkCompatibleProps(this.props);
223
+    if (this.props.captureMode === "mount") {
224
+      this.capture();
225
+    } else {
226
+      this.syncCaptureLoop(this.props.captureMode);
227
+    }
228
+  }
229
+
230
+  componentWillReceiveProps(nextProps: Props) {
231
+    if (nextProps.captureMode !== this.props.captureMode) {
232
+      this.syncCaptureLoop(nextProps.captureMode);
233
+    }
234
+  }
235
+
236
+  componentDidUpdate() {
237
+    if (this.props.captureMode === "update") {
238
+      this.capture();
239
+    }
240
+  }
241
+
242
+  componentWillUnmount() {
243
+    this.syncCaptureLoop(null);
244
+  }
245
+
246
+  render() {
247
+    const { children } = this.props;
248
+    return (
249
+      <View ref={this.onRef} collapsable={false} onLayout={this.onLayout}>
250
+        {children}
251
+      </View>
252
+    );
253
+  }
254
+}

+ 6
- 1
windows/RNViewShot/RNViewShotModule.cs View File

@@ -35,7 +35,12 @@ namespace RNViewShot
35 35
         }
36 36
 
37 37
         [ReactMethod]
38
-        public void takeSnapshot(int tag, JObject options, IPromise promise)
38
+        public void releaseCapture (string uri) {
39
+          // TODO implement me
40
+        }
41
+
42
+        [ReactMethod]
43
+        public void captureRef(int tag, JObject options, IPromise promise)
39 44
         {
40 45
             string format = options["format"] != null ? options.Value<string>("format") : "png";
41 46
             double quality = options["quality"] != null ? options.Value<double>("quality") : 1.0;