Skip to content

Commit fe74b43

Browse files
committed
drop deprecated, introduce ViewShot component, rename APIs
1 parent 3494679 commit fe74b43

File tree

8 files changed

+424
-217
lines changed

8 files changed

+424
-217
lines changed

README.md

Lines changed: 108 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,84 @@ Capture a React Native view to an image.
55

66
<img src="https://github.com/gre/react-native-view-shot-example/raw/master/docs/recursive.gif" width=300 />
77

8-
> iOS: For React Native version between `0.30.x` and `0.39.x`, you should use `[email protected]`.
8+
## Install
99

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
1180

1281
```js
13-
import { takeSnapshot } from "react-native-view-shot";
82+
import { captureRef } from "react-native-view-shot";
1483

15-
takeSnapshot(viewRef, {
16-
format: "jpeg",
84+
captureRef(viewRef, {
85+
format: "jpg",
1786
quality: 0.8
1887
})
1988
.then(
@@ -22,39 +91,33 @@ takeSnapshot(viewRef, {
2291
);
2392
```
2493

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-
3394
Returns a Promise of the image URI.
3495

3596
- **`view`** is a reference to a React Native component.
3697
- **`options`** may include:
3798
- **`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)
40101
- **`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)*.
42103
- `"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*.
43104
- `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
44105
- **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height.
45106

46-
### DEPRECATED `path` option and `dirs` constants
107+
## `releaseCapture(uri)`
47108

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.
49110

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).
51112

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)
55116

56117
## Interoperability Table
57118

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+
58121
Model tested: iPhone 6 (iOS), Nexus 5 (Android).
59122

60123
| System | iOS | Android | Windows |
@@ -71,60 +134,44 @@ Model tested: iPhone 6 (iOS), Nexus 5 (Android).
71134
3. Component itself lacks platform support.
72135
4. But you can just use the react-native-maps snapshot function: https://github.com/airbnb/react-native-maps#take-snapshot-of-map
73136

74-
## Caveats
137+
## Troubleshooting / FAQ
75138

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?
77140

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.
80144

81-
### specific to Android implementation
145+
### The snapshot is rejected with an error?
82146

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).
85148

86-
## Getting started
149+
### get a black or blank result or still have an error with simple views?
87150

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.
91152

92-
### Mostly automatic installation
153+
### black background instead of transparency / weird border appear around texts?
93154

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.
97162

98-
### Manual installation
163+
### Getting "The content size must not be zero or negative."
99164

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.
101166
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`.
106168

107-
#### Android
169+
### Snapshotted image does not match my width and height but is twice/3-times bigger
108170

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)
121172

122-
#### Windows
123173

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+
---
128175

129176
## Thanks
130177

android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java

Lines changed: 25 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import android.content.Context;
55
import android.graphics.Bitmap;
6+
import android.net.Uri;
67
import android.os.AsyncTask;
78
import android.os.Environment;
89
import android.util.DisplayMetrics;
@@ -23,6 +24,7 @@
2324
import java.io.File;
2425
import java.io.FilenameFilter;
2526
import java.io.IOException;
27+
import java.util.Collections;
2628
import java.util.HashMap;
2729
import java.util.Map;
2830

@@ -42,7 +44,7 @@ public String getName() {
4244

4345
@Override
4446
public Map<String, Object> getConstants() {
45-
return getSystemFolders(this.getReactApplicationContext());
47+
return Collections.emptyMap();
4648
}
4749

4850
@Override
@@ -52,41 +54,35 @@ public void onCatalystInstanceDestroy() {
5254
}
5355

5456
@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) {
5668
ReactApplicationContext context = getReactApplicationContext();
57-
String format = options.hasKey("format") ? options.getString("format") : "png";
69+
String format = options.getString("format");
5870
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");
7177
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
7278
Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
7379
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");
7682
try {
7783
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);
9086
}
9187
UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
9288
uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer,reactContext, promise));
@@ -136,25 +132,6 @@ public boolean accept(File dir, String filename) {
136132
}
137133
}
138134

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-
158135
/**
159136
* Create a temporary file in the cache directory on either internal or external storage,
160137
* whichever is available and has more free space.

android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,10 @@ public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
7979
return;
8080
}
8181
try {
82-
if ("file".equals(result) || "tmpfile".equals(result)) {
82+
if ("tmpfile".equals(result)) {
8383
os = new FileOutputStream(output);
8484
captureView(view, os);
8585
String uri = Uri.fromFile(output).toString();
86-
reactContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse(uri)));
8786
promise.resolve(uri);
8887
}
8988
else if ("base64".equals(result)) {
@@ -101,9 +100,6 @@ else if ("data-uri".equals(result)) {
101100
data = "data:image/"+extension+";base64," + data;
102101
promise.resolve(data);
103102
}
104-
else {
105-
promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Unsupported result: "+result+". Try one of: file | base64 | data-uri");
106-
}
107103
}
108104
catch (Exception e) {
109105
e.printStackTrace();

0 commit comments

Comments
 (0)