Browse Source

Fix conflicts and update view support table

Ryan Linton 7 years ago
parent
commit
d5b53d65e2

+ 44
- 11
README.md View File

@@ -10,9 +10,9 @@ Snapshot a React Native view and save it to an image.
10 10
 ## Usage
11 11
 
12 12
 ```js
13
-import RNViewShot from "react-native-view-shot";
13
+import { takeSnapshot } from "react-native-view-shot";
14 14
 
15
-RNViewShot.takeSnapshot(viewRef, {
15
+takeSnapshot(viewRef, {
16 16
   format: "jpeg",
17 17
   quality: 0.8
18 18
 })
@@ -28,34 +28,68 @@ RNViewShot.takeSnapshot(viewRef, {
28 28
 
29 29
 ## Full API
30 30
 
31
-### `RNViewShot.takeSnapshot(view, options)`
31
+### `takeSnapshot(view, options)`
32 32
 
33 33
 Returns a Promise of the image URI.
34 34
 
35 35
 - **`view`** is a reference to a React Native component.
36 36
 - **`options`** may include:
37
- - **`width`** / **`height`** *(number)*: the width and height of the image to capture.
37
+ - **`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 38
  - **`format`** *(string)*: either `png` or `jpg`/`jpeg` or `webm` (Android). Defaults to `png`.
39 39
  - **`quality`** *(number)*: the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpeg)
40 40
  - **`result`** *(string)*, the method you want to use to save the snapshot, one of:
41 41
     - `"file"` (default): save to a temporary file *(that will only exist for as long as the app is running)*.
42 42
     - `"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 43
     - `"data-uri"`: same as `base64` but also includes the [Data URI scheme](https://en.wikipedia.org/wiki/Data_URI_scheme) header.
44
- - **`filename`** *(string)*: the name of the generated file if any (Android only). Defaults to `ReactNative_snapshot_image_${timestamp}`.
45
- - **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height. (Android only)
44
+ - **`path`** *(string)*: The absolute path where the file get generated. See *`dirs` constants* for more information.
45
+ - **`snapshotContentContainer`** *(bool)*: if true and when view is a ScrollView, the "content container" height will be evaluated instead of the container height.
46
+
47
+### `dirs` constants
48
+
49
+By default, takeSnapshot will export in a temporary folder and the snapshot file will be deleted as soon as the app leaves. If you use the `path` option, you make the snapshot file more permanent and at a specific file location. To make file location more 'universal', the library exports some classic directory constants:
50
+
51
+```js
52
+import { takeSnapshot, dirs } from "react-native-view-shot";
53
+// cross platform dirs:
54
+const { CacheDir, DocumentDir, MainBundleDir, MovieDir, MusicDir, PictureDir } = dirs;
55
+// only available Android:
56
+const { DCIMDir, DownloadDir, RingtoneDir, SDCardDir } = dirs;
57
+
58
+takeSnapshot(viewRef, { path: PictureDir+"/foo.png" })
59
+.then(
60
+  uri => console.log("Image saved to", uri),
61
+  error => console.error("Oops, snapshot failed", error)
62
+);
63
+```
64
+
65
+## Supported views
66
+
67
+Model tested: iPhone 6 (iOS), Nexus 5 (Android).
68
+
69
+| System             | iOS                | Android           | Windows           |
70
+|--------------------|--------------------|-------------------|-------------------|
71
+| View,Text,Image,.. | YES                | YES               | YES               |                    
72
+| WebView            | YES                | YES<sup>1</sup>   | YES               |
73
+| gl-react v2        | YES                | NO<sup>2</sup>    | NO<sup>3</sup>    |
74
+| react-native-video | NO                 | NO                | NO
75
+| react-native-maps  | YES                | [NO](https://github.com/gre/react-native-view-shot/issues/36) | NO<sup>3</sup>
76
+
77
+>
78
+1. Only supported by wrapping a `<View collapsable={false}>` parent and snapshotting it.
79
+2. It returns an empty image (not a failure Promise).
80
+3. Component itself lacks platform support.
46 81
 
47 82
 ## Caveats
48 83
 
49 84
 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.
50 85
 
51
-- Support of special components like Video / GL views remains untested.
86
+- 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).
52 87
 - 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.
53 88
 
54 89
 ### specific to Android implementation
55 90
 
56
-- you need to make sure `collapsable` is set to `false` if you want to snapshot a **View**. Otherwise that view won't reflect any UI View. ([found by @gaguirre](https://github.com/gre/react-native-view-shot/issues/7#issuecomment-245302844))
57
-- if you want to share out the screenshoted file, you will have to copy it somewhere first so it's accessible to an Intent, see comment: https://github.com/gre/react-native-view-shot/issues/11#issuecomment-251080804 .
58
--  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 you can give it to `Image.getSize` for instance.
91
+- 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))
92
+-  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`)
59 93
 
60 94
 ## Getting started
61 95
 
@@ -100,7 +134,6 @@ react-native link react-native-view-shot
100 134
 3. In Visual Studio, in the solution explorer, right click on your Application project then select `Add` ➜ `Reference`
101 135
 4. Under the projects tab select `RNViewShot` (UWP) or `RNViewShot.Net46` (WPF)
102 136
 
103
-
104 137
 ## Thanks
105 138
 
106 139
 - To initial iOS work done by @jsierles in https://github.com/jsierles/react-native-view-snapshot

+ 9
- 6
android/build.gradle View File

@@ -25,14 +25,17 @@ android {
25 25
     }
26 26
 }
27 27
 
28
-repositories {
29
-    mavenCentral()
30
-    maven {
31
-        // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
32
-        url "$projectDir/../../../node_modules/react-native/android"
28
+allprojects {
29
+    repositories {
30
+        mavenLocal()
31
+        jcenter()
32
+        maven {
33
+            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
34
+            url "$rootDir/../node_modules/react-native/android"
35
+        }
33 36
     }
34 37
 }
35 38
 
36 39
 dependencies {
37
-    compile "com.facebook.react:react-native:+"  // From node_modules
40
+    compile 'com.facebook.react:react-native:+'
38 41
 }

+ 40
- 11
android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java View File

@@ -4,6 +4,7 @@ package fr.greweb.reactnativeviewshot;
4 4
 import android.content.Context;
5 5
 import android.graphics.Bitmap;
6 6
 import android.os.AsyncTask;
7
+import android.os.Environment;
7 8
 import android.util.DisplayMetrics;
8 9
 import android.view.View;
9 10
 
@@ -22,6 +23,8 @@ import com.facebook.react.uimanager.UIManagerModule;
22 23
 import java.io.File;
23 24
 import java.io.FilenameFilter;
24 25
 import java.io.IOException;
26
+import java.util.HashMap;
27
+import java.util.Map;
25 28
 
26 29
 public class RNViewShotModule extends ReactContextBaseJavaModule {
27 30
 
@@ -37,6 +40,11 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
37 40
         return "RNViewShot";
38 41
     }
39 42
 
43
+    @Override
44
+    public Map<String, Object> getConstants() {
45
+        return getSystemFolders(this.getReactApplicationContext());
46
+    }
47
+
40 48
     @Override
41 49
     public void onCatalystInstanceDestroy() {
42 50
         super.onCatalystInstanceDestroy();
@@ -66,17 +74,25 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
66 74
         String result = options.hasKey("result") ? options.getString("result") : "file";
67 75
         Boolean snapshotContentContainer = options.hasKey("snapshotContentContainer") ? options.getBoolean("snapshotContentContainer") : false;
68 76
         try {
69
-            String name = options.hasKey("filename") ? options.getString("filename") : null;
70
-            File tmpFile = "file".equals(result) ? createTempFile(getReactApplicationContext(), format, name) : null;
77
+            File file = null;
78
+            if ("file".equals(result)) {
79
+                if (options.hasKey("path")) {
80
+                    file = new File(options.getString("path"));
81
+                    file.createNewFile();
82
+                }
83
+                else {
84
+                    file = createTempFile(getReactApplicationContext(), format);
85
+                }
86
+            }
71 87
             UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
72
-            uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, tmpFile, result, snapshotContentContainer, promise));
88
+            uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer, promise));
73 89
         }
74 90
         catch (Exception e) {
75 91
             promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
76 92
         }
77 93
     }
78 94
 
79
-    private static final String TEMP_FILE_PREFIX = "ReactNative_snapshot_image_";
95
+    private static final String TEMP_FILE_PREFIX = "ReactNative-snapshot-image";
80 96
 
81 97
     /**
82 98
      * Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped
@@ -116,11 +132,30 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
116 132
         }
117 133
     }
118 134
 
135
+    static private Map<String, Object> getSystemFolders(ReactApplicationContext ctx) {
136
+        Map<String, Object> res = new HashMap<>();
137
+        res.put("CacheDir", ctx.getCacheDir().getAbsolutePath());
138
+        res.put("DCIMDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath());
139
+        res.put("DocumentDir", ctx.getFilesDir().getAbsolutePath());
140
+        res.put("DownloadDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath());
141
+        res.put("MainBundleDir", ctx.getApplicationInfo().dataDir);
142
+        res.put("MovieDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath());
143
+        res.put("MusicDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
144
+        res.put("PictureDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath());
145
+        res.put("RingtoneDir", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_RINGTONES).getAbsolutePath());
146
+        String state;
147
+        state = Environment.getExternalStorageState();
148
+        if (state.equals(Environment.MEDIA_MOUNTED)) {
149
+            res.put("SDCardDir", Environment.getExternalStorageDirectory().getAbsolutePath());
150
+        }
151
+        return res;
152
+    }
153
+
119 154
     /**
120 155
      * Create a temporary file in the cache directory on either internal or external storage,
121 156
      * whichever is available and has more free space.
122 157
      */
