Преглед изворни кода

implemented super fast way of screenshot making. removed places that slow down screenshot making and delivery to the react side.

Oleksandr Kucherenko пре 6 година
родитељ
комит
a999019bc2
1 измењених фајлова са 381 додато и 79 уклоњено
  1. 381
    79
      android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java

+ 381
- 79
android/src/main/java/fr/greweb/reactnativeviewshot/ViewShot.java Прегледај датотеку

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