Browse Source

Fixes <ViewShot>

Gaëtan Renaudeau 5 years ago
parent
commit
c12e8cd95a
No account linked to committer's email address
6 changed files with 130 additions and 90 deletions
  1. 63
    50
      README.md
  2. 0
    18
      RNViewShot.podspec
  3. 1
    0
      package.json
  4. 20
    0
      react-native-view-shot.podspec
  5. 42
    22
      src/index.js
  6. 4
    0
      yarn.lock

+ 63
- 50
README.md View File

1
-
2
 # react-native-view-shot ![](https://img.shields.io/npm/v/react-native-view-shot.svg) ![](https://img.shields.io/badge/react--native-%2040+-05F561.svg)
1
 # react-native-view-shot ![](https://img.shields.io/npm/v/react-native-view-shot.svg) ![](https://img.shields.io/badge/react--native-%2040+-05F561.svg)
3
 
2
 
4
 Capture a React Native view to an image.
3
 Capture a React Native view to an image.
9
 
8
 
10
 ```bash
9
 ```bash
11
 yarn add react-native-view-shot
10
 yarn add react-native-view-shot
12
-react-native link react-native-view-shot
13
 ```
11
 ```
14
 
12
 
15
 Make sure react-native-view-shot is correctly linked in XCode (might require a manual installation, refer to [React Native doc](https://facebook.github.io/react-native/docs/linking-libraries-ios.html)).
13
 Make sure react-native-view-shot is correctly linked in XCode (might require a manual installation, refer to [React Native doc](https://facebook.github.io/react-native/docs/linking-libraries-ios.html)).
16
 
14
 
15
+**Before React Native 0.60.x you would have to:**
16
+
17
+```bash
18
+react-native link react-native-view-shot
19
+```
20
+
21
+**Since 0.60.x, [autolink](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) should just work**, On iOS, you might have to:
22
+
23
+```bash
24
+cd ios && pod install && cd ..
25
+```
26
+
17
 ## Recommended High Level API
27
 ## Recommended High Level API
18
 
28
 
19
 ```js
29
 ```js
81
   }
91
   }
82
 }
92
 }
83
 ```
93
 ```
94
+
84
 **Props:**
95
 **Props:**
85
 
96
 
86
 - **`children`**: the actual content to rasterize.
97
 - **`children`**: the actual content to rasterize.
101
 captureRef(viewRef, {
112
 captureRef(viewRef, {
102
   format: "jpg",
113
   format: "jpg",
103
   quality: 0.8
114
   quality: 0.8
104
-})
105
-.then(
115
+}).then(
106
   uri => console.log("Image saved to", uri),
116
   uri => console.log("Image saved to", uri),
107
   error => console.error("Oops, snapshot failed", error)
117
   error => console.error("Oops, snapshot failed", error)
108
 );
118
 );
112
 
122
 
113
 - **`view`** is a reference to a React Native component.
123
 - **`view`** is a reference to a React Native component.
114
 - **`options`** may include:
124
 - **`options`** may include:
115
-  - **`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).
116
-  - **`format`** *(string)*: either `png` or `jpg` or `webm` (Android). Defaults to `png`.
117
-  - **`quality`** *(number)*: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
118
-  - **`result`** *(string)*, the method you want to use to save the snapshot, one of:
119
-    - `"tmpfile"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
120
-    - `"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*.
125
+  - **`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).
126
+  - **`format`** _(string)_: either `png` or `jpg` or `webm` (Android). Defaults to `png`.
127
+  - **`quality`** _(number)_: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
128
+  - **`result`** _(string)_, the method you want to use to save the snapshot, one of:
129
+    - `"tmpfile"` (default): save to a temporary file _(that will only exist for as long as the app is running)_.
130
+    - `"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_.
121
     - `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
131
     - `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
122
-  - **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height.
132
+  - **`snapshotContentContainer`** _(bool)_: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height.
123
 
133
 
124
 ## `releaseCapture(uri)`
134
 ## `releaseCapture(uri)`
125
 
135
 
135
 captureScreen({
145
 captureScreen({
136
   format: "jpg",
146
   format: "jpg",
137
   quality: 0.8
147
   quality: 0.8
138
-})
139
-.then(
148
+}).then(
140
   uri => console.log("Image saved to", uri),
149
   uri => console.log("Image saved to", uri),
141
   error => console.error("Oops, snapshot failed", error)
150
   error => console.error("Oops, snapshot failed", error)
142
 );
151
 );
143
 ```
152
 ```
144
 
153
 
145
-This method will capture the contents of the currently displayed screen as a native hardware screenshot. It does not require a ref input, as it does not work at the view level. This means that ScrollViews will not be captured in their entirety - only the portions currently visible to the user. 
154
+This method will capture the contents of the currently displayed screen as a native hardware screenshot. It does not require a ref input, as it does not work at the view level. This means that ScrollViews will not be captured in their entirety - only the portions currently visible to the user.
146
 
155
 
147
 Returns a Promise of the image URI.
156
 Returns a Promise of the image URI.
148
 
157
 
158
 
167
 
159
 Model tested: iPhone 6 (iOS), Nexus 5 (Android).
168
 Model tested: iPhone 6 (iOS), Nexus 5 (Android).
160
 
169
 
161
-| System             | iOS                | Android           | Windows           |
162
-|--------------------|--------------------|-------------------|-------------------|
163
-| View,Text,Image,.. | YES                | YES               | YES               |                    
164
-| WebView            | YES                | YES<sup>1</sup>   | YES               |
165
-| gl-react v2        | YES                | NO<sup>2</sup>    | NO<sup>3</sup>    |
166
-| react-native-video | NO                 | NO                | NO                |
167
-| react-native-maps  | YES                | NO<sup>4</sup>    | NO<sup>3</sup>    |
168
-| react-native-svg   | YES                | YES               | maybe?            |
169
-| react-native-camera   | NO                | YES               | NO <sup>3</sup>        |
170
+| System                | iOS              | Android           | Windows                |
171
+| --------------------- | ---------------- | ----------------- | ---------------------- |
172
+| View,Text,Image,..    | YES              | YES               | YES                    |
173
+| WebView               | YES              | YES<sup>1</sup>   | YES                    |
174
+| gl-react v2           | YES              | NO<sup>2</sup>    | NO<sup>3</sup>         |
175
+| react-native-video    | NO               | NO                | NO                     |
176
+| react-native-maps     | YES              | NO<sup>4</sup>    | NO<sup>3</sup>         |
177
+| react-native-svg      | YES              | YES               | maybe?                 |
178
+| react-native-camera   | NO               | YES               | NO <sup>3</sup>        |
170
 
179
 
171
 >
180
 >
181
+
172
 1. Only supported by wrapping a `<View collapsable={false}>` parent and snapshotting it.
182
 1. Only supported by wrapping a `<View collapsable={false}>` parent and snapshotting it.
173
 2. It returns an empty image (not a failure Promise).
183
 2. It returns an empty image (not a failure Promise).
174
 3. Component itself lacks platform support.
184
 3. Component itself lacks platform support.
177
 ## Performance Optimization
187
 ## Performance Optimization
178
 
188
 
179
 During profiling captured several things that influence on performance:
189
 During profiling captured several things that influence on performance:
180
-1) (de-)allocation of memory for bitmap
181
-2) (de-)allocation of memory for Base64 output buffer
182
-3) compression of bitmap to different image formats: PNG, JPG
190
+
191
+1. (de-)allocation of memory for bitmap
192
+2. (de-)allocation of memory for Base64 output buffer
193
+3. compression of bitmap to different image formats: PNG, JPG
183
 
194
 
184
 To solve that in code introduced several new approaches:
195
 To solve that in code introduced several new approaches:
196
+
185
 - reusable images, that reduce load on GC;
197
 - reusable images, that reduce load on GC;
186
 - reusable arrays/buffers that also reduce load on GC;
198
 - reusable arrays/buffers that also reduce load on GC;
187
 - RAW image format for avoiding expensive compression;
199
 - RAW image format for avoiding expensive compression;
194
 Introduced a new image format RAW. it correspond a ARGB array of pixels.
206
 Introduced a new image format RAW. it correspond a ARGB array of pixels.
195
 
207
 
196
 Advantages:
208
 Advantages:
209
+
197
 - no compression, so its supper quick. Screenshot taking is less than 16ms;
210
 - no compression, so its supper quick. Screenshot taking is less than 16ms;
198
 
211
 
199
 RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
212
 RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
209
 ### How to work with zip-base64 and RAW format?
222
 ### How to work with zip-base64 and RAW format?
210
 
223
 
211
 ```js
224
 ```js
212
-const fs = require('fs')
213
-const zlib = require('zlib')
214
-const PNG = require('pngjs').PNG
215
-const Buffer = require('buffer').Buffer
225
+const fs = require("fs");
226
+const zlib = require("zlib");
227
+const PNG = require("pngjs").PNG;
228
+const Buffer = require("buffer").Buffer;
216
 
229
 
217
-const format = Platform.OS === 'android' ? 'raw' : 'png'
218
-const result = Platform.OS === 'android' ? 'zip-base64' : 'base64'
230
+const format = Platform.OS === "android" ? "raw" : "png";
231
+const result = Platform.OS === "android" ? "zip-base64" : "base64";
219
 
232
 
220
 captureRef(this.ref, { result, format }).then(data => {
233
 captureRef(this.ref, { result, format }).then(data => {
221
-    // expected pattern 'width:height|', example: '1080:1731|'
222
-    const resolution = /^(\d+):(\d+)\|/g.exec(data)
223
-    const width = (resolution || ['', 0, 0])[1]
224
-    const height = (resolution || ['', 0, 0])[2]
225
-    const base64 = data.substr((resolution || [''])[0].length || 0)
226
-
227
-    // convert from base64 to Buffer
228
-    const buffer = Buffer.from(base64, 'base64')
229
-    // un-compress data
230
-    const inflated = zlib.inflateSync(buffer)
231
-    // compose PNG
232
-    const png = new PNG({ width, height })
233
-    png.data = inflated
234
-    const pngData = PNG.sync.write(png)
235
-    // save composed PNG
236
-    fs.writeFileSync(output, pngData)
237
-})
234
+  // expected pattern 'width:height|', example: '1080:1731|'
235
+  const resolution = /^(\d+):(\d+)\|/g.exec(data);
236
+  const width = (resolution || ["", 0, 0])[1];
237
+  const height = (resolution || ["", 0, 0])[2];
238
+  const base64 = data.substr((resolution || [""])[0].length || 0);
239
+
240
+  // convert from base64 to Buffer
241
+  const buffer = Buffer.from(base64, "base64");
242
+  // un-compress data
243
+  const inflated = zlib.inflateSync(buffer);
244
+  // compose PNG
245
+  const png = new PNG({ width, height });
246
+  png.data = inflated;
247
+  const pngData = PNG.sync.write(png);
248
+  // save composed PNG
249
+  fs.writeFileSync(output, pngData);
250
+});
238
 ```
251
 ```
239
 
252
 
240
 Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
253
 Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
244
 > Note: code is tested in large commercial project.
257
 > Note: code is tested in large commercial project.
245
 
258
 
246
 > Note #2: Don't forget to add packages into your project:
259
 > Note #2: Don't forget to add packages into your project:
260
+>
247
 > ```js
261
 > ```js
248
 > yarn add pngjs
262
 > yarn add pngjs
249
 > yarn add zlib
263
 > yarn add zlib
285
 
299
 
286
 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)
300
 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)
287
 
301
 
288
-
289
 ---
302
 ---
290
 
303
 
291
 ## Thanks
304
 ## Thanks

+ 0
- 18
RNViewShot.podspec View File

1
-require 'json'
2
-version = JSON.parse(File.read('package.json'))["version"]
3
-
4
-Pod::Spec.new do |s|
5
-
6
-  s.name           = "RNViewShot"
7
-  s.version        = version
8
-  s.summary        = "Capture a React Native view to an image"
9
-  s.homepage       = "https://github.com/gre/react-native-view-shot"
10
-  s.license        = "MIT"
11
-  s.author         = { "Gaëtan Renaudeau" => "renaudeau.gaetan@gmail.com" }
12
-  s.platform       = :ios, "7.0"
13
-  s.source         = { :git => "https://github.com/gre/react-native-view-shot.git", :tag => "v#{s.version}" }
14
-  s.source_files   = 'ios/*.{h,m}'
15
-  s.preserve_paths = "**/*.js"
16
-  s.dependency 'React'
17
-
18
-end

+ 1
- 0
package.json View File

13
     "rasterize"
13
     "rasterize"
14
   ],
14
   ],
15
   "author": "Gaëtan Renaudeau <renaudeau.gaetan@gmail.com>",
15
   "author": "Gaëtan Renaudeau <renaudeau.gaetan@gmail.com>",
16
+  "homepage": "https://github.com/gre/react-native-view-shot",
16
   "license": "MIT",
17
   "license": "MIT",
17
   "repository": {
18
   "repository": {
18
     "type": "git",
19
     "type": "git",

+ 20
- 0
react-native-view-shot.podspec View File

1
+require 'json'
2
+
3
+package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+Pod::Spec.new do |s|
6
+  s.name         = package['name']
7
+  s.version      = package['version']
8
+  s.summary      = package['description']
9
+  s.license      = package['license']
10
+
11
+  s.authors      = package['author']
12
+  s.homepage     = package['homepage']
13
+  s.platform     = :ios, "9.0"
14
+
15
+  s.source       = { :git => "https://github.com/gre/react-native-view-shot.git", :tag => "v#{s.version}" }
16
+  s.source_files  = "ios/**/*.{h,m}"
17
+
18
+  s.dependency 'React'
19
+end
20
+

+ 42
- 22
src/index.js View File

3
 import { View, NativeModules, Platform, findNodeHandle } from "react-native";
3
 import { View, NativeModules, Platform, findNodeHandle } from "react-native";
4
 const { RNViewShot } = NativeModules;
4
 const { RNViewShot } = NativeModules;
5
 
5
 
6
-import type { Element, ElementRef, ElementType, Ref } from 'react';
7
-import type { ViewStyleProp } from 'StyleSheet';
8
-import type { LayoutEvent } from 'CoreEventTypes';
6
+import type { Element, ElementRef, ElementType, Ref } from "react";
7
+import type { ViewStyleProp } from "StyleSheet";
8
+import type { LayoutEvent } from "CoreEventTypes";
9
 
9
 
10
 const neverEndingPromise = new Promise(() => {});
10
 const neverEndingPromise = new Promise(() => {});
11
 
11
 
20
 
20
 
21
 if (!RNViewShot) {
21
 if (!RNViewShot) {
22
   console.warn(
22
   console.warn(
23
-    "NativeModules.RNViewShot is undefined. Make sure the library is linked on the native side."
23
+    "react-native-view-shot: NativeModules.RNViewShot is undefined. Make sure the library is linked on the native side."
24
   );
24
   );
25
 }
25
 }
26
 
26
 
76
   if (acceptedFormats.indexOf(options.format) === -1) {
76
   if (acceptedFormats.indexOf(options.format) === -1) {
77
     options.format = defaultOptions.format;
77
     options.format = defaultOptions.format;
78
     errors.push(
78
     errors.push(
79
-      "option format '" + options.format + "' is not in valid formats: " + acceptedFormats.join(" | ")
79
+      "option format '" +
80
+        options.format +
81
+        "' is not in valid formats: " +
82
+        acceptedFormats.join(" | ")
80
     );
83
     );
81
   }
84
   }
82
   if (acceptedResults.indexOf(options.result) === -1) {
85
   if (acceptedResults.indexOf(options.result) === -1) {
83
     options.result = defaultOptions.result;
86
     options.result = defaultOptions.result;
84
     errors.push(
87
     errors.push(
85
-      "option result '" + options.result  + "' is not in valid formats: " + acceptedResults.join(" | ")
88
+      "option result '" +
89
+        options.result +
90
+        "' is not in valid formats: " +
91
+        acceptedResults.join(" | ")
86
     );
92
     );
87
   }
93
   }
88
   return { options, errors };
94
   return { options, errors };
89
 }
95
 }
90
 
96
 
97
+export function ensureModuleIsLoaded() {
98
+  if (!RNViewShot) {
99
+    throw new Error(
100
+      "react-native-view-shot: NativeModules.RNViewShot is undefined. Make sure the library is linked on the native side."
101
+    );
102
+  }
103
+}
104
+
91
 export function captureRef<T: ElementType>(
105
 export function captureRef<T: ElementType>(
92
   view: number | ?View | Ref<T>,
106
   view: number | ?View | Ref<T>,
93
   optionsObject?: Object
107
   optionsObject?: Object
94
 ): Promise<string> {
108
 ): Promise<string> {
95
-  if (view && typeof view === "object" && "current" in view && view.current) { // React.RefObject
109
+  ensureModuleIsLoaded();
110
+  if (view && typeof view === "object" && "current" in view && view.current) {
111
+    // React.RefObject
96
     view = view.current;
112
     view = view.current;
113
+    if (!view) {
114
+      return Promise.reject(new Error("ref.current is null"));
115
+    }
97
   }
116
   }
98
   if (typeof view !== "number") {
117
   if (typeof view !== "number") {
99
     const node = findNodeHandle(view);
118
     const node = findNodeHandle(view);
100
-    if (!node)
119
+    if (!node) {
101
       return Promise.reject(
120
       return Promise.reject(
102
         new Error("findNodeHandle failed to resolve view=" + String(view))
121
         new Error("findNodeHandle failed to resolve view=" + String(view))
103
       );
122
       );
123
+    }
104
     view = node;
124
     view = node;
105
   }
125
   }
106
   const { options, errors } = validateOptions(optionsObject);
126
   const { options, errors } = validateOptions(optionsObject);
123
   }
143
   }
124
 }
144
 }
125
 
145
 
126
-export function captureScreen(
127
-  optionsObject?: Options
128
-): Promise<string> {
146
+export function captureScreen(optionsObject?: Options): Promise<string> {
147
+  ensureModuleIsLoaded();
129
   const { options, errors } = validateOptions(optionsObject);
148
   const { options, errors } = validateOptions(optionsObject);
130
   if (__DEV__ && errors.length > 0) {
149
   if (__DEV__ && errors.length > 0) {
131
     console.warn(
150
     console.warn(
172
   static captureRef = captureRef;
191
   static captureRef = captureRef;
173
   static releaseCapture = releaseCapture;
192
   static releaseCapture = releaseCapture;
174
   constructor(props) {
193
   constructor(props) {
175
-    super(props)
176
-    this.state={}
194
+    super(props);
195
+    this.state = {};
177
   }
196
   }
178
   root: ?View;
197
   root: ?View;
179
 
198
 
253
     }
272
     }
254
   }
273
   }
255
 
274
 
256
-  static getDerivedStateFromProps(props, state) {
257
-    if(props.captureMode !== undefined) {
258
-      if (props.captureMode !== this.props.captureMode) {
259
-        this.syncCaptureLoop(props.captureMode);
275
+  componentDidUpdate(prevProps) {
276
+    if (this.props.captureMode !== undefined) {
277
+      if (this.props.captureMode !== prevProps.captureMode) {
278
+        this.syncCaptureLoop(this.props.captureMode);
260
       }
279
       }
261
     }
280
     }
262
-    return null;
263
-  }
264
-
265
-  componentDidUpdate() {
266
     if (this.props.captureMode === "update") {
281
     if (this.props.captureMode === "update") {
267
       this.capture();
282
       this.capture();
268
     }
283
     }
275
   render() {
290
   render() {
276
     const { children } = this.props;
291
     const { children } = this.props;
277
     return (
292
     return (
278
-      <View ref={this.onRef} collapsable={false} onLayout={this.onLayout} style={this.props.style}>
293
+      <View
294
+        ref={this.onRef}
295
+        collapsable={false}
296
+        onLayout={this.onLayout}
297
+        style={this.props.style}
298
+      >
279
         {children}
299
         {children}
280
       </View>
300
       </View>
281
     );
301
     );

+ 4
- 0
yarn.lock View File

1
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+# yarn lockfile v1
3
+
4
+