Skip to content

Instantly share code, notes, and snippets.

@gre
Last active August 22, 2016 15:51
Show Gist options
  • Save gre/c466944b8ee7c1b1f17ba79f145d0664 to your computer and use it in GitHub Desktop.
Save gre/c466944b8ee7c1b1f17ba79f145d0664 to your computer and use it in GitHub Desktop.

This is a PoC that will be later a PR to react-native-view-snapshot to implement the feature for Android.

We still need to wait React Native to have the addUIBlock feature provided by this PR.

Also, I'll wait react-native-view-snapshot to implement the same API as the current RN takeSnapshot on iOS allowing the options.

Usage

import {NativeModules, findNodeHandle} from "react-native";
NativeModules.ViewSnapshot
.takeSnapshot(findNodeHandle(aRef), {format: "jpeg", quality: 0.8 })
.then((uri) => this.setState({uri}))
.catch((error) => alert(error));
package com.github.jsierles.reactnativeviewsnapshot;
import javax.annotation.Nullable;
import android.graphics.Bitmap;
import android.net.Uri;
import android.view.View;
import com.facebook.react.bridge.Promise;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Snapshot utility class allow to screenshot a view.
*/
public class ViewSnapshot implements UIBlock {
static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT";
private int tag;
private Bitmap.CompressFormat format;
private double quality;
private Integer width;
private Integer height;
private File output;
private Promise promise;
public ViewSnapshot(
int tag,
Bitmap.CompressFormat format,
double quality,
@Nullable Integer width,
@Nullable Integer height,
File output,
Promise promise) {
this.tag = tag;
this.format = format;
this.quality = quality;
this.width = width;
this.height = height;
this.output = output;
this.promise = promise;
}
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
FileOutputStream fos = null;
View view = nativeViewHierarchyManager.resolveView(tag);
try {
fos = new FileOutputStream(output);
captureView(view, fos);
String uri = Uri.fromFile(output).toString();
promise.resolve(uri);
}
catch (Exception e) {
promise.reject(ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
}
finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* Screenshot a view and return the captured bitmap.
* @param view the view to capture
* @return the screenshot or null if it failed.
*/
private void captureView (View view, FileOutputStream fos) {
int w = view.getWidth();
int h = view.getHeight();
if (w <= 0 || h <= 0) {
throw new RuntimeException("Impossible to snapshot the view: view is invalid");
}
Bitmap bitmap = view.getDrawingCache();
if (bitmap == null)
view.setDrawingCacheEnabled(true);
bitmap = view.getDrawingCache();
if (width != null && height != null && (width != w || height != h)) {
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
}
if (bitmap == null) {
throw new RuntimeException("Impossible to snapshot the view");
}
bitmap.compress(format, (int)(100.0 * quality), fos);
}
}
package com.github.jsierles.reactnativeviewsnapshot;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.util.DisplayMetrics;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
public class ViewSnapshotModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
public ViewSnapshotModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "ViewSnapshot";
}
@Override
public void onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy();
new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@ReactMethod
public void takeSnapshot(int tag, ReadableMap options, Promise promise) {
ReactApplicationContext context = getReactApplicationContext();
String format = options.hasKey("format") ? options.getString("format") : "png";
Bitmap.CompressFormat compressFormat =
format.equals("png")
? Bitmap.CompressFormat.PNG
: format.equals("jpg")||format.equals("jpeg")
? Bitmap.CompressFormat.JPEG
: format.equals("webm")
? Bitmap.CompressFormat.WEBP
: null;
if (compressFormat == null) {
throw new JSApplicationIllegalArgumentException("Unsupported image format: " + format);
}
double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0;
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null;
try {
File tmpFile = createTempFile(getReactApplicationContext(), format);
UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new ViewSnapshot(tag, compressFormat, quality, width, height, tmpFile, promise));
}
catch (Exception e) {
promise.reject(ViewSnapshot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
}
}
private static final String TEMP_FILE_PREFIX = "ReactNative_snapshot_image_";
/**
* Asynchronous task that cleans up cache dirs (internal and, if available, external) of cropped
* image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
* down) and when the module is instantiated, to handle the case where the app crashed.
*/
private static class CleanTask extends GuardedAsyncTask<Void, Void> {
private final Context mContext;
private CleanTask(ReactContext context) {
super(context);
mContext = context;
}
@Override
protected void doInBackgroundGuarded(Void... params) {
cleanDirectory(mContext.getCacheDir());
File externalCacheDir = mContext.getExternalCacheDir();
if (externalCacheDir != null) {
cleanDirectory(externalCacheDir);
}
}
private void cleanDirectory(File directory) {
File[] toDelete = directory.listFiles(
new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.startsWith(TEMP_FILE_PREFIX);
}
});
if (toDelete != null) {
for (File file: toDelete) {
file.delete();
}
}
}
}
/**
* Create a temporary file in the cache directory on either internal or external storage,
* whichever is available and has more free space.
*/
private File createTempFile(Context context, String ext)
throws IOException {
File externalCacheDir = context.getExternalCacheDir();
File internalCacheDir = context.getCacheDir();
File cacheDir;
if (externalCacheDir == null && internalCacheDir == null) {
throw new IOException("No cache directory available");
}
if (externalCacheDir == null) {
cacheDir = internalCacheDir;
}
else if (internalCacheDir == null) {
cacheDir = externalCacheDir;
} else {
cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
externalCacheDir : internalCacheDir;
}
return File.createTempFile(TEMP_FILE_PREFIX, ext, cacheDir);
}
}
package com.github.jsierles.reactnativeviewsnapshot;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ViewSnapshotPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ViewSnapshotModule(reactContext));
return modules;
}
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment