Parcourir la source

Merge pull request #177 from OleksandrKucherenko/android-performance-improvements

Android performance improvements
Gaëtan Renaudeau il y a 6 ans
Parent
révision
2457a00167
No account linked to committer's email address

+ 12
- 5
.gitignore Voir le fichier

@@ -29,11 +29,18 @@ npm-debug.log
29 29
 
30 30
 # android
31 31
 #
32
-android/build/
33
-android/.gradle/
34
-android/.idea/
35
-android/*.iml
36
-android/gradle/
32
+.vscode/
33
+.settings/
34
+android/bin
35
+android/gradle/wrapper
37 36
 android/gradlew
38 37
 android/gradlew.bat
39 38
 android/local.properties
39
+*.iml
40
+.gradle
41
+/local.properties
42
+.idea/
43
+captures/
44
+.externalNativeBuild
45
+.project
46
+

+ 75
- 0
README.md Voir le fichier

@@ -174,6 +174,81 @@ Model tested: iPhone 6 (iOS), Nexus 5 (Android).
174 174
 3. Component itself lacks platform support.
175 175
 4. But you can just use the react-native-maps snapshot function: https://github.com/airbnb/react-native-maps#take-snapshot-of-map
176 176
 
177
+## Performance Optimization
178
+
179
+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
183
+
184
+To solve that in code introduced several new approaches:
185
+- reusable images, that reduce load on GC;
186
+- reusable arrays/buffers that also reduce load on GC;
187
+- RAW image format for avoiding expensive compression;
188
+- ZIP deflate compression for RAW data, that works faster in compare to `Bitmap.compress`
189
+
190
+more details and code snippet are below.
191
+
192
+### RAW Images
193
+
194
+Introduced a new image format RAW. it correspond a ARGB array of pixels.
195
+
196
+Advantages:
197
+- no compression, so its supper quick. Screenshot taking is less than 16ms;
198
+
199
+RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
200
+
201
+RAW file on disk saved in format: `${width}:${height}|${base64}` string.
202
+
203
+### zip-base64
204
+
205
+In compare to BASE64 result string this format fast try to apply zip/deflate compression on screenshot results
206
+and only after that convert results to base64 string. In combination zip-base64 + raw we got a super fast
207
+approach for capturing screen views and deliver them to the react side.
208
+
209
+### How to work with zip-base64 and RAW format?
210
+
211
+```js
212
+const fs = require('fs')
213
+const zlib = require('zlib')
214
+const PNG = require('pngjs').PNG
215
+const Buffer = require('buffer').Buffer
216
+
217
+const format = Platform.OS === 'android' ? 'raw' : 'png'
218
+const result = Platform.OS === 'android' ? 'zip-base64' : 'base64'
219
+
220
+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
+})
238
+```
239
+
240
+Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
241
+
242
+Hint: use `process.fork()` approach for converting raw data into PNGs.
243
+
244
+> Note: code is tested in large commercial project.
245
+
246
+> Note #2: Don't forget to add packages into your project:
247
+> ```js
248
+> yarn add pngjs
249
+> yarn add zlib
250
+> ```
251
+
177 252
 ## Troubleshooting / FAQ
178 253
 
179 254
 ### Saving to a file?

+ 25
- 18
android/build.gradle Voir le fichier

@@ -1,41 +1,48 @@
1 1
 buildscript {
2
-    repositories {
3
-        jcenter()
4
-    }
5
-
6
-    dependencies {
7
-        classpath 'com.android.tools.build:gradle:2.3.0'
2
+    /* In case of submodule usage, do not try to apply own repositories and plugins,
3
+        root project is responsible for that. */
4
+    if (rootProject.buildDir == project.buildDir) {
5
+        repositories {
6
+            google()
7
+            jcenter()
8
+        }
9
+        dependencies {
10
+            classpath 'com.android.tools.build:gradle:2.3.0'
11
+        }
8 12
     }
9 13
 }
10 14
 
11 15
 apply plugin: 'com.android.library'
12 16
 
13 17
 android {
14
-    compileSdkVersion 26
15
-    buildToolsVersion "26.0.1"
18
+    compileSdkVersion 27
19
+    buildToolsVersion "28.0.3"
16 20
 
17 21
     defaultConfig {
18 22
         minSdkVersion 16
19
-        targetSdkVersion 26
23
+        targetSdkVersion 27
24
+
20 25
         versionCode 1
21 26
         versionName "1.0"
22 27
     }
28
+
23 29
     lintOptions {
24 30
         abortOnError false
25 31
     }
26 32
 }
27 33
 
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
-        }
34
+repositories {
35
+    google()
36
+    jcenter()
37
+    mavenLocal()
38
+    maven {
39
+        // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
40
+        url "$rootDir/../node_modules/react-native/android"
36 41
     }
37 42
 }
38 43
 
39 44
 dependencies {
40
-    compile 'com.facebook.react:react-native:+'
45
+    implementation 'com.android.support:support-v4:27.+'
46
+
47
+    api 'com.facebook.react:react-native:+'
41 48
 }

+ 77
- 59
android/src/main/java/fr/greweb/reactnativeviewshot/RNViewShotModule.java Voir le fichier

@@ -1,35 +1,36 @@
1 1
 
2 2
 package fr.greweb.reactnativeviewshot;
3 3
 
4
+import android.app.Activity;
4 5
 import android.content.Context;
5
-import android.graphics.Bitmap;
6 6
 import android.net.Uri;
7 7
 import android.os.AsyncTask;
8
-import android.os.Environment;
8
+import android.support.annotation.NonNull;
9 9
 import android.util.DisplayMetrics;
10
-import android.view.View;
11
-
12
-import com.facebook.react.bridge.ReactApplicationContext;
13
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
14
-import com.facebook.react.bridge.ReactMethod;
10
+import android.util.Log;
15 11
 
16 12
 import com.facebook.react.bridge.GuardedAsyncTask;
17
-import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
18 13
 import com.facebook.react.bridge.Promise;
14
+import com.facebook.react.bridge.ReactApplicationContext;
19 15
 import com.facebook.react.bridge.ReactContext;
16
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
17
+import com.facebook.react.bridge.ReactMethod;
20 18
 import com.facebook.react.bridge.ReadableMap;
21
-import com.facebook.react.uimanager.UIBlock;
22 19
 import com.facebook.react.uimanager.UIManagerModule;
23 20
 
24 21
 import java.io.File;
25 22
 import java.io.FilenameFilter;
26 23
 import java.io.IOException;
27 24
 import java.util.Collections;
28
-import java.util.HashMap;
29 25
 import java.util.Map;
30 26
 
27
+import fr.greweb.reactnativeviewshot.ViewShot.Formats;
28
+import fr.greweb.reactnativeviewshot.ViewShot.Results;
29
+
31 30
 public class RNViewShotModule extends ReactContextBaseJavaModule {
32 31
 
32
+    public static final String RNVIEW_SHOT = "RNViewShot";
33
+
33 34
     private final ReactApplicationContext reactContext;
34 35
 
35 36
     public RNViewShotModule(ReactApplicationContext reactContext) {
@@ -39,7 +40,7 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
39 40
 
40 41
     @Override
41 42
     public String getName() {
42
-        return "RNViewShot";
43
+        return RNVIEW_SHOT;
43 44
     }
44 45
 
45 46
     @Override
@@ -67,30 +68,40 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
67 68
 
68 69
     @ReactMethod
69 70
     public void captureRef(int tag, ReadableMap options, Promise promise) {
70
-        ReactApplicationContext context = getReactApplicationContext();
71
-        String format = options.getString("format");
72
-        Bitmap.CompressFormat compressFormat =
73
-          format.equals("jpg")
74
-          ? Bitmap.CompressFormat.JPEG
75
-          : format.equals("webm")
76
-          ? Bitmap.CompressFormat.WEBP
77
-          : Bitmap.CompressFormat.PNG;
78
-        double quality = options.getDouble("quality");
79
-        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
80
-        Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
81
-        Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null;
82
-        String result = options.getString("result");
83
-        Boolean snapshotContentContainer = options.getBoolean("snapshotContentContainer");
71
+        final ReactApplicationContext context = getReactApplicationContext();
72
+        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
73
+
74
+        final String extension = options.getString("format");
75
+        final int imageFormat = "jpg".equals(extension)
76
+                ? Formats.JPEG
77
+                : "webm".equals(extension)
78
+                ? Formats.WEBP
79
+                : "raw".equals(extension)
80
+                ? Formats.RAW
81
+                : Formats.PNG;
82
+
83
+        final double quality = options.getDouble("quality");
84
+        final Integer scaleWidth = options.hasKey("width") ? (int) (dm.density * options.getDouble("width")) : null;
85
+        final Integer scaleHeight = options.hasKey("height") ? (int) (dm.density * options.getDouble("height")) : null;
86
+        final String resultStreamFormat = options.getString("result");
87
+        final Boolean snapshotContentContainer = options.getBoolean("snapshotContentContainer");
88
+
84 89
         try {
85
-            File file = null;
86
-            if ("tmpfile".equals(result)) {
87
-              file = createTempFile(getReactApplicationContext(), format);
90
+            File outputFile = null;
91
+            if (Results.TEMP_FILE.equals(resultStreamFormat)) {
92
+                outputFile = createTempFile(getReactApplicationContext(), extension);
88 93
             }
89
-            UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
90
-            uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer,reactContext, getCurrentActivity(), promise));
91
-        }
92
-        catch (Exception e) {
93
-            promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
94
+
95
+            final Activity activity = getCurrentActivity();
96
+            final UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
97
+
98
+            uiManager.addUIBlock(new ViewShot(
99
+                    tag, extension, imageFormat, quality,
100
+                    scaleWidth, scaleHeight, outputFile, resultStreamFormat,
101
+                    snapshotContentContainer, reactContext, activity, promise)
102
+            );
103
+        } catch (final Throwable ignored) {
104
+            promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag " + tag);
94 105
         }
95 106
     }
96 107
 
@@ -106,34 +117,41 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
106 117
      * image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
107 118
      * down) and when the module is instantiated, to handle the case where the app crashed.
108 119
      */
109
-    private static class CleanTask extends GuardedAsyncTask<Void, Void> {
110
-        private final Context mContext;
120
+    private static class CleanTask extends GuardedAsyncTask<Void, Void> implements FilenameFilter {
121
+        private final File cacheDir;
122
+        private final File externalCacheDir;
111 123
 
112 124
         private CleanTask(ReactContext context) {
113 125
             super(context);
114
-            mContext = context;
126
+
127
+            cacheDir = context.getCacheDir();
128
+            externalCacheDir = context.getExternalCacheDir();
115 129
         }
116 130
 
117 131
         @Override
118 132
         protected void doInBackgroundGuarded(Void... params) {
119
-            cleanDirectory(mContext.getCacheDir());
120
-            File externalCacheDir = mContext.getExternalCacheDir();
133
+            if (null != cacheDir) {
134
+                cleanDirectory(cacheDir);
135
+            }
136
+
121 137
             if (externalCacheDir != null) {
122 138
                 cleanDirectory(externalCacheDir);
123 139
             }
124 140
         }
125 141
 
126
-        private void cleanDirectory(File directory) {
127
-            File[] toDelete = directory.listFiles(
128
-                    new FilenameFilter() {
129
-                        @Override
130
-                        public boolean accept(File dir, String filename) {
131
-                            return filename.startsWith(TEMP_FILE_PREFIX);
132
-                        }
133
-                    });
142
+        @Override
143
+        public final boolean accept(File dir, String filename) {
144
+            return filename.startsWith(TEMP_FILE_PREFIX);
145
+        }
146
+
147
+        private void cleanDirectory(@NonNull final File directory) {
148
+            final File[] toDelete = directory.listFiles(this);
149
+
134 150
             if (toDelete != null) {
135
-                for (File file: toDelete) {
136
-                    file.delete();
151
+                for (File file : toDelete) {
152
+                    if (file.delete()) {
153
+                        Log.d(RNVIEW_SHOT, "deleted file: " + file.getAbsolutePath());
154
+                    }
137 155
                 }
138 156
             }
139 157
         }
@@ -143,26 +161,26 @@ public class RNViewShotModule extends ReactContextBaseJavaModule {
143 161
      * Create a temporary file in the cache directory on either internal or external storage,
144 162
      * whichever is available and has more free space.
145 163
      */
146
-    private File createTempFile(Context context, String ext)
147
-            throws IOException {
148
-        File externalCacheDir = context.getExternalCacheDir();
149
-        File internalCacheDir = context.getCacheDir();
150
-        File cacheDir;
164
+    @NonNull
165
+    private File createTempFile(@NonNull final Context context, @NonNull final String ext) throws IOException {
166
+        final File externalCacheDir = context.getExternalCacheDir();
167
+        final File internalCacheDir = context.getCacheDir();
168
+        final File cacheDir;
169
+
151 170
         if (externalCacheDir == null && internalCacheDir == null) {
152 171
             throw new IOException("No cache directory available");
153 172
         }
173
+
154 174
         if (externalCacheDir == null) {
155 175
             cacheDir = internalCacheDir;
156
-        }
157
-        else if (internalCacheDir == null) {
176
+        } else if (internalCacheDir == null) {
158 177
             cacheDir = externalCacheDir;
159 178
         } else {
160 179
             cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
161 180
                     externalCacheDir : internalCacheDir;
162 181
         }
163
-        String suffix = "." + ext;
164
-        File tmpFile = File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
165
-        return tmpFile;
166
-    }
167 182
 
183
+        final String suffix = "." + ext;
184
+        return File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
185
+    }
168 186
 }

+ 407
- 79
android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java Voir le fichier

@@ -1,16 +1,22 @@
1 1
 package fr.greweb.reactnativeviewshot;
2 2
 
3
-import javax.annotation.Nullable;
4
-
5 3
 import android.app.Activity;
6
-import android.content.Intent;
7 4
 import android.graphics.Bitmap;
8 5
 import android.graphics.Canvas;
6
+import android.graphics.Color;
7
+import android.graphics.Matrix;
8
+import android.graphics.Point;
9
+import android.graphics.Rect;
10
+import android.graphics.RectF;
9 11
 import android.net.Uri;
12
+import android.support.annotation.IntDef;
13
+import android.support.annotation.NonNull;
14
+import android.support.annotation.StringDef;
10 15
 import android.util.Base64;
11 16
 import android.view.TextureView;
12 17
 import android.view.View;
13 18
 import android.view.ViewGroup;
19
+import android.view.ViewParent;
14 20
 import android.widget.ScrollView;
15 21
 
16 22
 import com.facebook.react.bridge.Promise;
@@ -23,42 +29,113 @@ import java.io.File;
23 29
 import java.io.FileOutputStream;
24 30
 import java.io.IOException;
25 31
 import java.io.OutputStream;
32
+import java.nio.ByteBuffer;
33
+import java.nio.charset.Charset;
26 34
 import java.util.ArrayList;
35
+import java.util.Arrays;
36
+import java.util.Collections;
27 37
 import java.util.List;
38
+import java.util.Locale;
39
+import java.util.Set;
40
+import java.util.WeakHashMap;
41
+import java.util.zip.Deflater;
42
+
43
+import javax.annotation.Nullable;
28 44
 
29 45
 /**
30 46
  * Snapshot utility class allow to screenshot a view.
31 47
  */
32 48
 public class ViewShot implements UIBlock {
33
-
49
+    //region Constants
34 50
     static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
51
+    /**
52
+     * pre-allocated output stream size for screenshot. In real life example it will eb around 7Mb.
53
+     */
54
+    private static final int PREALLOCATE_SIZE = 64 * 1024;
55
+    /**
56
+     * ARGB size in bytes.
57
+     */
58
+    private static final int ARGB_SIZE = 4;
35 59
 
60
+    @SuppressWarnings("WeakerAccess")
61
+    @IntDef({Formats.JPEG, Formats.PNG, Formats.WEBP, Formats.RAW})
62
+    public @interface Formats {
63
+        int JPEG = 0; // Bitmap.CompressFormat.JPEG.ordinal();
64
+        int PNG = 1;  // Bitmap.CompressFormat.PNG.ordinal();
65
+        int WEBP = 2; // Bitmap.CompressFormat.WEBP.ordinal();
66
+        int RAW = -1;
67
+
68
+        Bitmap.CompressFormat[] mapping = {
69
+                Bitmap.CompressFormat.JPEG,
70
+                Bitmap.CompressFormat.PNG,
71
+                Bitmap.CompressFormat.WEBP
72
+        };
73
+    }
74
+
75
+    /**
76
+     * Supported Output results.
77
+     */
78
+    @StringDef({Results.BASE_64, Results.DATA_URI, Results.TEMP_FILE, Results.ZIP_BASE_64})
79
+    public @interface Results {
80
+        /**
81
+         * Save screenshot as temp file on device.
82
+         */
83
+        String TEMP_FILE = "tmpfile";
84
+        /**
85
+         * Base 64 encoded image.
86
+         */
87
+        String BASE_64 = "base64";
88
+        /**
89
+         * Zipped RAW image in base 64 encoding.
90
+         */
91
+        String ZIP_BASE_64 = "zip-base64";
92
+        /**
93
+         * Base64 data uri.
94
+         */
95
+        String DATA_URI = "data-uri";
96
+    }
97
+    //endregion
98
+
99
+    //region Static members
100
+    /**
101
+     * Image output buffer used as a source for base64 encoding
102
+     */
103
+    private static byte[] outputBuffer = new byte[PREALLOCATE_SIZE];
104
+    //endregion
105
+
106
+    //region Class members
36 107
     private final int tag;
37 108
     private final String extension;
38
-    private final Bitmap.CompressFormat format;
109
+    @Formats
110
+    private final int format;
39 111
     private final double quality;
40 112
     private final Integer width;
41 113
     private final Integer height;
42 114
     private final File output;
115
+    @Results
43 116
     private final String result;
44 117
     private final Promise promise;
45 118
     private final Boolean snapshotContentContainer;
119
+    @SuppressWarnings({"unused", "FieldCanBeLocal"})
46 120
     private final ReactApplicationContext reactContext;
47 121
     private final Activity currentActivity;
122
+    //endregion
48 123
 
124
+    //region Constructors
125
+    @SuppressWarnings("WeakerAccess")
49 126
     public ViewShot(
50
-            int tag,
51
-            String extension,
52
-            Bitmap.CompressFormat format,
53
-            double quality,
127
+            final int tag,
128
+            final String extension,
129
+            @Formats final int format,
130
+            final double quality,
54 131
             @Nullable Integer width,
55 132
             @Nullable Integer height,
56
-            File output,
57
-            String result,
58
-            Boolean snapshotContentContainer,
59
-            ReactApplicationContext reactContext,
60
-            Activity currentActivity,
61
-            Promise promise) {
133
+            final File output,
134
+            @Results final String result,
135
+            final Boolean snapshotContentContainer,
136
+            final ReactApplicationContext reactContext,
137
+            final Activity currentActivity,
138
+            final Promise promise) {
62 139
         this.tag = tag;
63 140
         this.extension = extension;
64 141
         this.format = format;
@@ -72,7 +149,9 @@ public class ViewShot implements UIBlock {
72 149
         this.currentActivity = currentActivity;
73 150
         this.promise = promise;
74 151
     }
152
+    //endregion
75 153
 
154
+    //region Overrides
76 155
     @Override
77 156
     public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
78 157
         final View view;
@@ -84,74 +163,146 @@ public class ViewShot implements UIBlock {
84 163
         }
85 164
 
86 165
         if (view == null) {
87
-            promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "No view found with reactTag: "+tag);
166
+            promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "No view found with reactTag: " + tag);
88 167
             return;
89 168
         }
169
+
90 170
         try {
91
-            if ("tmpfile".equals(result)) {
92
-                captureView(view, new FileOutputStream(output));
93
-                final String uri = Uri.fromFile(output).toString();
94
-                promise.resolve(uri);
95
-            } else if ("base64".equals(result)) {
96
-                final ByteArrayOutputStream os = new ByteArrayOutputStream();
97
-                captureView(view, os);
98
-                final byte[] bytes = os.toByteArray();
99
-                final String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
100
-                promise.resolve(data);
101
-            } else if ("data-uri".equals(result)) {
102
-                final ByteArrayOutputStream os = new ByteArrayOutputStream();
103
-                captureView(view, os);
104
-                final byte[] bytes = os.toByteArray();
105
-                String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
106
-                // correct the extension if JPG
107
-                String imageFormat = extension;
108
-                if ("jpg".equals(extension)) {
109
-                    imageFormat = "jpeg";
110
-                }
111
-                data = "data:image/"+imageFormat+";base64," + data;
112
-                promise.resolve(data);
171
+            final ReusableByteArrayOutputStream stream = new ReusableByteArrayOutputStream(outputBuffer);
172
+            stream.setSize(proposeSize(view));
173
+            outputBuffer = stream.innerBuffer();
174
+
175
+            if (Results.TEMP_FILE.equals(result) && Formats.RAW == this.format) {
176
+                saveToRawFileOnDevice(view);
177
+            } else if (Results.TEMP_FILE.equals(result) && Formats.RAW != this.format) {
178
+                saveToTempFileOnDevice(view);
179
+            } else if (Results.BASE_64.equals(result) || Results.ZIP_BASE_64.equals(result)) {
180
+                saveToBase64String(view);
181
+            } else if (Results.DATA_URI.equals(result)) {
182
+                saveToDataUriString(view);
113 183
             }
114
-        } catch (Exception e) {
115
-            e.printStackTrace();
184
+        } catch (final Throwable ignored) {
116 185
             promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to capture view snapshot");
117 186
         }
118 187
     }
188
+    //endregion
189
+
190
+    //region Implementation
191
+    private void saveToTempFileOnDevice(@NonNull final View view) throws IOException {
192
+        final FileOutputStream fos = new FileOutputStream(output);
193
+        captureView(view, fos);
194
+
195
+        promise.resolve(Uri.fromFile(output).toString());
196
+    }
197
+
198
+    private void saveToRawFileOnDevice(@NonNull final View view) throws IOException {
199
+        final String uri = Uri.fromFile(output).toString();
200
+
201
+        final FileOutputStream fos = new FileOutputStream(output);
202
+        final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
203
+        final Point size = captureView(view, os);
204
+
205
+        // in case of buffer grow that will be a new array with bigger size
206
+        outputBuffer = os.innerBuffer();
207
+        final int length = os.size();
208
+        final String resolution = String.format(Locale.US, "%d:%d|", size.x, size.y);
209
+
210
+        fos.write(resolution.getBytes(Charset.forName("US-ASCII")));
211
+        fos.write(outputBuffer, 0, length);
212
+        fos.close();
213
+
214
+        promise.resolve(uri);
215
+    }
216
+
217
+    private void saveToDataUriString(@NonNull final View view) throws IOException {
218
+        final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
219
+        captureView(view, os);
220
+
221
+        outputBuffer = os.innerBuffer();
222
+        final int length = os.size();
119 223
 
120
-    private List<View> getAllChildren(View v) {
224
+        final String data = Base64.encodeToString(outputBuffer, 0, length, Base64.NO_WRAP);
121 225
 
226
+        // correct the extension if JPG
227
+        final String imageFormat = "jpg".equals(extension) ? "jpeg" : extension;
228
+
229
+        promise.resolve("data:image/" + imageFormat + ";base64," + data);
230
+    }
231
+
232
+    private void saveToBase64String(@NonNull final View view) throws IOException {
233
+        final boolean isRaw = Formats.RAW == this.format;
234
+        final boolean isZippedBase64 = Results.ZIP_BASE_64.equals(this.result);
235
+
236
+        final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
237
+        final Point size = captureView(view, os);
238
+
239
+        // in case of buffer grow that will be a new array with bigger size
240
+        outputBuffer = os.innerBuffer();
241
+        final int length = os.size();
242
+        final String resolution = String.format(Locale.US, "%d:%d|", size.x, size.y);
243
+        final String header = (isRaw ? resolution : "");
244
+        final String data;
245
+
246
+        if (isZippedBase64) {
247
+            final Deflater deflater = new Deflater();
248
+            deflater.setInput(outputBuffer, 0, length);
249
+            deflater.finish();
250
+
251
+            final ReusableByteArrayOutputStream zipped = new ReusableByteArrayOutputStream(new byte[32]);
252
+            byte[] buffer = new byte[1024];
253
+            while (!deflater.finished()) {
254
+                int count = deflater.deflate(buffer); // returns the generated code... index
255
+                zipped.write(buffer, 0, count);
256
+            }
257
+
258
+            data = header + Base64.encodeToString(zipped.innerBuffer(), 0, zipped.size(), Base64.NO_WRAP);
259
+        } else {
260
+            data = header + Base64.encodeToString(outputBuffer, 0, length, Base64.NO_WRAP);
261
+        }
262
+
263
+        promise.resolve(data);
264
+    }
265
+
266
+    @NonNull
267
+    private List<View> getAllChildren(@NonNull final View v) {
122 268
         if (!(v instanceof ViewGroup)) {
123
-            ArrayList<View> viewArrayList = new ArrayList<View>();
269
+            final ArrayList<View> viewArrayList = new ArrayList<>();
124 270
             viewArrayList.add(v);
271
+
125 272
             return viewArrayList;
126 273
         }
127 274
 
128
-        ArrayList<View> result = new ArrayList<View>();
275
+        final ArrayList<View> result = new ArrayList<>();
129 276
 
130 277
         ViewGroup viewGroup = (ViewGroup) v;
131 278
         for (int i = 0; i < viewGroup.getChildCount(); i++) {
132
-
133 279
             View child = viewGroup.getChildAt(i);
134 280
 
135 281
             //Do not add any parents, just add child elements
136 282
             result.addAll(getAllChildren(child));
137 283
         }
284
+
138 285
         return result;
139 286
     }
140 287
 
141 288
     /**
142
-     * Screenshot a view and return the captured bitmap.
143
-     * @param view the view to capture
144
-     * @return the screenshot or null if it failed.
289
+     * Wrap {@link #captureViewImpl(View, OutputStream)} call and on end close output stream.
145 290
      */
146
-    private void captureView(View view, OutputStream os) throws IOException {
291
+    private Point captureView(@NonNull final View view, @NonNull final OutputStream os) throws IOException {
147 292
         try {
148
-            captureViewImpl(view, os);
293
+            return captureViewImpl(view, os);
149 294
         } finally {
150 295
             os.close();
151 296
         }
152 297
     }
153 298
 
154
-    private void captureViewImpl(View view, OutputStream os) {
299
+    /**
300
+     * Screenshot a view and return the captured bitmap.
301
+     *
302
+     * @param view the view to capture
303
+     * @return screenshot resolution, Width * Height
304
+     */
305
+    private Point captureViewImpl(@NonNull final View view, @NonNull final OutputStream os) {
155 306
         int w = view.getWidth();
156 307
         int h = view.getHeight();
157 308
 
@@ -161,47 +312,224 @@ public class ViewShot implements UIBlock {
161 312
 
162 313
         //evaluate real height
163 314
         if (snapshotContentContainer) {
164
-            h=0;
165
-            ScrollView scrollView = (ScrollView)view;
315
+            h = 0;
316
+            ScrollView scrollView = (ScrollView) view;
166 317
             for (int i = 0; i < scrollView.getChildCount(); i++) {
167 318
                 h += scrollView.getChildAt(i).getHeight();
168 319
             }
169 320
         }
170
-        Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
171
-        Bitmap childBitmapBuffer;
172
-        Canvas c = new Canvas(bitmap);
321
+
322
+        final Point resolution = new Point(w, h);
323
+        Bitmap bitmap = getBitmapForScreenshot(w, h);
324
+
325
+        final Canvas c = new Canvas(bitmap);
173 326
         view.draw(c);
174 327
 
175 328
         //after view is drawn, go through children
176
-        List<View> childrenList = getAllChildren(view);
177
-
178
-        for (View child : childrenList) {
179
-            if(child instanceof TextureView) {
180
-                ((TextureView) child).setOpaque(false);
181
-                childBitmapBuffer = ((TextureView) child).getBitmap(child.getWidth(), child.getHeight());
182
-                if (childBitmapBuffer != null) {
183
-                    int left = child.getLeft();
184
-                    int top = child.getTop();
185
-                    View parentElem = (View)child.getParent();
186
-                    while (parentElem != null) {
187
-                        if (parentElem == view) {
188
-                            break;
189
-                        }
190
-                        left += parentElem.getLeft();
191
-                        top += parentElem.getTop();
192
-                        parentElem = (View)parentElem.getParent();
193
-                    }
194
-                    c.drawBitmap(childBitmapBuffer, left + child.getPaddingLeft(), top + child.getPaddingTop(), null);
329
+        final List<View> childrenList = getAllChildren(view);
330
+
331
+        for (final View child : childrenList) {
332
+            // skip any child that we don't know how to process
333
+            if (!(child instanceof TextureView)) continue;
334
+            // skip all invisible to user child views
335
+            if (child.getVisibility() != View.VISIBLE) continue;
336
+
337
+            final TextureView tvChild = (TextureView) child;
338
+            tvChild.setOpaque(false);
339
+
340
+            final Point offsets = getParentOffsets(view, child);
341
+            final int left = child.getLeft() + child.getPaddingLeft() + offsets.x;
342
+            final int top = child.getTop() + child.getPaddingTop() + offsets.y;
343
+            final int childWidth = child.getWidth();
344
+            final int childHeight = child.getHeight();
345
+            final Rect source = new Rect(0, 0, childWidth, childHeight);
346
+            final RectF destination = new RectF(left, top, left + childWidth, top + childHeight);
347
+
348
+            // get re-usable bitmap
349
+            final Bitmap childBitmapBuffer = tvChild.getBitmap(getBitmapForScreenshot(child.getWidth(), child.getHeight()));
350
+
351
+            c.save();
352
+            c.setMatrix(concatMatrix(view, child));
353
+            // due to re-use of bitmaps for screenshot, we can get bitmap that is bigger in size than requested
354
+            c.drawBitmap(childBitmapBuffer, source, destination, null);
355
+            c.restore();
356
+            recycleBitmap(childBitmapBuffer);
357
+        }
358
+
359
+        if (width != null && height != null && (width != w || height != h)) {
360
+            final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
361
+            recycleBitmap(bitmap);
362
+
363
+            bitmap = scaledBitmap;
364
+        }
365
+
366
+        // special case, just save RAW ARGB array without any compression
367
+        if (Formats.RAW == this.format && os instanceof ReusableByteArrayOutputStream) {
368
+            final int total = w * h * ARGB_SIZE;
369
+            final ReusableByteArrayOutputStream rbaos = cast(os);
370
+            bitmap.copyPixelsToBuffer(rbaos.asBuffer(total));
371
+            rbaos.setSize(total);
372
+        } else {
373
+            final Bitmap.CompressFormat cf = Formats.mapping[this.format];
374
+
375
+            bitmap.compress(cf, (int) (100.0 * quality), os);
376
+        }
377
+
378
+        recycleBitmap(bitmap);
379
+
380
+        return resolution; // return image width and height
381
+    }
382
+
383
+    /** Concat all the transformation matrix's from child to parent. */
384
+    @NonNull
385
+    private Matrix concatMatrix(@NonNull final View view, @NonNull final View child){
386
+        final Matrix transform = new Matrix();
387
+
388
+        View iterator = child;
389
+        do {
390
+
391
+            final Matrix m = iterator.getMatrix();
392
+            transform.preConcat(m);
393
+
394
+            iterator = (View)iterator.getParent();
395
+        } while( iterator != view );
396
+
397
+        return transform;
398
+    }
399
+
400
+    @NonNull
401
+    private Point getParentOffsets(@NonNull final View view, @NonNull final View child) {
402
+        int left = 0;
403
+        int top = 0;
404
+
405
+        View parentElem = (View) child.getParent();
406
+        while (parentElem != null) {
407
+            if (parentElem == view) break;
408
+
409
+            left += parentElem.getLeft();
410
+            top += parentElem.getTop();
411
+            parentElem = (View) parentElem.getParent();
412
+        }
413
+
414
+        return new Point(left, top);
415
+    }
416
+
417
+    @SuppressWarnings("unchecked")
418
+    private static <T extends A, A> T cast(final A instance) {
419
+        return (T) instance;
420
+    }
421
+    //endregion
422
+
423
+    //region Cache re-usable bitmaps
424
+    /**
425
+     * Synchronization guard.
426
+     */
427
+    private static final Object guardBitmaps = new Object();
428
+    /**
429
+     * Reusable bitmaps for screenshots.
430
+     */
431
+    private static final Set<Bitmap> weakBitmaps = Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
432
+
433
+    /**
434
+     * Propose allocation size of the array output stream.
435
+     */
436
+    private static int proposeSize(@NonNull final View view) {
437
+        final int w = view.getWidth();
438
+        final int h = view.getHeight();
439
+
440
+        return Math.min(w * h * ARGB_SIZE, 32);
441
+    }
442
+
443
+    /**
444
+     * Return bitmap to set of available.
445
+     */
446
+    private static void recycleBitmap(@NonNull final Bitmap bitmap) {
447
+        synchronized (guardBitmaps) {
448
+            weakBitmaps.add(bitmap);
449
+        }
450
+    }
451
+
452
+    /**
453
+     * Try to find a bitmap for screenshot in reusabel set and if not found create a new one.
454
+     */
455
+    @NonNull
456
+    private static Bitmap getBitmapForScreenshot(final int width, final int height) {
457
+        synchronized (guardBitmaps) {
458
+            for (final Bitmap bmp : weakBitmaps) {
459
+                if (bmp.getWidth() * bmp.getHeight() <= width * height) {
460
+                    weakBitmaps.remove(bmp);
461
+                    bmp.eraseColor(Color.TRANSPARENT);
462
+                    return bmp;
195 463
                 }
196 464
             }
197 465
         }
198 466
 
199
-        if (width != null && height != null && (width != w || height != h)) {
200
-            bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
467
+        return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
468
+    }
469
+    //endregion
470
+
471
+    //region Nested declarations
472
+
473
+    /**
474
+     * Stream that can re-use pre-allocated buffer.
475
+     */
476
+    @SuppressWarnings("WeakerAccess")
477
+    public static class ReusableByteArrayOutputStream extends ByteArrayOutputStream {
478
+        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
479
+
480
+        public ReusableByteArrayOutputStream(@NonNull final byte[] buffer) {
481
+            super(0);
482
+
483
+            this.buf = buffer;
484
+        }
485
+
486
+        /**
487
+         * Get access to inner buffer without any memory copy operations.
488
+         */
489
+        public byte[] innerBuffer() {
490
+            return this.buf;
491
+        }
492
+
493
+        @NonNull
494
+        public ByteBuffer asBuffer(final int size) {
495
+            if (this.buf.length < size) {
496
+                grow(size);
497
+            }
498
+
499
+            return ByteBuffer.wrap(this.buf);
201 500
         }
202
-        if (bitmap == null) {
203
-            throw new RuntimeException("Impossible to snapshot the view");
501
+
502
+        public void setSize(final int size) {
503
+            this.count = size;
504
+        }
505
+
506
+        /**
507
+         * Increases the capacity to ensure that it can hold at least the
508
+         * number of elements specified by the minimum capacity argument.
509
+         *
510
+         * @param minCapacity the desired minimum capacity
511
+         */
512
+        protected void grow(int minCapacity) {
513
+            // overflow-conscious code
514
+            int oldCapacity = buf.length;
515
+            int newCapacity = oldCapacity << 1;
516
+            if (newCapacity - minCapacity < 0)
517
+                newCapacity = minCapacity;
518
+            if (newCapacity - MAX_ARRAY_SIZE > 0)
519
+                newCapacity = hugeCapacity(minCapacity);
520
+            buf = Arrays.copyOf(buf, newCapacity);
521
+        }
522
+
523
+        protected static int hugeCapacity(int minCapacity) {
524
+            if (minCapacity < 0) // overflow
525
+                throw new OutOfMemoryError();
526
+
527
+            return (minCapacity > MAX_ARRAY_SIZE) ?
528
+                    Integer.MAX_VALUE :
529
+                    MAX_ARRAY_SIZE;
204 530
         }
205
-        bitmap.compress(format, (int)(100.0 * quality), os);
531
+
206 532
     }
533
+    //endregion
534
+
207 535
 }

+ 27
- 1
example/App.js Voir le fichier

@@ -9,7 +9,8 @@ import {
9 9
   TextInput,
10 10
   Picker,
11 11
   Slider,
12
-  WebView
12
+  WebView,
13
+  ART
13 14
 } from "react-native";
14 15
 import SvgUri from "react-native-svg-uri";
15 16
 import omit from "lodash/omit";
@@ -148,6 +149,8 @@ export default class App extends Component {
148 149
             />
149 150
             <Btn label="📷 All (ScrollView)" onPress={this.snapshot("full")} />
150 151
             <Btn label="📷 SVG" onPress={this.snapshot("svg")} />
152
+            <Btn label="📷 Transform" onPress={this.snapshot("transformParent")} />
153
+            <Btn label="📷 Transform Child" onPress={this.snapshot("transform")} />
151 154
             <Btn label="📷 GL React" onPress={this.snapshot("gl")} />
152 155
             <Btn label="📷 MapView" onPress={this.snapshot("mapview")} />
153 156
             <Btn label="📷 WebView" onPress={this.snapshot("webview")} />
@@ -169,6 +172,7 @@ export default class App extends Component {
169 172
               <Picker.Item label="PNG" value="png" />
170 173
               <Picker.Item label="JPEG" value="jpeg" />
171 174
               <Picker.Item label="WEBM (android only)" value="webm" />
175
+              <Picker.Item label="RAW (android only)" value="raw" />
172 176
               <Picker.Item label="INVALID" value="_invalid_" />
173 177
             </Picker>
174 178
           </View>
@@ -239,6 +243,7 @@ export default class App extends Component {
239 243
             >
240 244
               <Picker.Item label="tmpfile" value="tmpfile" />
241 245
               <Picker.Item label="base64" value="base64" />
246
+              <Picker.Item label="zip-base64 (Android Only)" value="zip-base64" />
242 247
               <Picker.Item label="data URI" value="data-uri" />
243 248
               <Picker.Item label="INVALID" value="_invalid_" />
244 249
             </Picker>
@@ -258,6 +263,24 @@ export default class App extends Component {
258 263
         <View ref="empty" collapsable={false} />
259 264
         <View style={styles.experimental} ref="complex" collapsable={false}>
260 265
           <Text style={styles.experimentalTitle}>Experimental Stuff</Text>
266
+          <View ref="transformParent" collapsable={false}>
267
+              <View ref="transformInner" collapsable={false} style={styles.experimentalTransform}>
268
+                <Text ref="transform" >Transform</Text>
269
+                <ART.Surface ref="surface" width={20} height={20}>
270
+                    <ART.Text
271
+                            fill="#000000"
272
+                            font={{fontFamily:'Arial',fontSize: 6}}
273
+                            >Sample Text</ART.Text>
274
+                    <ART.Shape
275
+                            d='M2.876,10.6499757 L16.375,18.3966817 C16.715,18.5915989 17.011,18.4606545 17.125,18.3956822 C17.237,18.3307098 17.499,18.1367923 17.499,17.746958 L17.499,2.25254636 C17.499,1.86271212 17.237,1.66879457 17.125,1.6038222 C17.011,1.53884983 16.715,1.4079055 16.375,1.60282262 L2.876,9.34952866 C2.537,9.54544536 2.5,9.86930765 2.5,10.000252 C2.5,10.1301967 2.537,10.4550586 2.876,10.6499757 M16.749,20 C16.364,20 15.98,19.8990429 15.629,19.6971288 L2.13,11.9504227 L2.129,11.9504227 C1.422,11.5445953 1,10.8149056 1,10.000252 C1,9.18459879 1.422,8.45590864 2.129,8.04908162 L15.629,0.302375584 C16.332,-0.10245228 17.173,-0.10045313 17.876,0.306373884 C18.579,0.713200898 18.999,1.44089148 18.999,2.25254636 L18.999,17.746958 C18.999,18.5586129 18.579,19.2863035 17.876,19.6931305 C17.523,19.8970438 17.136,20 16.749,20'
276
+                            fill="blue"
277
+                            stroke="black"
278
+                            strokeWidth={0}
279
+                            >
280
+                    </ART.Shape>
281
+                </ART.Surface>
282
+              </View>
283
+          </View>
261 284
           <View ref="svg" collapsable={false}>
262 285
             <SvgUri
263 286
               width={200}
@@ -327,6 +350,9 @@ const styles = StyleSheet.create({
327 350
     fontSize: 16,
328 351
     margin: 10
329 352
   },
353
+  experimentalTransform: {
354
+    transform: [{ rotate: '180deg' }]
355
+  },
330 356
   p1: {
331 357
     marginBottom: 10,
332 358
     flexDirection: "row",

+ 23
- 5
example/android/app/build.gradle Voir le fichier

@@ -65,6 +65,10 @@ import com.android.build.OutputFile
65 65
  * ]
66 66
  */
67 67
 
68
+if (!project.file("../../node_modules/react-native/react.gradle").exists()) {
69
+    throw new RuntimeException("React modules are not installed. execute: 'yarn install' in 'example' folder. ")
70
+}
71
+
68 72
 apply from: "../../node_modules/react-native/react.gradle"
69 73
 
70 74
 /**
@@ -83,15 +87,17 @@ def enableSeparateBuildPerCPUArchitecture = false
83 87
 def enableProguardInReleaseBuilds = false
84 88
 
85 89
 android {
86
-    compileSdkVersion 23
87
-    buildToolsVersion '25.0.3'
90
+    compileSdkVersion 27
91
+    buildToolsVersion '28.0.3'
88 92
 
89 93
     defaultConfig {
90 94
         applicationId "com.viewshotexample"
91 95
         minSdkVersion 16
92
-        targetSdkVersion 22
96
+        targetSdkVersion 27
97
+
93 98
         versionCode 1
94 99
         versionName "1.0"
100
+
95 101
         ndk {
96 102
             abiFilters "armeabi-v7a", "x86"
97 103
         }
@@ -115,7 +121,7 @@ android {
115 121
         variant.outputs.each { output ->
116 122
             // For each separate APK per architecture, set a unique version code as described here:
117 123
             // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
118
-            def versionCodes = ["armeabi-v7a":1, "x86":2]
124
+            def versionCodes = ["armeabi-v7a": 1, "x86": 2]
119 125
             def abi = output.getFilter(OutputFile.ABI)
120 126
             if (abi != null) {  // null for the universal-debug, universal-release variants
121 127
                 output.versionCodeOverride =
@@ -125,11 +131,23 @@ android {
125 131
     }
126 132
 }
127 133
 
134
+repositories {
135
+    google()
136
+    jcenter()
137
+    mavenLocal()
138
+    maven {
139
+        // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
140
+        url "${project.projectDir}/../node_modules/react-native/android"
141
+    }
142
+}
143
+
128 144
 dependencies {
129 145
     compile project(':react-native-svg')
130 146
     compile fileTree(dir: "libs", include: ["*.jar"])
131
-    compile "com.android.support:appcompat-v7:23.0.1"
147
+
148
+    compile "com.android.support:appcompat-v7:27.+"
132 149
     compile "com.facebook.react:react-native:+"  // From node_modules
150
+
133 151
     compile project(':react-native-view-shot')
134 152
     compile project(':gl-react-native')
135 153
     compile project(':react-native-maps')

+ 30
- 1
example/android/build.gradle Voir le fichier

@@ -2,19 +2,34 @@
2 2
 
3 3
 buildscript {
4 4
     repositories {
5
+        google()
5 6
         jcenter()
6 7
     }
7 8
     dependencies {
8
-        classpath 'com.android.tools.build:gradle:2.3.3'
9
+        classpath 'com.android.tools.build:gradle:3.1.4'
9 10
 
10 11
         // NOTE: Do not place your application dependencies here; they belong
11 12
         // in the individual module build.gradle files
12 13
     }
13 14
 }
14 15
 
16
+// This will apply compileSdkVersion and buildToolsVersion to any android modules
17
+subprojects { subproject ->
18
+    afterEvaluate { project ->
19
+        if (!project.name.equalsIgnoreCase("app") && project.hasProperty("android")) {
20
+            android {
21
+                compileSdkVersion 27
22
+                buildToolsVersion '28.0.3'
23
+            }
24
+        }
25
+    }
26
+}
27
+
28
+
15 29
 allprojects {
16 30
     repositories {
17 31
         mavenLocal()
32
+        google()
18 33
         jcenter()
19 34
         maven {
20 35
             // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
@@ -22,3 +37,17 @@ allprojects {
22 37
         }
23 38
     }
24 39
 }
40
+
41
+
42
+subprojects {
43
+    configurations.all {
44
+        resolutionStrategy {
45
+            eachDependency { details ->
46
+                /* Override by group name */
47
+                switch (details.requested.group) {
48
+                    case 'com.android.support': details.useVersion '27.+'; break
49
+                }
50
+            }
51
+        }
52
+    }
53
+}

BIN
example/android/gradle/wrapper/gradle-wrapper.jar Voir le fichier


+ 2
- 2
example/android/gradle/wrapper/gradle-wrapper.properties Voir le fichier

@@ -1,6 +1,6 @@
1
-#Tue Sep 12 09:47:32 CEST 2017
1
+#Tue Oct 09 08:59:03 CEST 2018
2 2
 distributionBase=GRADLE_USER_HOME
3 3
 distributionPath=wrapper/dists
4 4
 zipStoreBase=GRADLE_USER_HOME
5 5
 zipStorePath=wrapper/dists
6
-distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
6
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

+ 43
- 35
example/android/gradlew Voir le fichier

@@ -1,4 +1,4 @@
1
-#!/usr/bin/env bash
1
+#!/usr/bin/env sh
2 2
 
3 3
 ##############################################################################
4 4
 ##
@@ -6,20 +6,38 @@
6 6
 ##
7 7
 ##############################################################################
8 8
 
9
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10
-DEFAULT_JVM_OPTS=""
9
+# Attempt to set APP_HOME
10
+# Resolve links: $0 may be a link
11
+PRG="$0"
12
+# Need this for relative symlinks.
13
+while [ -h "$PRG" ] ; do
14
+    ls=`ls -ld "$PRG"`
15
+    link=`expr "$ls" : '.*-> \(.*\)$'`
16
+    if expr "$link" : '/.*' > /dev/null; then
17
+        PRG="$link"
18
+    else
19
+        PRG=`dirname "$PRG"`"/$link"
20
+    fi
21
+done
22
+SAVED="`pwd`"
23
+cd "`dirname \"$PRG\"`/" >/dev/null
24
+APP_HOME="`pwd -P`"
25
+cd "$SAVED" >/dev/null
11 26
 
12 27
 APP_NAME="Gradle"
13 28
 APP_BASE_NAME=`basename "$0"`
14 29
 
30
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31
+DEFAULT_JVM_OPTS=""
32
+
15 33
 # Use the maximum available, or set MAX_FD != -1 to use that value.
16 34
 MAX_FD="maximum"
17 35
 
18
-warn ( ) {
36
+warn () {
19 37
     echo "$*"
20 38
 }
21 39
 
22
-die ( ) {
40
+die () {
23 41
     echo
24 42
     echo "$*"
25 43
     echo
@@ -30,6 +48,7 @@ die ( ) {
30 48
 cygwin=false
31 49
 msys=false
32 50
 darwin=false
51
+nonstop=false
33 52
 case "`uname`" in
34 53
   CYGWIN* )
35 54
     cygwin=true
@@ -40,31 +59,11 @@ case "`uname`" in
40 59
   MINGW* )
41 60
     msys=true
42 61
     ;;
62
+  NONSTOP* )
63
+    nonstop=true
64
+    ;;
43 65
 esac
44 66
 
45
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
46
-if $cygwin ; then
47
-    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48
-fi
49
-
50
-# Attempt to set APP_HOME
51
-# Resolve links: $0 may be a link
52
-PRG="$0"
53
-# Need this for relative symlinks.
54
-while [ -h "$PRG" ] ; do
55
-    ls=`ls -ld "$PRG"`
56
-    link=`expr "$ls" : '.*-> \(.*\)$'`
57
-    if expr "$link" : '/.*' > /dev/null; then
58
-        PRG="$link"
59
-    else
60
-        PRG=`dirname "$PRG"`"/$link"
61
-    fi
62
-done
63
-SAVED="`pwd`"
64
-cd "`dirname \"$PRG\"`/" >&-
65
-APP_HOME="`pwd -P`"
66
-cd "$SAVED" >&-
67
-
68 67
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 68
 
70 69
 # Determine the Java command to use to start the JVM.
@@ -90,7 +89,7 @@ location of your Java installation."
90 89
 fi
91 90
 
92 91
 # Increase the maximum file descriptors if we can.
93
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
92
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
94 93
     MAX_FD_LIMIT=`ulimit -H -n`
95 94
     if [ $? -eq 0 ] ; then
96 95
         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -114,6 +113,7 @@ fi
114 113
 if $cygwin ; then
115 114
     APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 115
     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116
+    JAVACMD=`cygpath --unix "$JAVACMD"`
117 117
 
118 118
     # We build the pattern for arguments to be converted via cygpath
119 119
     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,11 +154,19 @@ if $cygwin ; then
154 154
     esac
155 155
 fi
156 156
 
157
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158
-function splitJvmOpts() {
159
-    JVM_OPTS=("$@")
157
+# Escape application args
158
+save () {
159
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160
+    echo " "
160 161
 }
161
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
162
+APP_ARGS=$(save "$@")
163
+
164
+# Collect all arguments for the java command, following the shell quoting and substitution rules
165
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166
+
167
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169
+  cd "$(dirname "$0")"
170
+fi
163 171
 
164
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
172
+exec "$JAVACMD" "$@"

+ 4
- 10
example/android/gradlew.bat Voir le fichier

@@ -8,14 +8,14 @@
8 8
 @rem Set local scope for the variables with windows NT shell
9 9
 if "%OS%"=="Windows_NT" setlocal
10 10
 
11
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12
-set DEFAULT_JVM_OPTS=
13
-
14 11
 set DIRNAME=%~dp0
15 12
 if "%DIRNAME%" == "" set DIRNAME=.
16 13
 set APP_BASE_NAME=%~n0
17 14
 set APP_HOME=%DIRNAME%
18 15
 
16
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17
+set DEFAULT_JVM_OPTS=
18
+
19 19
 @rem Find java.exe
20 20
 if defined JAVA_HOME goto findJavaFromJavaHome
21 21
 
@@ -46,10 +46,9 @@ echo location of your Java installation.
46 46
 goto fail
47 47
 
48 48
 :init
49
-@rem Get command-line arguments, handling Windowz variants
49
+@rem Get command-line arguments, handling Windows variants
50 50
 
51 51
 if not "%OS%" == "Windows_NT" goto win9xME_args
52
-if "%@eval[2+2]" == "4" goto 4NT_args
53 52
 
54 53
 :win9xME_args
55 54
 @rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
60 59
 if "x%~1" == "x" goto execute
61 60
 
62 61
 set CMD_LINE_ARGS=%*
63
-goto execute
64
-
65
-:4NT_args
66
-@rem Get arguments from the 4NT Shell from JP Software
67
-set CMD_LINE_ARGS=%$
68 62
 
69 63
 :execute
70 64
 @rem Setup the command line

+ 2
- 1
example/android/settings.gradle Voir le fichier

@@ -1,8 +1,9 @@
1 1
 rootProject.name = 'ViewShotExample'
2
+include ':app'
3
+
2 4
 include ':react-native-svg'
3 5
 project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
4 6
 
5
-include ':app'
6 7
 include ':react-native-view-shot'
7 8
 project(':react-native-view-shot').projectDir = new File(rootProject.projectDir, '../../android')
8 9
 

+ 4
- 3
src/index.d.ts Voir le fichier

@@ -23,9 +23,9 @@ declare module 'react-native-view-shot' {
23 23
          */
24 24
         height?: number;
25 25
         /**
26
-         * either png or jpg or webm (Android). Defaults to png.
26
+         * either png or jpg or webm (Android). Defaults to png. raw is a ARGB array of image pixels.
27 27
          */
28
-        format?: 'jpg' | 'png' | 'webm';
28
+        format?: 'jpg' | 'png' | 'webm' | 'raw';
29 29
         /**
30 30
          * the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
31 31
          */
@@ -36,8 +36,9 @@ declare module 'react-native-view-shot' {
36 36
          " - base64": encode as base64 and returns the raw string. Use only with small images as this may result of
37 37
          *   lags (the string is sent over the bridge). N.B. This is not a data uri, use data-uri instead.
38 38
          " - data-uri": same as base64 but also includes the Data URI scheme header.
39
+         " - zip-base64: compress data with zip deflate algorithm and than convert to base64 and return as a raw string."
39 40
          */
40
-        result?: 'tmpfile' | 'base64' | 'data-uri';
41
+        result?: 'tmpfile' | 'base64' | 'data-uri' | 'zip-base64';
41 42
         /**
42 43
          * if true and when view is a ScrollView, the "content container" height will be evaluated instead of the
43 44
          * container height.

+ 8
- 6
src/index.js Voir le fichier

@@ -8,9 +8,9 @@ const neverEndingPromise = new Promise(() => {});
8 8
 type Options = {
9 9
   width?: number,
10 10
   height?: number,
11
-  format: "png" | "jpg" | "webm",
11
+  format: "png" | "jpg" | "webm" | "raw",
12 12
   quality: number,
13
-  result: "tmpfile" | "base64" | "data-uri",
13
+  result: "tmpfile" | "base64" | "data-uri" | "zip-base64",
14 14
   snapshotContentContainer: boolean
15 15
 };
16 16
 
@@ -21,10 +21,12 @@ if (!RNViewShot) {
21 21
 }
22 22
 
23 23
 const acceptedFormats = ["png", "jpg"].concat(
24
-  Platform.OS === "android" ? ["webm"] : []
24
+  Platform.OS === "android" ? ["webm", "raw"] : []
25 25
 );
26 26
 
27
-const acceptedResults = ["tmpfile", "base64", "data-uri"];
27
+const acceptedResults = ["tmpfile", "base64", "data-uri"].concat(
28
+  Platform.OS === "android" ? ["zip-base64"] : []
29
+);
28 30
 
29 31
 const defaultOptions = {
30 32
   format: "png",
@@ -70,13 +72,13 @@ function validateOptions(
70 72
   if (acceptedFormats.indexOf(options.format) === -1) {
71 73
     options.format = defaultOptions.format;
72 74
     errors.push(
73
-      "option format is not in valid formats: " + acceptedFormats.join(" | ")
75
+      "option format '" + options.format + "' is not in valid formats: " + acceptedFormats.join(" | ")
74 76
     );
75 77
   }
76 78
   if (acceptedResults.indexOf(options.result) === -1) {
77 79
     options.result = defaultOptions.result;
78 80
     errors.push(
79
-      "option result is not in valid formats: " + acceptedResults.join(" | ")
81
+      "option result '" + options.result  + "' is not in valid formats: " + acceptedResults.join(" | ")
80 82
     );
81 83
   }
82 84
   return { options, errors };