123
-    private File createTempFile(Context context, String ext, String name)
158
+    private File createTempFile(Context context, String ext)
124 159
             throws IOException {
125 160
         File externalCacheDir = context.getExternalCacheDir();
126 161
         File internalCacheDir = context.getCacheDir();
@@ -139,12 +174,6 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
139 174
         }
140 175
         String suffix = "." + ext;
141 176
         File tmpFile = File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
142
-        if (name != null) {
143
-            File renamed = new File(cacheDir, name + suffix);
144
-            tmpFile.renameTo(renamed);
145
-            return renamed;
146
-        }
147
-
148 177
         return tmpFile;
149 178
     }
150 179
 

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

@@ -124,7 +124,7 @@ public class ViewShot implements UIBlock {
124 124
         }
125 125
 
126 126
         //evaluate real height
127
-        if (this.snapshotContentContainer){
127
+        if (snapshotContentContainer) {
128 128
             h=0;
129 129
             ScrollView scrollView = (ScrollView)view;
130 130
             for (int i = 0; i < scrollView.getChildCount(); i++) {

+ 25
- 11
index.js View File

@@ -1,19 +1,33 @@
1 1
 //@flow
2
-
3 2
 import { NativeModules, findNodeHandle } from "react-native";
4
-
5 3
 const { RNViewShot } = NativeModules;
6 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
+
7 20
 export function takeSnapshot(
8 21
   view: number | ReactElement<any>,
9
-  options ?: {
10
-    width ?: number;
11
-    height ?: number;
12
-    filename ?: string;
13
-    format ?: "png" | "jpg" | "jpeg" | "webm";
14
-    quality ?: number;
15
-    result ?: "file" | "base64" | "data-uri";
16
-  }
22
+  options?: {
23
+    width?: number,
24
+    height?: number,
25
+    path?: string,
26
+    format?: "png" | "jpg" | "jpeg" | "webm",
27
+    quality?: number,
28
+    result?: "file" | "base64" | "data-uri",
29
+    snapshotContentContainer?: bool
30
+  } = {}
17 31
 ): Promise<string> {
18 32
   if (typeof view !== "number") {
19 33
     const node = findNodeHandle(view);
@@ -23,4 +37,4 @@ export function takeSnapshot(
23 37
   return RNViewShot.takeSnapshot(view, options);
24 38
 }
25 39
 
26
-export default { takeSnapshot };
40
+export default { takeSnapshot, dirs };

+ 79
- 11
ios/RNViewShot.m View File

@@ -5,6 +5,7 @@
5 5
 #import <React/UIView+React.h>
6 6
 #import <React/RCTUtils.h>
7 7
 #import <React/RCTConvert.h>
8
+#import <React/RCTScrollView.h>
8 9
 #import <React/RCTUIManager.h>
9 10
 #import <React/RCTBridge.h>
10 11
 
@@ -20,6 +21,18 @@ RCT_EXPORT_MODULE()
20 21
   return self.bridge.uiManager.methodQueue;
21 22
 }
22 23
 
24
+- (NSDictionary *)constantsToExport
25
+{
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
+           };
34
+}
35
+
23 36
 // forked from RN implementation
24 37
 // https://github.com/facebook/react-native/blob/f35b372883a76b5666b016131d59268b42f3c40d/React/Modules/RCTUIManager.m#L1367
25 38
 
@@ -42,18 +55,61 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
42 55
     CGSize size = [RCTConvert CGSize:options];
43 56
     NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
44 57
     NSString *result = [RCTConvert NSString:options[@"result"] ?: @"file"];
45
-    
58
+    BOOL snapshotContentContainer = [RCTConvert BOOL:options[@"snapshotContentContainer"] ?: @"false"];
59
+
46 60
     // Capture image
61
+    BOOL success;
62
+    
63
+    UIView* rendered;
64
+    UIScrollView* scrollView;
65
+    if (snapshotContentContainer) {
66
+      if (![view isKindOfClass:[RCTScrollView class]]) {
67
+        reject(RCTErrorUnspecified, [NSString stringWithFormat:@"snapshotContentContainer can only be used on a RCTScrollView. instead got: %@", view], nil);
68
+        return;
69
+      }
70
+      RCTScrollView* rctScrollView = view;
71
+      scrollView = rctScrollView.scrollView;
72
+      rendered = scrollView;
73
+    }
74
+    else {
75
+      rendered = view;
76
+    }
77
+    
78
+    if (size.width < 0.1 || size.height < 0.1) {
79
+      size = snapshotContentContainer ? scrollView.contentSize : view.bounds.size;
80
+    }
47 81
     if (size.width < 0.1 || size.height < 0.1) {
48
-      size = view.bounds.size;
82
+      reject(RCTErrorUnspecified, [NSString stringWithFormat:@"The content size must not be zero or negative. Got: (%g, %g)", size.width, size.height], nil);
83
+      return;
84
+    }
85
+    
86
+    CGPoint savedContentOffset;
87
+    CGRect savedFrame;
88
+    if (snapshotContentContainer) {
89
+      // Save scroll & frame and set it temporarily to the full content size
90
+      savedContentOffset = scrollView.contentOffset;
91
+      savedFrame = scrollView.frame;
92
+      scrollView.contentOffset = CGPointZero;
93
+      scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
49 94
     }
50 95
     UIGraphicsBeginImageContextWithOptions(size, NO, 0);
51
-    BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
96
+    success = [rendered drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
52 97
     UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
53 98
     UIGraphicsEndImageContext();
54 99
     
55
-    if (!success || !image) {
56
-      reject(RCTErrorUnspecified, @"Failed to capture view snapshot", nil);
100
+    if (snapshotContentContainer) {
101
+      // Restore scroll & frame
102
+      scrollView.contentOffset = savedContentOffset;
103
+      scrollView.frame = savedFrame;
104
+    }
105
+    
106
+    if (!success) {
107
+      reject(RCTErrorUnspecified, @"The view cannot be captured. drawViewHierarchyInRect was not successful. This is a potential technical or security limitation.", nil);
108
+      return;
109
+    }
110
+
111
+    if (!image) {
112
+      reject(RCTErrorUnspecified, @"Failed to capture view snapshot. UIGraphicsGetImageFromCurrentImageContext() returned nil!", nil);
57 113
       return;
58 114
     }
59 115
     
@@ -75,10 +131,22 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
75 131
       NSString *res = nil;
76 132
       if ([result isEqualToString:@"file"]) {
77 133
         // Save to a temp file
78
-        NSString *tempFilePath = RCTTempFilePath(format, &error);
79
-        if (tempFilePath) {
80
-          if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) {
81
-            res = tempFilePath;
134
+        NSString *path;
135
+        if (options[@"path"]) {
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;
82 150
           }
83 151
         }
84 152
       }
@@ -95,13 +163,13 @@ RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)target
95 163
         reject(RCTErrorUnspecified, [NSString stringWithFormat:@"Unsupported result: %@. Try one of: file | base64 | data-uri", result], nil);
96 164
         return;
97 165
       }
98
-      if (res != nil) {
166
+      if (res && !error) {
99 167
         resolve(res);
100 168
         return;
101 169
       }
102 170
       
103 171
       // If we reached here, something went wrong
104
-      if (error != nil) reject(RCTErrorUnspecified, error.localizedDescription, error);
172
+      if (error) reject(RCTErrorUnspecified, error.localizedDescription, error);
105 173
       else reject(RCTErrorUnspecified, @"viewshot unknown error", nil);
106 174
     });
107 175
   }];

+ 1
- 1
package.json View File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "react-native-view-shot",
3
-  "version": "1.7.0",
3
+  "version": "1.8.0",
4 4
   "description": "Snapshot a React Native view and save it to an image",
5 5
   "main": "index.js",
6 6
   "keywords": [

+ 1
- 1
windows/RNViewShot/RNViewShot.csproj View File

@@ -118,7 +118,7 @@
118 118
     <EmbeddedResource Include="Properties\RNViewShot.rd.xml" />
119 119
   </ItemGroup>
120 120
   <ItemGroup>
121
-    <ProjectReference Include="$(SolutionDir)..\node_modules\react-native-windows\ReactWindows\ReactNative\ReactNative.csproj">
121
+    <ProjectReference Include="..\..\..\react-native-windows\ReactWindows\ReactNative\ReactNative.csproj">
122 122
       <Project>{c7673ad5-e3aa-468c-a5fd-fa38154e205c}</Project>
123 123
       <Name>ReactNative</Name>
124 124
     </ProjectReference>

+ 2
- 1
windows/RNViewShot/project.json View File

@@ -1,6 +1,7 @@
1 1
 {
2 2
   "dependencies": {
3
-    "Microsoft.NETCore.UniversalWindowsPlatform": "5.2.2"
3
+    "Microsoft.NETCore.UniversalWindowsPlatform": "5.2.2",
4
+    "Newtonsoft.Json": "10.0.1-beta1"
4 5
   },
5 6
   "frameworks": {
6 7
     "uap10.0": {}