From 0e0a196b89456acfeb60bd4c8273818dda1f8e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Sat, 12 Mar 2016 15:28:27 +0100 Subject: [PATCH 1/4] [Android] Implement UIManager.takeSnapshot --- Examples/UIExplorer/SnapshotExample.js | 37 ++++++- Examples/UIExplorer/UIExplorerList.android.js | 4 + Libraries/Utilities/UIManager.js | 9 +- React/Modules/RCTUIManager.m | 11 +- .../uimanager/NativeViewHierarchyManager.java | 6 ++ .../facebook/react/uimanager/Snapshot.java | 43 ++++++++ .../react/uimanager/UIImplementation.java | 24 +++++ .../react/uimanager/UIManagerModule.java | 100 ++++++++++++++++++ .../react/uimanager/UIViewOperationQueue.java | 37 +++++++ 9 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java diff --git a/Examples/UIExplorer/SnapshotExample.js b/Examples/UIExplorer/SnapshotExample.js index d843acee80d3..497c10a36ee6 100644 --- a/Examples/UIExplorer/SnapshotExample.js +++ b/Examples/UIExplorer/SnapshotExample.js @@ -33,29 +33,56 @@ var ScreenshotExample = React.createClass({ render() { return ( - + this._view = ref}> + - Click to take a screenshot + Click here to screenshot the App (JPG 50%) - + + Click here to screenshot this View (PNG) + + + ); }, takeScreenshot() { UIManager - .takeSnapshot('window', {format: 'jpeg', quality: 0.8}) // See UIManager.js for options + .takeSnapshot('window', {format: 'jpeg', quality: 0.5 }) // See UIManager.js for options + .then((uri) => this.setState({uri})) + .catch((error) => alert(error)); + }, + + takeScreenshot2() { + UIManager + .takeSnapshot(this._view) .then((uri) => this.setState({uri})) .catch((error) => alert(error)); } }); var style = StyleSheet.create({ + root: { + backgroundColor: '#fff', + position: 'relative', + }, button: { - marginBottom: 10, + margin: 5, fontWeight: '500', + backgroundColor: 'transparent', }, image: { + width: 40, + height: 40, + position: 'absolute', + top: 0, + right: 0, + }, + result: { flex: 1, height: 300, resizeMode: 'contain', diff --git a/Examples/UIExplorer/UIExplorerList.android.js b/Examples/UIExplorer/UIExplorerList.android.js index 341dddc928c5..0249919d34ee 100644 --- a/Examples/UIExplorer/UIExplorerList.android.js +++ b/Examples/UIExplorer/UIExplorerList.android.js @@ -47,6 +47,10 @@ var ComponentExamples: Array = [ key: 'ScrollViewSimpleExample', module: require('./ScrollViewSimpleExample'), }, + { + key: 'SnapshotExample', + module: require('./SnapshotExample'), + }, { key: 'StatusBarExample', module: require('./StatusBarExample'), diff --git a/Libraries/Utilities/UIManager.js b/Libraries/Utilities/UIManager.js index efd8ce5b0728..b051a7019c57 100644 --- a/Libraries/Utilities/UIManager.js +++ b/Libraries/Utilities/UIManager.js @@ -49,15 +49,15 @@ const _takeSnapshot = UIManager.takeSnapshot; * Capture an image of the screen, window or an individual view. The image * will be stored in a temporary file that will only exist for as long as the * app is running. - * + * * The `view` argument can be the literal string `window` if you want to * capture the entire window, or it can be a reference to a specific * React Native component. * * The `options` argument may include: * - width/height (number) - the width and height of the image to capture. - * - format (string) - either 'png' or 'jpeg'. Defaults to 'png'. - * - quality (number) - the quality when using jpeg. 0.0 - 1.0 (default). + * - format (string) - either 'png' or 'jpg'/'jpeg' or 'webm' (Android). Defaults to 'png'. + * - quality (number) - the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpeg) * * Returns a Promise. * @platform ios @@ -78,7 +78,8 @@ UIManager.takeSnapshot = async function( if (typeof view !== 'number' && view !== 'window') { view = findNodeHandle(view) || 'window'; } - return _takeSnapshot(view, options); + const isTag = typeof view === 'number'; + return _takeSnapshot(isTag ? null : view, isTag ? view : 0, options || {}); }; module.exports = UIManager; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index d0db1c40aa5b..887ab6670281 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -1226,7 +1226,8 @@ static void RCTMeasureLayout(RCTShadowView *view, callback(@[results]); } -RCT_EXPORT_METHOD(takeSnapshot:(id /* NSString or NSNumber */)target +RCT_EXPORT_METHOD(takeSnapshot:(NSString *)target + withTag:(nonnull NSNumber *)tag withOptions:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) @@ -1235,10 +1236,10 @@ static void RCTMeasureLayout(RCTShadowView *view, // Get view UIView *view; - if (target == nil || [target isEqual:@"window"]) { + if (target && [target isEqual:@"window"]) { view = RCTKeyWindow(); - } else if ([target isKindOfClass:[NSNumber class]]) { - view = viewRegistry[target]; + } else { + view = viewRegistry[tag]; if (!view) { RCTLogError(@"No view found with reactTag: %@", target); return; @@ -1269,7 +1270,7 @@ static void RCTMeasureLayout(RCTShadowView *view, NSData *data; if ([format isEqualToString:@"png"]) { data = UIImagePNGRepresentation(image); - } else if ([format isEqualToString:@"jpeg"]) { + } else if ([format isEqualToString:@"jpeg"]||[format isEqualToString:@"jpg"]) { CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1]; data = UIImageJPEGRepresentation(image, quality); } else { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index b5939823acf2..7d1f685132a7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -12,6 +12,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; +import android.graphics.Bitmap; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.Menu; @@ -26,17 +27,22 @@ import com.facebook.react.animation.AnimationListener; import com.facebook.react.animation.AnimationRegistry; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationCausedNativeException; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.modules.camera.ImageEditingManager; import com.facebook.react.touch.JSResponderHandler; import com.facebook.react.uimanager.layoutanimation.LayoutAnimationController; import com.facebook.systrace.Systrace; import com.facebook.systrace.SystraceMessage; +import java.io.FileOutputStream; +import java.io.IOException; + /** * Delegate of {@link UIManagerModule} that owns the native view hierarchy and mapping between * native view names used in JS and corresponding instances of {@link ViewManager}. The diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java new file mode 100644 index 000000000000..c9dfa4bcc7dc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java @@ -0,0 +1,43 @@ +package com.facebook.react.uimanager; + +import android.graphics.Bitmap; +import android.support.annotation.Nullable; +import android.view.View; +import java.io.FileOutputStream; + +public class Snapshot { + + Bitmap.CompressFormat format; + double quality; + Integer width; + Integer height; + + public Snapshot(Bitmap.CompressFormat format, double quality, @Nullable Integer width, @Nullable Integer height ) { + this.format = format; + this.quality = quality; + this.width = width; + this.height = height; + } + + public void captureViewToFileOutputStream (View view, FileOutputStream out) { + Bitmap bitmap = captureView(view); + if (bitmap == null) { + throw new RuntimeException("Impossible to snapshot the view"); + } + bitmap.compress(format, (int)(100.0 * quality), out); + } + + public Bitmap captureView (View view) { + int w = view.getWidth(); + int h = view.getHeight(); + if (w <= 0 || h <= 0) return null; + Bitmap bitmap = view.getDrawingCache(); + if (bitmap == null) + view.setDrawingCacheEnabled(true); + bitmap = view.getDrawingCache(); + if (width != null && height != null && (width != w || height != h)) { + bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height); + } + return bitmap; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index b407c416a5a9..ff0d80672743 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -10,6 +10,7 @@ import javax.annotation.Nullable; +import java.io.File; import java.util.Arrays; import java.util.List; @@ -18,6 +19,8 @@ import com.facebook.react.animation.Animation; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; @@ -729,4 +732,25 @@ protected void applyUpdatesRecursive( } cssNode.markUpdateSeen(); } + + public void takeSnapshot(String target, int tag, Snapshot snapshot, File destFile, Promise promise) { + int t; + if (target == null) { + t = tag; + } + else { + if (target.equals("window")) { + t = mShadowNodeRegistry.getRootTag(0); + } + else { + throw new JSApplicationIllegalArgumentException("Invalid snapshot target: "+target); + } + } + mOperationsQueue.enqueueTakeSnapshot( + t, + snapshot, + destFile, + promise + ); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index ddeb94e6db2e..ec98b8cc978f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -9,16 +9,27 @@ package com.facebook.react.uimanager; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.AsyncTask; + import javax.annotation.Nullable; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; import java.util.List; import java.util.Map; import com.facebook.react.animation.Animation; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.GuardedAsyncTask; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.OnBatchCompleteListener; +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.ReadableArray; @@ -76,6 +87,7 @@ public UIManagerModule( List viewManagerList, UIImplementation uiImplementation) { super(reactContext); + new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); mEventDispatcher = new EventDispatcher(reactContext); mModuleConstants = createConstants(viewManagerList); mUIImplementation = uiImplementation; @@ -112,6 +124,7 @@ public void onHostDestroy() { public void onCatalystInstanceDestroy() { super.onCatalystInstanceDestroy(); mEventDispatcher.onCatalystInstanceDestroyed(); + new CleanTask(getReactApplicationContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private static Map createConstants(List viewManagerList) { @@ -449,4 +462,91 @@ public EventDispatcher getEventDispatcher() { public void sendAccessibilityEvent(int tag, int eventType) { mUIImplementation.sendAccessibilityEvent(tag, eventType); } + + @ReactMethod + public void takeSnapshot(String target, int tag, ReadableMap options, Promise promise) { + 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; + Integer width = options.hasKey("width") ? options.getInt("width") : null; + Integer height = options.hasKey("height") ? options.getInt("height") : null; + try { + File tmpFile = createTempFile(getReactApplicationContext(), format); + mUIImplementation.takeSnapshot(target, tag, new Snapshot(compressFormat, quality, width, height), tmpFile, promise); + } + catch (Exception e) { + promise.reject(e); + } + } + + + 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 { + 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); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index b3b931e0f8ef..ad8def86bd8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -9,9 +9,14 @@ package com.facebook.react.uimanager; +import android.net.Uri; +import android.view.View; + import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; +import java.io.File; +import java.io.FileOutputStream; import java.util.ArrayList; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -19,6 +24,7 @@ import com.facebook.react.animation.Animation; import com.facebook.react.animation.AnimationRegistry; import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.SoftAssertions; import com.facebook.react.bridge.ReactContext; @@ -491,6 +497,33 @@ public void execute() { } } + private final class TakeSnapshot extends ViewOperation { + private Snapshot snapshot; + private Promise promise; + private File destFile; + + public TakeSnapshot(int tag, Snapshot snapshot, File destFile, Promise promise) { + super(tag); + this.snapshot = snapshot; + this.promise = promise; + this.destFile = destFile; + } + @Override + public void execute() { + try { + View view = mNativeViewHierarchyManager.resolveView(mTag); + FileOutputStream fileOutputStream = new FileOutputStream(destFile); + snapshot.captureViewToFileOutputStream(view, fileOutputStream); + fileOutputStream.close(); + String uri = Uri.fromFile(destFile).toString(); + promise.resolve(uri); + } + catch (Exception e) { + promise.reject(e); + } + } + } + private final NativeViewHierarchyManager mNativeViewHierarchyManager; private final AnimationRegistry mAnimationRegistry; @@ -687,6 +720,10 @@ public void enqueueSendAccessibilityEvent(int tag, int eventType) { mOperations.add(new SendAccessibilityEvent(tag, eventType)); } + public void enqueueTakeSnapshot(int tag, Snapshot snapshot, File destFile, Promise promise) { + mOperations.add(new TakeSnapshot(tag, snapshot, destFile, promise)); + } + /* package */ void dispatchViewUpdates(final int batchId) { // Store the current operation queues to dispatch and create new empty ones to continue // receiving new operations From 5b9d73176f76352c749f20834704b4b26dde659b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Sat, 12 Mar 2016 18:35:28 +0100 Subject: [PATCH 2/4] fix warnings --- Examples/UIExplorer/SnapshotExample.js | 4 +++- .../facebook/react/uimanager/NativeViewHierarchyManager.java | 3 --- .../src/main/java/com/facebook/react/uimanager/Snapshot.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Examples/UIExplorer/SnapshotExample.js b/Examples/UIExplorer/SnapshotExample.js index 497c10a36ee6..d11fcc0006d4 100644 --- a/Examples/UIExplorer/SnapshotExample.js +++ b/Examples/UIExplorer/SnapshotExample.js @@ -25,6 +25,8 @@ var { } = React; var ScreenshotExample = React.createClass({ + _view: null, + getInitialState() { return { uri: undefined, @@ -33,7 +35,7 @@ var ScreenshotExample = React.createClass({ render() { return ( - this._view = ref}> + { this._view = ref; }}> Date: Sun, 13 Mar 2016 10:38:39 +0100 Subject: [PATCH 3/4] convert width/height to pixels & resize if needed --- Examples/UIExplorer/SnapshotExample.js | 16 +++++++++++++--- .../com/facebook/react/uimanager/Snapshot.java | 4 ++-- .../react/uimanager/UIManagerModule.java | 7 +++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Examples/UIExplorer/SnapshotExample.js b/Examples/UIExplorer/SnapshotExample.js index d11fcc0006d4..88dd663188e5 100644 --- a/Examples/UIExplorer/SnapshotExample.js +++ b/Examples/UIExplorer/SnapshotExample.js @@ -41,12 +41,14 @@ var ScreenshotExample = React.createClass({ style={style.image} /> - Click here to screenshot the App (JPG 50%) + Screenshot the App (JPG 50%) - Click here to screenshot this View (PNG) + Screenshot this View (PNG) + + + Screenshot the App & resize to 32x48 - ); @@ -64,6 +66,13 @@ var ScreenshotExample = React.createClass({ .takeSnapshot(this._view) .then((uri) => this.setState({uri})) .catch((error) => alert(error)); + }, + + takeScreenshot3() { + UIManager + .takeSnapshot('window', { width: 32, height: 48 }) + .then((uri) => this.setState({uri})) + .catch((error) => alert(error)); } }); @@ -76,6 +85,7 @@ var style = StyleSheet.create({ margin: 5, fontWeight: '500', backgroundColor: 'transparent', + textDecorationLine: 'underline' }, image: { width: 40, diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java index ba062cb1998f..976af4683638 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java @@ -12,7 +12,7 @@ public class Snapshot { Integer width; Integer height; - public Snapshot(Bitmap.CompressFormat format, double quality, @Nullable Integer width, @Nullable Integer height ) { + public Snapshot(Bitmap.CompressFormat format, double quality, @Nullable Integer width, @Nullable Integer height) { this.format = format; this.quality = quality; this.width = width; @@ -36,7 +36,7 @@ public Bitmap captureView (View view) { view.setDrawingCacheEnabled(true); bitmap = view.getDrawingCache(); if (width != null && height != null && (width != w || height != h)) { - bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height); + bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); } return bitmap; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index ec98b8cc978f..2f42a7ef1b89 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -12,6 +12,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.os.AsyncTask; +import android.util.DisplayMetrics; import javax.annotation.Nullable; @@ -465,6 +466,7 @@ public void sendAccessibilityEvent(int tag, int eventType) { @ReactMethod public void takeSnapshot(String target, 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 : @@ -475,8 +477,9 @@ public void takeSnapshot(String target, int tag, ReadableMap options, Promise pr throw new JSApplicationIllegalArgumentException("Unsupported image format: " + format); } double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0; - Integer width = options.hasKey("width") ? options.getInt("width") : null; - Integer height = options.hasKey("height") ? options.getInt("height") : null; + 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); mUIImplementation.takeSnapshot(target, tag, new Snapshot(compressFormat, quality, width, height), tmpFile, promise); From 4177bcef3f1393a125229e822192f6fd5ee642d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Fri, 18 Mar 2016 20:56:30 +0100 Subject: [PATCH 4/4] Address feedbacks and more javadoc --- .../facebook/react/uimanager/Snapshot.java | 15 ++++++++++++ .../react/uimanager/UIManagerModule.java | 2 +- .../react/uimanager/UIViewOperationQueue.java | 23 ++++++++++++++++--- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java index 976af4683638..c8fbe01ab73c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/Snapshot.java @@ -5,8 +5,13 @@ import android.view.View; import java.io.FileOutputStream; +/** + * Snapshot utility class allow to screenshot a view. + */ public class Snapshot { + static final String ERROR_UNABLE_TO_SNAPSHOT = "E_UNABLE_TO_SNAPSHOT"; + Bitmap.CompressFormat format; double quality; Integer width; @@ -19,6 +24,11 @@ public Snapshot(Bitmap.CompressFormat format, double quality, @Nullable Integer this.height = height; } + /** + * Write the captured image to a file output stream. + * @param view the view to screenshot + * @param out the file output stream to write + */ public void captureViewToFileOutputStream (View view, FileOutputStream out) { Bitmap bitmap = captureView(view); if (bitmap == null) { @@ -27,6 +37,11 @@ public void captureViewToFileOutputStream (View view, FileOutputStream out) { bitmap.compress(format, (int)(100.0 * quality), out); } + /** + * Screenshot a view and return the captured bitmap. + * @param view the view to capture + * @return the screenshot or null if it failed. + */ public Bitmap captureView (View view) { int w = view.getWidth(); int h = view.getHeight(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index 2f42a7ef1b89..0e5821641d5e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -485,7 +485,7 @@ public void takeSnapshot(String target, int tag, ReadableMap options, Promise pr mUIImplementation.takeSnapshot(target, tag, new Snapshot(compressFormat, quality, width, height), tmpFile, promise); } catch (Exception e) { - promise.reject(e); + promise.reject(Snapshot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index ad8def86bd8d..f60db327f7b3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -17,6 +17,7 @@ import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -502,6 +503,13 @@ private final class TakeSnapshot extends ViewOperation { private Promise promise; private File destFile; + /** + * Create a TakeSnapshot operation that will screenshot a view and save it to a file. + * @param tag the view to snapshot + * @param snapshot the snapshot instance (configure the snapshot and provide utility methods) + * @param destFile the output file + * @param promise will be resolved with the uri (or rejected) + */ public TakeSnapshot(int tag, Snapshot snapshot, File destFile, Promise promise) { super(tag); this.snapshot = snapshot; @@ -510,16 +518,25 @@ public TakeSnapshot(int tag, Snapshot snapshot, File destFile, Promise promise) } @Override public void execute() { + FileOutputStream fileOutputStream = null; try { View view = mNativeViewHierarchyManager.resolveView(mTag); - FileOutputStream fileOutputStream = new FileOutputStream(destFile); + fileOutputStream = new FileOutputStream(destFile); snapshot.captureViewToFileOutputStream(view, fileOutputStream); - fileOutputStream.close(); String uri = Uri.fromFile(destFile).toString(); promise.resolve(uri); } catch (Exception e) { - promise.reject(e); + promise.reject(Snapshot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+mTag); + } + finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } }