Browse Source

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

Android performance improvements
Gaëtan Renaudeau 6 years ago
parent
commit
2457a00167
No account linked to committer's email address

+ 12
- 5
.gitignore View File

29
 
29
 
30
 # android
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
 android/gradlew
36
 android/gradlew
38
 android/gradlew.bat
37
 android/gradlew.bat
39
 android/local.properties
38
 android/local.properties
39
+*.iml
40
+.gradle
41
+/local.properties
42
+.idea/
43
+captures/
44
+.externalNativeBuild
45
+.project
46
+

+ 75
- 0
README.md View File

174
 3. Component itself lacks platform support.
174
 3. Component itself lacks platform support.
175
 4. But you can just use the react-native-maps snapshot function: https://github.com/airbnb/react-native-maps#take-snapshot-of-map
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
 ## Troubleshooting / FAQ
252
 ## Troubleshooting / FAQ
178
 
253
 
179
 ### Saving to a file?
254
 ### Saving to a file?

+ 25
- 18
android/build.gradle View File

1
 buildscript {
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
 apply plugin: 'com.android.library'
15
 apply plugin: 'com.android.library'
12
 
16
 
13
 android {
17
 android {
14
-    compileSdkVersion 26
15
-    buildToolsVersion "26.0.1"
18
+    compileSdkVersion 27
19
+    buildToolsVersion "28.0.3"
16
 
20
 
17
     defaultConfig {
21
     defaultConfig {
18
         minSdkVersion 16
22
         minSdkVersion 16
19
-        targetSdkVersion 26
23
+        targetSdkVersion 27
24
+
20
         versionCode 1
25
         versionCode 1
21
         versionName "1.0"
26
         versionName "1.0"
22
     }
27
     }
28
+
23
     lintOptions {
29
     lintOptions {
24
         abortOnError false
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
 dependencies {
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 View File

1
 
1
 
2
 package fr.greweb.reactnativeviewshot;
2
 package fr.greweb.reactnativeviewshot;
3
 
3
 
4
+import android.app.Activity;
4
 import android.content.Context;
5
 import android.content.Context;
5
-import android.graphics.Bitmap;
6
 import android.net.Uri;
6
 import android.net.Uri;
7
 import android.os.AsyncTask;
7
 import android.os.AsyncTask;
8
-import android.os.Environment;
8
+import android.support.annotation.NonNull;
9
 import android.util.DisplayMetrics;
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
 import com.facebook.react.bridge.GuardedAsyncTask;
12
 import com.facebook.react.bridge.GuardedAsyncTask;
17
-import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
18
 import com.facebook.react.bridge.Promise;
13
 import com.facebook.react.bridge.Promise;
14
+import com.facebook.react.bridge.ReactApplicationContext;
19
 import com.facebook.react.bridge.ReactContext;
15
 import com.facebook.react.bridge.ReactContext;
16
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
17
+import com.facebook.react.bridge.ReactMethod;
20
 import com.facebook.react.bridge.ReadableMap;
18
 import com.facebook.react.bridge.ReadableMap;
21
-import com.facebook.react.uimanager.UIBlock;
22
 import com.facebook.react.uimanager.UIManagerModule;
19
 import com.facebook.react.uimanager.UIManagerModule;
23
 
20
 
24
 import java.io.File;
21
 import java.io.File;
25
 import java.io.FilenameFilter;
22
 import java.io.FilenameFilter;
26
 import java.io.IOException;
23
 import java.io.IOException;
27
 import java.util.Collections;
24
 import java.util.Collections;
28
-import java.util.HashMap;
29
 import java.util.Map;
25
 import java.util.Map;
30
 
26
 
27
+import fr.greweb.reactnativeviewshot.ViewShot.Formats;
28
+import fr.greweb.reactnativeviewshot.ViewShot.Results;
29
+
31
 public class RNViewShotModule extends ReactContextBaseJavaModule {
30
 public class RNViewShotModule extends ReactContextBaseJavaModule {
32
 
31
 
32
+    public static final String RNVIEW_SHOT = "RNViewShot";
33
+
33
     private final ReactApplicationContext reactContext;
34
     private final ReactApplicationContext reactContext;
34
 
35
 
35
     public RNViewShotModule(ReactApplicationContext reactContext) {
36
     public RNViewShotModule(ReactApplicationContext reactContext) {
39
 
40
 
40
     @Override
41
     @Override
41
     public String getName() {
42
     public String getName() {
42
-        return "RNViewShot";
43
+        return RNVIEW_SHOT;
43
     }
44
     }
44
 
45
 
45
     @Override
46
     @Override
67
 
68
 
68
     @ReactMethod
69
     @ReactMethod
69
     public void captureRef(int tag, ReadableMap options, Promise promise) {
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
         try {
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
      * image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
117
      * image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
107
      * down) and when the module is instantiated, to handle the case where the app crashed.
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
         private CleanTask(ReactContext context) {
124
         private CleanTask(ReactContext context) {
113
             super(context);
125
             super(context);
114
-            mContext = context;
126
+
127
+            cacheDir = context.getCacheDir();
128
+            externalCacheDir = context.getExternalCacheDir();
115
         }
129
         }
116
 
130
 
117
         @Override
131
         @Override
118
         protected void doInBackgroundGuarded(Void... params) {
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
             if (externalCacheDir != null) {
137
             if (externalCacheDir != null) {
122
                 cleanDirectory(externalCacheDir);
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
             if (toDelete != null) {
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
      * Create a temporary file in the cache directory on either internal or external storage,
161
      * Create a temporary file in the cache directory on either internal or external storage,
144
      * whichever is available and has more free space.
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
         if (externalCacheDir == null && internalCacheDir == null) {
170
         if (externalCacheDir == null && internalCacheDir == null) {
152
             throw new IOException("No cache directory available");
171
             throw new IOException("No cache directory available");
153
         }
172
         }
173
+
154
         if (externalCacheDir == null) {
174
         if (externalCacheDir == null) {
155
             cacheDir = internalCacheDir;
175
             cacheDir = internalCacheDir;
156
-        }
157
-        else if (internalCacheDir == null) {
176
+        } else if (internalCacheDir == null) {
158
             cacheDir = externalCacheDir;
177
             cacheDir = externalCacheDir;
159
         } else {
178
         } else {
160
             cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
179
             cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
161
                     externalCacheDir : internalCacheDir;
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 View File

1
 package fr.greweb.reactnativeviewshot;
1
 package fr.greweb.reactnativeviewshot;
2
 
2
 
3
-import javax.annotation.Nullable;
4
-
5
 import android.app.Activity;
3
 import android.app.Activity;
6
-import android.content.Intent;
7
 import android.graphics.Bitmap;
4
 import android.graphics.Bitmap;
8
 import android.graphics.Canvas;
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
 import android.net.Uri;
11
 import android.net.Uri;
12
+import android.support.annotation.IntDef;
13
+import android.support.annotation.NonNull;
14
+import android.support.annotation.StringDef;
10
 import android.util.Base64;
15
 import android.util.Base64;
11
 import android.view.TextureView;
16
 import android.view.TextureView;
12
 import android.view.View;
17
 import android.view.View;
13
 import android.view.ViewGroup;
18
 import android.view.ViewGroup;
19
+import android.view.ViewParent;
14
 import android.widget.ScrollView;
20
 import android.widget.ScrollView;
15
 
21
 
16
 import com.facebook.react.bridge.Promise;
22
 import com.facebook.react.bridge.Promise;
23
 import java.io.FileOutputStream;
29
 import java.io.FileOutputStream;
24
 import java.io.IOException;
30
 import java.io.IOException;
25
 import java.io.OutputStream;
31
 import java.io.OutputStream;
32
+import java.nio.ByteBuffer;
33
+import java.nio.charset.Charset;
26
 import java.util.ArrayList;
34
 import java.util.ArrayList;
35
+import java.util.Arrays;
36
+import java.util.Collections;
27
 import java.util.List;
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
  * Snapshot utility class allow to screenshot a view.
46
  * Snapshot utility class allow to screenshot a view.
31
  */
47
  */
32
 public class ViewShot implements UIBlock {
48
 public class ViewShot implements UIBlock {
33
-
49
+    //region Constants
34
     static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
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
     private final int tag;
107
     private final int tag;
37
     private final String extension;
108
     private final String extension;
38
-    private final Bitmap.CompressFormat format;
109
+    @Formats
110
+    private final int format;
39
     private final double quality;
111
     private final double quality;
40
     private final Integer width;
112
     private final Integer width;
41
     private final Integer height;
113
     private final Integer height;
42
     private final File output;
114
     private final File output;
115
+    @Results
43
     private final String result;
116
     private final String result;
44
     private final Promise promise;
117
     private final Promise promise;
45
     private final Boolean snapshotContentContainer;
118
     private final Boolean snapshotContentContainer;
119
+    @SuppressWarnings({"unused", "FieldCanBeLocal"})
46
     private final ReactApplicationContext reactContext;
120
     private final ReactApplicationContext reactContext;
47
     private final Activity currentActivity;
121
     private final Activity currentActivity;
122
+    //endregion
48
 
123
 
124
+    //region Constructors
125
+    @SuppressWarnings("WeakerAccess")
49
     public ViewShot(
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
             @Nullable Integer width,
131
             @Nullable Integer width,
55
             @Nullable Integer height,
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
         this.tag = tag;
139
         this.tag = tag;
63
         this.extension = extension;
140
         this.extension = extension;
64
         this.format = format;
141
         this.format = format;
72
         this.currentActivity = currentActivity;
149
         this.currentActivity = currentActivity;
73
         this.promise = promise;
150
         this.promise = promise;
74
     }
151
     }
152
+    //endregion
75
 
153
 
154
+    //region Overrides
76
     @Override
155
     @Override
77
     public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
156
     public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
78
         final View view;
157
         final View view;
84
         }
163
         }
85
 
164
 
86
         if (view == null) {
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
             return;
167
             return;
89
         }
168
         }
169
+
90
         try {
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
             promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to capture view snapshot");
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
         if (!(v instanceof ViewGroup)) {
268
         if (!(v instanceof ViewGroup)) {
123
-            ArrayList<View> viewArrayList = new ArrayList<View>();
269
+            final ArrayList<View> viewArrayList = new ArrayList<>();
124
             viewArrayList.add(v);
270
             viewArrayList.add(v);
271
+
125
             return viewArrayList;
272
             return viewArrayList;
126
         }
273
         }
127
 
274
 
128
-        ArrayList<View> result = new ArrayList<View>();
275
+        final ArrayList<View> result = new ArrayList<>();
129
 
276
 
130
         ViewGroup viewGroup = (ViewGroup) v;
277
         ViewGroup viewGroup = (ViewGroup) v;
131
         for (int i = 0; i < viewGroup.getChildCount(); i++) {
278
         for (int i = 0; i < viewGroup.getChildCount(); i++) {
132
-
133
             View child = viewGroup.getChildAt(i);
279
             View child = viewGroup.getChildAt(i);
134
 
280
 
135
             //Do not add any parents, just add child elements
281
             //Do not add any parents, just add child elements
136
             result.addAll(getAllChildren(child));
282
             result.addAll(getAllChildren(child));
137
         }
283
         }
284
+
138
         return result;
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
         try {
292
         try {
148
-            captureViewImpl(view, os);
293
+            return captureViewImpl(view, os);
149
         } finally {
294
         } finally {
150
             os.close();
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
         int w = view.getWidth();
306
         int w = view.getWidth();
156
         int h = view.getHeight();
307
         int h = view.getHeight();
157
 
308
 
161
 
312
 
162
         //evaluate real height
313
         //evaluate real height
163
         if (snapshotContentContainer) {
314
         if (snapshotContentContainer) {
164
-            h=0;
165
-            ScrollView scrollView = (ScrollView)view;
315
+            h = 0;
316
+            ScrollView scrollView = (ScrollView) view;
166
             for (int i = 0; i < scrollView.getChildCount(); i++) {
317
             for (int i = 0; i < scrollView.getChildCount(); i++) {
167
                 h += scrollView.getChildAt(i).getHeight();
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
         view.draw(c);
326
         view.draw(c);
174
 
327
 
175
         //after view is drawn, go through children
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 View File

9
   TextInput,
9
   TextInput,
10
   Picker,
10
   Picker,
11
   Slider,
11
   Slider,
12
-  WebView
12
+  WebView,
13
+  ART
13
 } from "react-native";
14
 } from "react-native";
14
 import SvgUri from "react-native-svg-uri";
15
 import SvgUri from "react-native-svg-uri";
15
 import omit from "lodash/omit";
16
 import omit from "lodash/omit";
148
             />
149
             />
149
             <Btn label="📷 All (ScrollView)" onPress={this.snapshot("full")} />
150
             <Btn label="📷 All (ScrollView)" onPress={this.snapshot("full")} />
150
             <Btn label="📷 SVG" onPress={this.snapshot("svg")} />
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
             <Btn label="📷 GL React" onPress={this.snapshot("gl")} />
154
             <Btn label="📷 GL React" onPress={this.snapshot("gl")} />
152
             <Btn label="📷 MapView" onPress={this.snapshot("mapview")} />
155
             <Btn label="📷 MapView" onPress={this.snapshot("mapview")} />
153
             <Btn label="📷 WebView" onPress={this.snapshot("webview")} />
156
             <Btn label="📷 WebView" onPress={this.snapshot("webview")} />
169
               <Picker.Item label="PNG" value="png" />
172
               <Picker.Item label="PNG" value="png" />
170
               <Picker.Item label="JPEG" value="jpeg" />
173
               <Picker.Item label="JPEG" value="jpeg" />
171
               <Picker.Item label="WEBM (android only)" value="webm" />
174
               <Picker.Item label="WEBM (android only)" value="webm" />
175
+              <Picker.Item label="RAW (android only)" value="raw" />
172
               <Picker.Item label="INVALID" value="_invalid_" />
176
               <Picker.Item label="INVALID" value="_invalid_" />
173
             </Picker>
177
             </Picker>
174
           </View>
178
           </View>
239
             >
243
             >
240
               <Picker.Item label="tmpfile" value="tmpfile" />
244
               <Picker.Item label="tmpfile" value="tmpfile" />
241
               <Picker.Item label="base64" value="base64" />
245
               <Picker.Item label="base64" value="base64" />
246
+              <Picker.Item label="zip-base64 (Android Only)" value="zip-base64" />
242
               <Picker.Item label="data URI" value="data-uri" />
247
               <Picker.Item label="data URI" value="data-uri" />
243
               <Picker.Item label="INVALID" value="_invalid_" />
248
               <Picker.Item label="INVALID" value="_invalid_" />
244
             </Picker>
249
             </Picker>
258
         <View ref="empty" collapsable={false} />
263
         <View ref="empty" collapsable={false} />
259
         <View style={styles.experimental} ref="complex" collapsable={false}>
264
         <View style={styles.experimental} ref="complex" collapsable={false}>
260
           <Text style={styles.experimentalTitle}>Experimental Stuff</Text>
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
           <View ref="svg" collapsable={false}>
284
           <View ref="svg" collapsable={false}>
262
             <SvgUri
285
             <SvgUri
263
               width={200}
286
               width={200}
327
     fontSize: 16,
350
     fontSize: 16,
328
     margin: 10
351
     margin: 10
329
   },
352
   },
353
+  experimentalTransform: {
354
+    transform: [{ rotate: '180deg' }]
355
+  },
330
   p1: {
356
   p1: {
331
     marginBottom: 10,
357
     marginBottom: 10,
332
     flexDirection: "row",
358
     flexDirection: "row",

+ 23
- 5
example/android/app/build.gradle View File

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
 apply from: "../../node_modules/react-native/react.gradle"
72
 apply from: "../../node_modules/react-native/react.gradle"
69
 
73
 
70
 /**
74
 /**
83
 def enableProguardInReleaseBuilds = false
87
 def enableProguardInReleaseBuilds = false
84
 
88
 
85
 android {
89
 android {
86
-    compileSdkVersion 23
87
-    buildToolsVersion '25.0.3'
90
+    compileSdkVersion 27
91
+    buildToolsVersion '28.0.3'
88
 
92
 
89
     defaultConfig {
93
     defaultConfig {
90
         applicationId "com.viewshotexample"
94
         applicationId "com.viewshotexample"
91
         minSdkVersion 16
95
         minSdkVersion 16
92
-        targetSdkVersion 22
96
+        targetSdkVersion 27
97
+
93
         versionCode 1
98
         versionCode 1
94
         versionName "1.0"
99
         versionName "1.0"
100
+
95
         ndk {
101
         ndk {
96
             abiFilters "armeabi-v7a", "x86"
102
             abiFilters "armeabi-v7a", "x86"
97
         }
103
         }
115
         variant.outputs.each { output ->
121
         variant.outputs.each { output ->
116
             // For each separate APK per architecture, set a unique version code as described here:
122
             // For each separate APK per architecture, set a unique version code as described here:
117
             // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
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
             def abi = output.getFilter(OutputFile.ABI)
125
             def abi = output.getFilter(OutputFile.ABI)
120
             if (abi != null) {  // null for the universal-debug, universal-release variants
126
             if (abi != null) {  // null for the universal-debug, universal-release variants
121
                 output.versionCodeOverride =
127
                 output.versionCodeOverride =
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
 dependencies {
144
 dependencies {
129
     compile project(':react-native-svg')
145
     compile project(':react-native-svg')
130
     compile fileTree(dir: "libs", include: ["*.jar"])
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
     compile "com.facebook.react:react-native:+"  // From node_modules
149
     compile "com.facebook.react:react-native:+"  // From node_modules
150
+
133
     compile project(':react-native-view-shot')
151
     compile project(':react-native-view-shot')
134
     compile project(':gl-react-native')
152
     compile project(':gl-react-native')
135
     compile project(':react-native-maps')
153
     compile project(':react-native-maps')

+ 30
- 1
example/android/build.gradle View File

2
 
2
 
3
 buildscript {
3
 buildscript {
4
     repositories {
4
     repositories {
5
+        google()
5
         jcenter()
6
         jcenter()
6
     }
7
     }
7
     dependencies {
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
         // NOTE: Do not place your application dependencies here; they belong
11
         // NOTE: Do not place your application dependencies here; they belong
11
         // in the individual module build.gradle files
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
 allprojects {
29
 allprojects {
16
     repositories {
30
     repositories {
17
         mavenLocal()
31
         mavenLocal()
32
+        google()
18
         jcenter()
33
         jcenter()
19
         maven {
34
         maven {
20
             // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
35
             // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
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 View File


+ 2
- 2
example/android/gradle/wrapper/gradle-wrapper.properties View File

1
-#Tue Sep 12 09:47:32 CEST 2017
1
+#Tue Oct 09 08:59:03 CEST 2018
2
 distributionBase=GRADLE_USER_HOME
2
 distributionBase=GRADLE_USER_HOME
3
 distributionPath=wrapper/dists
3
 distributionPath=wrapper/dists
4
 zipStoreBase=GRADLE_USER_HOME
4
 zipStoreBase=GRADLE_USER_HOME
5
 zipStorePath=wrapper/dists
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 View File

1
-#!/usr/bin/env bash
1
+#!/usr/bin/env sh
2
 
2
 
3
 ##############################################################################
3
 ##############################################################################
4
 ##
4
 ##
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
 APP_NAME="Gradle"
27
 APP_NAME="Gradle"
13
 APP_BASE_NAME=`basename "$0"`
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
 # Use the maximum available, or set MAX_FD != -1 to use that value.
33
 # Use the maximum available, or set MAX_FD != -1 to use that value.
16
 MAX_FD="maximum"
34
 MAX_FD="maximum"
17
 
35
 
18
-warn ( ) {
36
+warn () {
19
     echo "$*"
37
     echo "$*"
20
 }
38
 }
21
 
39
 
22
-die ( ) {
40
+die () {
23
     echo
41
     echo
24
     echo "$*"
42
     echo "$*"
25
     echo
43
     echo
30
 cygwin=false
48
 cygwin=false
31
 msys=false
49
 msys=false
32
 darwin=false
50
 darwin=false
51
+nonstop=false
33
 case "`uname`" in
52
 case "`uname`" in
34
   CYGWIN* )
53
   CYGWIN* )
35
     cygwin=true
54
     cygwin=true
40
   MINGW* )
59
   MINGW* )
41
     msys=true
60
     msys=true
42
     ;;
61
     ;;
62
+  NONSTOP* )
63
+    nonstop=true
64
+    ;;
43
 esac
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
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
67
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69
 
68
 
70
 # Determine the Java command to use to start the JVM.
69
 # Determine the Java command to use to start the JVM.
90
 fi
89
 fi
91
 
90
 
92
 # Increase the maximum file descriptors if we can.
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
     MAX_FD_LIMIT=`ulimit -H -n`
93
     MAX_FD_LIMIT=`ulimit -H -n`
95
     if [ $? -eq 0 ] ; then
94
     if [ $? -eq 0 ] ; then
96
         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
95
         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
114
 if $cygwin ; then
113
 if $cygwin ; then
115
     APP_HOME=`cygpath --path --mixed "$APP_HOME"`
114
     APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116
     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
115
     CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116
+    JAVACMD=`cygpath --unix "$JAVACMD"`
117
 
117
 
118
     # We build the pattern for arguments to be converted via cygpath
118
     # We build the pattern for arguments to be converted via cygpath
119
     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
119
     ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
154
     esac
154
     esac
155
 fi
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 View File

8
 @rem Set local scope for the variables with windows NT shell
8
 @rem Set local scope for the variables with windows NT shell
9
 if "%OS%"=="Windows_NT" setlocal
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
 set DIRNAME=%~dp0
11
 set DIRNAME=%~dp0
15
 if "%DIRNAME%" == "" set DIRNAME=.
12
 if "%DIRNAME%" == "" set DIRNAME=.
16
 set APP_BASE_NAME=%~n0
13
 set APP_BASE_NAME=%~n0
17
 set APP_HOME=%DIRNAME%
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
 @rem Find java.exe
19
 @rem Find java.exe
20
 if defined JAVA_HOME goto findJavaFromJavaHome
20
 if defined JAVA_HOME goto findJavaFromJavaHome
21
 
21
 
46
 goto fail
46
 goto fail
47
 
47
 
48
 :init
48
 :init
49
-@rem Get command-line arguments, handling Windowz variants
49
+@rem Get command-line arguments, handling Windows variants
50
 
50
 
51
 if not "%OS%" == "Windows_NT" goto win9xME_args
51
 if not "%OS%" == "Windows_NT" goto win9xME_args
52
-if "%@eval[2+2]" == "4" goto 4NT_args
53
 
52
 
54
 :win9xME_args
53
 :win9xME_args
55
 @rem Slurp the command line arguments.
54
 @rem Slurp the command line arguments.
60
 if "x%~1" == "x" goto execute
59
 if "x%~1" == "x" goto execute
61
 
60
 
62
 set CMD_LINE_ARGS=%*
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
 :execute
63
 :execute
70
 @rem Setup the command line
64
 @rem Setup the command line

+ 2
- 1
example/android/settings.gradle View File

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

+ 4
- 3
src/index.d.ts View File

23
          */
23
          */
24
         height?: number;
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
          * the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
30
          * the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg)
31
          */
31
          */
36
          " - base64": encode as base64 and returns the raw string. Use only with small images as this may result of
36
          " - base64": encode as base64 and returns the raw string. Use only with small images as this may result of
37
          *   lags (the string is sent over the bridge). N.B. This is not a data uri, use data-uri instead.
37
          *   lags (the string is sent over the bridge). N.B. This is not a data uri, use data-uri instead.
38
          " - data-uri": same as base64 but also includes the Data URI scheme header.
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
          * if true and when view is a ScrollView, the "content container" height will be evaluated instead of the
43
          * if true and when view is a ScrollView, the "content container" height will be evaluated instead of the
43
          * container height.
44
          * container height.

+ 8
- 6
src/index.js View File

8
 type Options = {
8
 type Options = {
9
   width?: number,
9
   width?: number,
10
   height?: number,
10
   height?: number,
11
-  format: "png" | "jpg" | "webm",
11
+  format: "png" | "jpg" | "webm" | "raw",
12
   quality: number,
12
   quality: number,
13
-  result: "tmpfile" | "base64" | "data-uri",
13
+  result: "tmpfile" | "base64" | "data-uri" | "zip-base64",
14
   snapshotContentContainer: boolean
14
   snapshotContentContainer: boolean
15
 };
15
 };
16
 
16
 
21
 }
21
 }
22
 
22
 
23
 const acceptedFormats = ["png", "jpg"].concat(
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
 const defaultOptions = {
31
 const defaultOptions = {
30
   format: "png",
32
   format: "png",
70
   if (acceptedFormats.indexOf(options.format) === -1) {
72
   if (acceptedFormats.indexOf(options.format) === -1) {
71
     options.format = defaultOptions.format;
73
     options.format = defaultOptions.format;
72
     errors.push(
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
   if (acceptedResults.indexOf(options.result) === -1) {
78
   if (acceptedResults.indexOf(options.result) === -1) {
77
     options.result = defaultOptions.result;
79
     options.result = defaultOptions.result;
78
     errors.push(
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
   return { options, errors };
84
   return { options, errors };