123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- package fr.greweb.reactnativeviewshot;
-
- import android.app.Activity;
- import android.graphics.Bitmap;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Matrix;
- import android.graphics.Paint;
- import android.graphics.Point;
- import android.net.Uri;
- import android.support.annotation.IntDef;
- import android.support.annotation.NonNull;
- import android.support.annotation.StringDef;
- import android.util.Base64;
- import android.util.Log;
- import android.view.TextureView;
- import android.view.View;
- import android.view.ViewGroup;
- import android.widget.ScrollView;
-
- import com.facebook.react.bridge.Promise;
- import com.facebook.react.bridge.ReactApplicationContext;
- import com.facebook.react.uimanager.NativeViewHierarchyManager;
- import com.facebook.react.uimanager.UIBlock;
-
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.OutputStream;
- import java.nio.ByteBuffer;
- import java.nio.charset.Charset;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Locale;
- import java.util.Set;
- import java.util.WeakHashMap;
- import java.util.zip.Deflater;
-
- import javax.annotation.Nullable;
-
- import static android.view.View.VISIBLE;
-
- /**
- * Snapshot utility class allow to screenshot a view.
- */
- public class ViewShot implements UIBlock {
- //region Constants
- /**
- * Tag fort Class logs.
- */
- private static final String TAG = ViewShot.class.getSimpleName();
- /**
- * Error code that we return to RN.
- */
- public static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
- /**
- * pre-allocated output stream size for screenshot. In real life example it will eb around 7Mb.
- */
- private static final int PREALLOCATE_SIZE = 64 * 1024;
- /**
- * ARGB size in bytes.
- */
- private static final int ARGB_SIZE = 4;
-
- @SuppressWarnings("WeakerAccess")
- @IntDef({Formats.JPEG, Formats.PNG, Formats.WEBP, Formats.RAW})
- public @interface Formats {
- int JPEG = 0; // Bitmap.CompressFormat.JPEG.ordinal();
- int PNG = 1; // Bitmap.CompressFormat.PNG.ordinal();
- int WEBP = 2; // Bitmap.CompressFormat.WEBP.ordinal();
- int RAW = -1;
-
- Bitmap.CompressFormat[] mapping = {
- Bitmap.CompressFormat.JPEG,
- Bitmap.CompressFormat.PNG,
- Bitmap.CompressFormat.WEBP
- };
- }
-
- /**
- * Supported Output results.
- */
- @StringDef({Results.BASE_64, Results.DATA_URI, Results.TEMP_FILE, Results.ZIP_BASE_64})
- public @interface Results {
- /**
- * Save screenshot as temp file on device.
- */
- String TEMP_FILE = "tmpfile";
- /**
- * Base 64 encoded image.
- */
- String BASE_64 = "base64";
- /**
- * Zipped RAW image in base 64 encoding.
- */
- String ZIP_BASE_64 = "zip-base64";
- /**
- * Base64 data uri.
- */
- String DATA_URI = "data-uri";
- }
- //endregion
-
- //region Static members
- /**
- * Image output buffer used as a source for base64 encoding
- */
- private static byte[] outputBuffer = new byte[PREALLOCATE_SIZE];
- //endregion
-
- //region Class members
- private final int tag;
- private final String extension;
- @Formats
- private final int format;
- private final double quality;
- private final Integer width;
- private final Integer height;
- private final File output;
- @Results
- private final String result;
- private final Promise promise;
- private final Boolean snapshotContentContainer;
- @SuppressWarnings({"unused", "FieldCanBeLocal"})
- private final ReactApplicationContext reactContext;
- private final Activity currentActivity;
- //endregion
-
- //region Constructors
- @SuppressWarnings("WeakerAccess")
- public ViewShot(
- final int tag,
- final String extension,
- @Formats final int format,
- final double quality,
- @Nullable Integer width,
- @Nullable Integer height,
- final File output,
- @Results final String result,
- final Boolean snapshotContentContainer,
- final ReactApplicationContext reactContext,
- final Activity currentActivity,
- final Promise promise) {
- this.tag = tag;
- this.extension = extension;
- this.format = format;
- this.quality = quality;
- this.width = width;
- this.height = height;
- this.output = output;
- this.result = result;
- this.snapshotContentContainer = snapshotContentContainer;
- this.reactContext = reactContext;
- this.currentActivity = currentActivity;
- this.promise = promise;
- }
- //endregion
-
- //region Overrides
- @Override
- public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
- final View view;
-
- if (tag == -1) {
- view = currentActivity.getWindow().getDecorView().findViewById(android.R.id.content);
- } else {
- view = nativeViewHierarchyManager.resolveView(tag);
- }
-
- if (view == null) {
- Log.e(TAG, "No view found with reactTag: " + tag, new AssertionError());
- promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "No view found with reactTag: " + tag);
- return;
- }
-
- try {
- final ReusableByteArrayOutputStream stream = new ReusableByteArrayOutputStream(outputBuffer);
- stream.setSize(proposeSize(view));
- outputBuffer = stream.innerBuffer();
-
- if (Results.TEMP_FILE.equals(result) && Formats.RAW == this.format) {
- saveToRawFileOnDevice(view);
- } else if (Results.TEMP_FILE.equals(result) && Formats.RAW != this.format) {
- saveToTempFileOnDevice(view);
- } else if (Results.BASE_64.equals(result) || Results.ZIP_BASE_64.equals(result)) {
- saveToBase64String(view);
- } else if (Results.DATA_URI.equals(result)) {
- saveToDataUriString(view);
- }
- } catch (final Throwable ex) {
- Log.e(TAG, "Failed to capture view snapshot", ex);
- promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to capture view snapshot");
- }
- }
- //endregion
-
- //region Implementation
- private void saveToTempFileOnDevice(@NonNull final View view) throws IOException {
- final FileOutputStream fos = new FileOutputStream(output);
- captureView(view, fos);
-
- promise.resolve(Uri.fromFile(output).toString());
- }
-
- private void saveToRawFileOnDevice(@NonNull final View view) throws IOException {
- final String uri = Uri.fromFile(output).toString();
-
- final FileOutputStream fos = new FileOutputStream(output);
- final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
- final Point size = captureView(view, os);
-
- // in case of buffer grow that will be a new array with bigger size
- outputBuffer = os.innerBuffer();
- final int length = os.size();
- final String resolution = String.format(Locale.US, "%d:%d|", size.x, size.y);
-
- fos.write(resolution.getBytes(Charset.forName("US-ASCII")));
- fos.write(outputBuffer, 0, length);
- fos.close();
-
- promise.resolve(uri);
- }
-
- private void saveToDataUriString(@NonNull final View view) throws IOException {
- final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
- captureView(view, os);
-
- outputBuffer = os.innerBuffer();
- final int length = os.size();
-
- final String data = Base64.encodeToString(outputBuffer, 0, length, Base64.NO_WRAP);
-
- // correct the extension if JPG
- final String imageFormat = "jpg".equals(extension) ? "jpeg" : extension;
-
- promise.resolve("data:image/" + imageFormat + ";base64," + data);
- }
-
- private void saveToBase64String(@NonNull final View view) throws IOException {
- final boolean isRaw = Formats.RAW == this.format;
- final boolean isZippedBase64 = Results.ZIP_BASE_64.equals(this.result);
-
- final ReusableByteArrayOutputStream os = new ReusableByteArrayOutputStream(outputBuffer);
- final Point size = captureView(view, os);
-
- // in case of buffer grow that will be a new array with bigger size
- outputBuffer = os.innerBuffer();
- final int length = os.size();
- final String resolution = String.format(Locale.US, "%d:%d|", size.x, size.y);
- final String header = (isRaw ? resolution : "");
- final String data;
-
- if (isZippedBase64) {
- final Deflater deflater = new Deflater();
- deflater.setInput(outputBuffer, 0, length);
- deflater.finish();
-
- final ReusableByteArrayOutputStream zipped = new ReusableByteArrayOutputStream(new byte[32]);
- byte[] buffer = new byte[1024];
- while (!deflater.finished()) {
- int count = deflater.deflate(buffer); // returns the generated code... index
- zipped.write(buffer, 0, count);
- }
-
- data = header + Base64.encodeToString(zipped.innerBuffer(), 0, zipped.size(), Base64.NO_WRAP);
- } else {
- data = header + Base64.encodeToString(outputBuffer, 0, length, Base64.NO_WRAP);
- }
-
- promise.resolve(data);
- }
-
- @NonNull
- private List<View> getAllChildren(@NonNull final View v) {
- if (!(v instanceof ViewGroup)) {
- final ArrayList<View> viewArrayList = new ArrayList<>();
- viewArrayList.add(v);
-
- return viewArrayList;
- }
-
- final ArrayList<View> result = new ArrayList<>();
-
- ViewGroup viewGroup = (ViewGroup) v;
- for (int i = 0; i < viewGroup.getChildCount(); i++) {
- View child = viewGroup.getChildAt(i);
-
- //Do not add any parents, just add child elements
- result.addAll(getAllChildren(child));
- }
-
- return result;
- }
-
- /**
- * Wrap {@link #captureViewImpl(View, OutputStream)} call and on end close output stream.
- */
- private Point captureView(@NonNull final View view, @NonNull final OutputStream os) throws IOException {
- try {
- DebugViews.longDebug(TAG, DebugViews.logViewHierarchy(this.currentActivity));
-
- return captureViewImpl(view, os);
- } finally {
- os.close();
- }
- }
-
- /**
- * Screenshot a view and return the captured bitmap.
- *
- * @param view the view to capture
- * @return screenshot resolution, Width * Height
- */
- private Point captureViewImpl(@NonNull final View view, @NonNull final OutputStream os) {
- int w = view.getWidth();
- int h = view.getHeight();
-
- if (w <= 0 || h <= 0) {
- throw new RuntimeException("Impossible to snapshot the view: view is invalid");
- }
-
- // evaluate real height
- if (snapshotContentContainer) {
- h = 0;
- ScrollView scrollView = (ScrollView) view;
- for (int i = 0; i < scrollView.getChildCount(); i++) {
- h += scrollView.getChildAt(i).getHeight();
- }
- }
-
- final Point resolution = new Point(w, h);
- Bitmap bitmap = getBitmapForScreenshot(w, h);
-
- final Paint paint = new Paint();
- paint.setAntiAlias(true);
- paint.setFilterBitmap(true);
- paint.setDither(true);
-
- // Uncomment next line if you want to wait attached android studio debugger:
- // Debug.waitForDebugger();
-
- final Canvas c = new Canvas(bitmap);
- view.draw(c);
-
- //after view is drawn, go through children
- final List<View> childrenList = getAllChildren(view);
-
- for (final View child : childrenList) {
- // skip any child that we don't know how to process
- if (!(child instanceof TextureView)) continue;
-
- // skip all invisible to user child views
- if (child.getVisibility() != VISIBLE) continue;
-
- final TextureView tvChild = (TextureView) child;
- tvChild.setOpaque(false); // <-- switch off background fill
-
- // NOTE (olku): get re-usable bitmap. TextureView should use bitmaps with matching size,
- // otherwise content of the TextureView will be scaled to provided bitmap dimensions
- final Bitmap childBitmapBuffer = tvChild.getBitmap(getExactBitmapForScreenshot(child.getWidth(), child.getHeight()));
-
- final int countCanvasSave = c.save();
- applyTransformations(c, view, child);
-
- // due to re-use of bitmaps for screenshot, we can get bitmap that is bigger in size than requested
- c.drawBitmap(childBitmapBuffer, 0, 0, paint);
-
- c.restoreToCount(countCanvasSave);
- recycleBitmap(childBitmapBuffer);
- }
-
- if (width != null && height != null && (width != w || height != h)) {
- final Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
- recycleBitmap(bitmap);
-
- bitmap = scaledBitmap;
- }
-
- // special case, just save RAW ARGB array without any compression
- if (Formats.RAW == this.format && os instanceof ReusableByteArrayOutputStream) {
- final int total = w * h * ARGB_SIZE;
- final ReusableByteArrayOutputStream rbaos = cast(os);
- bitmap.copyPixelsToBuffer(rbaos.asBuffer(total));
- rbaos.setSize(total);
- } else {
- final Bitmap.CompressFormat cf = Formats.mapping[this.format];
-
- bitmap.compress(cf, (int) (100.0 * quality), os);
- }
-
- recycleBitmap(bitmap);
-
- return resolution; // return image width and height
- }
-
- /**
- * Concat all the transformation matrix's from parent to child.
- */
- @NonNull
- @SuppressWarnings("UnusedReturnValue")
- private Matrix applyTransformations(final Canvas c, @NonNull final View root, @NonNull final View child) {
- final Matrix transform = new Matrix();
- final LinkedList<View> ms = new LinkedList<>();
-
- // find all parents of the child view
- View iterator = child;
- do {
- ms.add(iterator);
-
- iterator = (View) iterator.getParent();
- } while (iterator != root);
-
- // apply transformations from parent --> child order
- Collections.reverse(ms);
-
- for (final View v : ms) {
- c.save();
-
- // apply each view transformations, so each child will be affected by them
- final float dx = v.getLeft() + ((v != child) ? v.getPaddingLeft() : 0) + v.getTranslationX();
- final float dy = v.getTop() + ((v != child) ? v.getPaddingTop() : 0) + v.getTranslationY();
- c.translate(dx, dy);
- c.rotate(v.getRotation(), v.getPivotX(), v.getPivotY());
- c.scale(v.getScaleX(), v.getScaleY());
-
- // compute the matrix just for any future use
- transform.postTranslate(dx, dy);
- transform.postRotate(v.getRotation(), v.getPivotX(), v.getPivotY());
- transform.postScale(v.getScaleX(), v.getScaleY());
- }
-
- return transform;
- }
-
- @SuppressWarnings("unchecked")
- private static <T extends A, A> T cast(final A instance) {
- return (T) instance;
- }
- //endregion
-
- //region Cache re-usable bitmaps
- /**
- * Synchronization guard.
- */
- private static final Object guardBitmaps = new Object();
- /**
- * Reusable bitmaps for screenshots.
- */
- private static final Set<Bitmap> weakBitmaps = Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
-
- /**
- * Propose allocation size of the array output stream.
- */
- private static int proposeSize(@NonNull final View view) {
- final int w = view.getWidth();
- final int h = view.getHeight();
-
- return Math.min(w * h * ARGB_SIZE, 32);
- }
-
- /**
- * Return bitmap to set of available.
- */
- private static void recycleBitmap(@NonNull final Bitmap bitmap) {
- synchronized (guardBitmaps) {
- weakBitmaps.add(bitmap);
- }
- }
-
- /**
- * Try to find a bitmap for screenshot in reusable set and if not found create a new one.
- */
- @NonNull
- private static Bitmap getBitmapForScreenshot(final int width, final int height) {
- synchronized (guardBitmaps) {
- for (final Bitmap bmp : weakBitmaps) {
- if (bmp.getWidth() == width && bmp.getHeight() == height) {
- weakBitmaps.remove(bmp);
- bmp.eraseColor(Color.TRANSPARENT);
- return bmp;
- }
- }
- }
-
- return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- }
-
- /**
- * Try to find a bitmap with exact width and height for screenshot in reusable set and if
- * not found create a new one.
- */
- @NonNull
- private static Bitmap getExactBitmapForScreenshot(final int width, final int height) {
- synchronized (guardBitmaps) {
- for (final Bitmap bmp : weakBitmaps) {
- if (bmp.getWidth() == width && bmp.getHeight() == height) {
- weakBitmaps.remove(bmp);
- bmp.eraseColor(Color.TRANSPARENT);
- return bmp;
- }
- }
- }
-
- return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- }
- //endregion
-
- //region Nested declarations
-
- /**
- * Stream that can re-use pre-allocated buffer.
- */
- @SuppressWarnings("WeakerAccess")
- public static class ReusableByteArrayOutputStream extends ByteArrayOutputStream {
- private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
-
- public ReusableByteArrayOutputStream(@NonNull final byte[] buffer) {
- super(0);
-
- this.buf = buffer;
- }
-
- /**
- * Get access to inner buffer without any memory copy operations.
- */
- public byte[] innerBuffer() {
- return this.buf;
- }
-
- @NonNull
- public ByteBuffer asBuffer(final int size) {
- if (this.buf.length < size) {
- grow(size);
- }
-
- return ByteBuffer.wrap(this.buf);
- }
-
- public void setSize(final int size) {
- this.count = size;
- }
-
- /**
- * Increases the capacity to ensure that it can hold at least the
- * number of elements specified by the minimum capacity argument.
- *
- * @param minCapacity the desired minimum capacity
- */
- protected void grow(int minCapacity) {
- // overflow-conscious code
- int oldCapacity = buf.length;
- int newCapacity = oldCapacity << 1;
- if (newCapacity - minCapacity < 0)
- newCapacity = minCapacity;
- if (newCapacity - MAX_ARRAY_SIZE > 0)
- newCapacity = hugeCapacity(minCapacity);
- buf = Arrays.copyOf(buf, newCapacity);
- }
-
- protected static int hugeCapacity(int minCapacity) {
- if (minCapacity < 0) // overflow
- throw new OutOfMemoryError();
-
- return (minCapacity > MAX_ARRAY_SIZE) ?
- Integer.MAX_VALUE :
- MAX_ARRAY_SIZE;
- }
-
- }
- //endregion
-
- }
|