Browse Source

Fixes <ViewShot>

Gaëtan Renaudeau 4 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,4 +1,3 @@
1
-
2 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 3
 Capture a React Native view to an image.
@@ -9,11 +8,22 @@ Capture a React Native view to an image.
9 8
 
10 9
 ```bash
11 10
 yarn add react-native-view-shot
12
-react-native link react-native-view-shot
13 11
 ```
14 12
 
15 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 27
 ## Recommended High Level API
18 28
 
19 29
 ```js
@@ -81,6 +91,7 @@ class ExampleCaptureScrollViewContent extends Component {
81 91
   }
82 92
 }
83 93
 ```
94
+
84 95
 **Props:**
85 96
 
86 97
 - **`children`**: the actual content to rasterize.
@@ -101,8 +112,7 @@ import { captureRef } from "react-native-view-shot";
101 112
 captureRef(viewRef, {
102 113
   format: "jpg",
103 114
   quality: 0.8
104
-})
105
-.then(
115
+}).then(
106 116
   uri => console.log("Image saved to", uri),
107 117
   error => console.error("Oops, snapshot failed", error)
108 118
 );
@@ -112,14 +122,14 @@ Returns a Promise of the image URI.
112 122
 
113 123
 - **`view`** is a reference to a React Native component.
114 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 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 134
 ## `releaseCapture(uri)`
125 135
 
@@ -135,14 +145,13 @@ import { captureScreen } from "react-native-view-shot";
135 145
 captureScreen({
136 146
   format: "jpg",
137 147
   quality: 0.8
138
-})
139
-.then(
148
+}).then(
140 149
   uri => console.log("Image saved to", uri),
141 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 156
 Returns a Promise of the image URI.
148 157
 
@@ -158,17 +167,18 @@ Returns a Promise of the image URI.
158 167
 
159 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 182
 1. Only supported by wrapping a `<View collapsable={false}>` parent and snapshotting it.
173 183
 2. It returns an empty image (not a failure Promise).
174 184
 3. Component itself lacks platform support.
@@ -177,11 +187,13 @@ Model tested: iPhone 6 (iOS), Nexus 5 (Android).
177 187
 ## Performance Optimization
178 188
 
179 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 195
 To solve that in code introduced several new approaches:
196
+
185 197
 - reusable images, that reduce load on GC;
186 198
 - reusable arrays/buffers that also reduce load on GC;
187 199
 - RAW image format for avoiding expensive compression;
@@ -194,6 +206,7 @@ more details and code snippet are below.
194 206
 Introduced a new image format RAW. it correspond a ARGB array of pixels.
195 207
 
196 208
 Advantages:
209
+
197 210
 - no compression, so its supper quick. Screenshot taking is less than 16ms;
198 211
 
199 212
 RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
@@ -209,32 +222,32 @@ approach for capturing screen views and deliver them to the react side.
209 222
 ### How to work with zip-base64 and RAW format?
210 223
 
211 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 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 253
 Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
@@ -244,6 +257,7 @@ Hint: use `process.fork()` approach for converting raw data into PNGs.
244 257
 > Note: code is tested in large commercial project.
245 258
 
246 259
 > Note #2: Don't forget to add packages into your project:
260
+>
247 261
 > ```js
248 262
 > yarn add pngjs
249 263
 > yarn add zlib
@@ -285,7 +299,6 @@ Alternatively, you can use the `ViewShot` component that will wait the first `on
285 299
 
286 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 304
 ## Thanks

+ 0
- 18
RNViewShot.podspec View File

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

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

@@ -0,0 +1,20 @@
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,9 +3,9 @@ import React, { Component } from "react";
3 3
 import { View, NativeModules, Platform, findNodeHandle } from "react-native";
4 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 10
 const neverEndingPromise = new Promise(() => {});
11 11
 
@@ -20,7 +20,7 @@ type Options = {
20 20
 
21 21
 if (!RNViewShot) {
22 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,31 +76,51 @@ function validateOptions(
76 76
   if (acceptedFormats.indexOf(options.format) === -1) {
77 77
     options.format = defaultOptions.format;
78 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 85
   if (acceptedResults.indexOf(options.result) === -1) {
83 86
     options.result = defaultOptions.result;
84 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 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 105
 export function captureRef<T: ElementType>(
92 106
   view: number | ?View | Ref<T>,
93 107
   optionsObject?: Object
94 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 112
     view = view.current;
113
+    if (!view) {
114
+      return Promise.reject(new Error("ref.current is null"));
115
+    }
97 116
   }
98 117
   if (typeof view !== "number") {
99 118
     const node = findNodeHandle(view);
100
-    if (!node)
119
+    if (!node) {
101 120
       return Promise.reject(
102 121
         new Error("findNodeHandle failed to resolve view=" + String(view))
103 122
       );
123
+    }
104 124
     view = node;
105 125
   }
106 126
   const { options, errors } = validateOptions(optionsObject);
@@ -123,9 +143,8 @@ export function releaseCapture(uri: string): void {
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 148
   const { options, errors } = validateOptions(optionsObject);
130 149
   if (__DEV__ && errors.length > 0) {
131 150
     console.warn(
@@ -172,8 +191,8 @@ export default class ViewShot extends Component<Props> {
172 191
   static captureRef = captureRef;
173 192
   static releaseCapture = releaseCapture;
174 193
   constructor(props) {
175
-    super(props)
176
-    this.state={}
194
+    super(props);
195
+    this.state = {};
177 196
   }
178 197
   root: ?View;
179 198
 
@@ -253,16 +272,12 @@ export default class ViewShot extends Component<Props> {
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 281
     if (this.props.captureMode === "update") {
267 282
       this.capture();
268 283
     }
@@ -275,7 +290,12 @@ export default class ViewShot extends Component<Props> {
275 290
   render() {
276 291
     const { children } = this.props;
277 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 299
         {children}
280 300
       </View>
281 301
     );

+ 4
- 0
yarn.lock View File

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