Added support for taking snapshots of the screen, window or individual views

Summary:This adds a `takeSnapshot` method to UIManager that can be used to capture screenshots as an image.

The takeSnapshot method accepts either 'screen', 'window' or a view ref as an argument.

You can also specify the size, format and quality of the captured image.

I've added an example of capturing a screenshot at UIExplorer > Snapshot / Screenshot.
I've also added an example of sharing a screenshot to the UIExplorer > ActionSheetIOS demo.

Reviewed By: javache

Differential Revision: D2958351

fb-gh-sync-id: d2eb93fea3297ec5aaa312854dd6add724a7f4f8
shipit-source-id: d2eb93fea3297ec5aaa312854dd6add724a7f4f8
This commit is contained in:
Nick Lockwood 2016-02-23 02:26:11 -08:00 committed by facebook-github-bot-7
parent 0513d3abb5
commit ac12f98689
8 changed files with 288 additions and 5 deletions

View File

@ -20,6 +20,7 @@ var {
ActionSheetIOS, ActionSheetIOS,
StyleSheet, StyleSheet,
Text, Text,
UIManager,
View, View,
} = React; } = React;
@ -127,9 +128,7 @@ var ShareActionSheetExample = React.createClass({
'com.apple.UIKit.activity.PostToTwitter' 'com.apple.UIKit.activity.PostToTwitter'
] ]
}, },
(error) => { (error) => alert(error),
console.error(error);
},
(success, method) => { (success, method) => {
var text; var text;
if (success) { if (success) {
@ -142,6 +141,50 @@ var ShareActionSheetExample = React.createClass({
} }
}); });
var ShareScreenshotExample = React.createClass({
getInitialState() {
return {
text: ''
};
},
render() {
return (
<View>
<Text onPress={this.showShareActionSheet} style={style.button}>
Click to show the Share ActionSheet
</Text>
<Text>
{this.state.text}
</Text>
</View>
);
},
showShareActionSheet() {
// Take the snapshot (returns a temp file uri)
UIManager.takeSnapshot('window').then((uri) => {
// Share image data
ActionSheetIOS.showShareActionSheetWithOptions({
url: uri,
excludedActivityTypes: [
'com.apple.UIKit.activity.PostToTwitter'
]
},
(error) => alert(error),
(success, method) => {
var text;
if (success) {
text = `Shared via ${method}`;
} else {
text = 'You didn\'t share';
}
this.setState({text});
});
}).catch((error) => alert(error));
}
});
var style = StyleSheet.create({ var style = StyleSheet.create({
button: { button: {
marginBottom: 10, marginBottom: 10,
@ -166,10 +209,16 @@ exports.examples = [
return <ShareActionSheetExample url="https://code.facebook.com" />; return <ShareActionSheetExample url="https://code.facebook.com" />;
} }
}, },
{ {
title: 'Share Local Image', title: 'Share Local Image',
render(): ReactElement { render(): ReactElement {
return <ShareActionSheetExample url="bunny.png" />; return <ShareActionSheetExample url="bunny.png" />;
} }
},
{
title: 'Share Screenshot',
render(): ReactElement {
return <ShareScreenshotExample />;
}
} }
]; ];

View File

@ -0,0 +1,73 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';
var React = require('react-native');
var {
Image,
StyleSheet,
Text,
UIManager,
View,
} = React;
var ScreenshotExample = React.createClass({
getInitialState() {
return {
uri: undefined,
};
},
render() {
return (
<View>
<Text onPress={this.takeScreenshot} style={style.button}>
Click to take a screenshot
</Text>
<Image style={style.image} source={{uri: this.state.uri}}/>
</View>
);
},
takeScreenshot() {
UIManager
.takeSnapshot('screen', {format: 'jpeg', quality: 0.8}) // See UIManager.js for options
.then((uri) => this.setState({uri}))
.catch((error) => alert(error));
}
});
var style = StyleSheet.create({
button: {
marginBottom: 10,
fontWeight: '500',
},
image: {
flex: 1,
height: 300,
resizeMode: 'contain',
backgroundColor: 'black',
},
});
exports.title = 'Snapshot / Screenshot';
exports.description = 'API to capture images from the screen.';
exports.examples = [
{
title: 'Take screenshot',
render(): ReactElement { return <ScreenshotExample />; }
},
];

View File

@ -220,6 +220,10 @@ var APIExamples: Array<UIExplorerExample> = [
key: 'RCTRootViewIOSExample', key: 'RCTRootViewIOSExample',
module: require('./RCTRootViewIOSExample'), module: require('./RCTRootViewIOSExample'),
}, },
{
key: 'SnapshotExample',
module: require('./SnapshotExample'),
},
{ {
key: 'StatusBarIOSExample', key: 'StatusBarIOSExample',
module: require('./StatusBarIOSExample'), module: require('./StatusBarIOSExample'),

View File

@ -12,6 +12,7 @@
'use strict'; 'use strict';
var UIManager = require('NativeModules').UIManager; var UIManager = require('NativeModules').UIManager;
var findNodeHandle = require('findNodeHandle');
if (!UIManager.setChildren) { if (!UIManager.setChildren) {
@ -39,7 +40,45 @@ if (!UIManager.setChildren) {
UIManager.setChildren = function(containerTag, createdTags) { UIManager.setChildren = function(containerTag, createdTags) {
var indexes = this._cachedIndexArray(createdTags.length); var indexes = this._cachedIndexArray(createdTags.length);
UIManager.manageChildren(containerTag, null, null, createdTags, indexes, null); UIManager.manageChildren(containerTag, null, null, createdTags, indexes, null);
} };
} }
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 `screen` or `window` if you
* want to capture the entire screen, 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).
*
* Returns a Promise.
* @platform ios
*/
UIManager.takeSnapshot = async function(
view ?: 'screen' | 'window' | ReactElement | number,
options ?: {
width ?: number;
height ?: number;
format ?: 'png' | 'jpeg';
quality ?: number;
},
) {
if (!_takeSnapshot) {
console.warn('UIManager.takeSnapshot is not available on this platform');
return;
}
if (typeof view !== 'number' && view !== 'screen' && view !== 'window') {
view = findNodeHandle(view) || 'screen';
}
return _takeSnapshot(view, options);
};
module.exports = UIManager; module.exports = UIManager;

View File

@ -109,6 +109,9 @@ RCT_EXTERN NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL);
// Determines if a given image URL actually refers to an XCAsset // Determines if a given image URL actually refers to an XCAsset
RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL); RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL);
// Creates a new, unique temporary file path with the specified extension
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *__nullable extension, NSError **error);
// Converts a CGColor to a hex string // Converts a CGColor to a hex string
RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color); RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);

View File

@ -589,6 +589,51 @@ BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL)
return YES; return YES;
} }
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **error)
{
static NSError *setupError = nil;
static NSString *directory;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ReactNative"];
// If the temporary directory already exists, we'll delete it to ensure
// that temp files from the previous run have all been deleted. This is not
// a security measure, it simply prevents the temp directory from using too
// much space, as the circumstances under which iOS clears it automatically
// are not well-defined.
NSFileManager *fileManager = [NSFileManager new];
if ([fileManager fileExistsAtPath:directory]) {
[fileManager removeItemAtPath:directory error:NULL];
}
if (![fileManager fileExistsAtPath:directory]) {
NSError *localError = nil;
if (![fileManager createDirectoryAtPath:directory
withIntermediateDirectories:YES
attributes:nil
error:&localError]) {
// This is bad
RCTLogError(@"Failed to create temporary directory: %@", localError);
setupError = localError;
directory = nil;
}
}
});
if (!directory || setupError) {
if (error) {
*error = setupError;
}
return nil;
}
// Append a unique filename
NSString *filename = [NSUUID new].UUIDString;
if (extension) {
filename = [filename stringByAppendingPathExtension:extension];
}
return [directory stringByAppendingPathComponent:filename];
}
static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4]) static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4])
{ {
CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color)); CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color));

View File

@ -1182,6 +1182,75 @@ RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
callback(@[results]); callback(@[results]);
} }
RCT_EXPORT_METHOD(takeSnapshot:(id /* NSString or NSNumber */)target
withOptions:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
// Get view
UIView *view;
if (target == nil || [target isEqual:@"screen"]) {
view = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:YES];
} else if ([target isEqual:@"window"]) {
view = RCTKeyWindow();
} else if ([target isKindOfClass:[NSNumber class]]) {
view = viewRegistry[target];
if (!view) {
RCTLogError(@"No view found with reactTag: %@", target);
return;
}
}
// Get options
CGSize size = [RCTConvert CGSize:options];
NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
// Capture image
if (size.width < 0.1 || size.height < 0.1) {
size = view.bounds.size;
}
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (!success || !image) {
reject(RCTErrorUnspecified, @"Failed to capture view snapshot", nil);
return;
}
// Convert image to data (on a background thread)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data;
if ([format isEqualToString:@"png"]) {
data = UIImagePNGRepresentation(image);
} else if ([format isEqualToString:@"jpeg"]) {
CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
data = UIImageJPEGRepresentation(image, quality);
} else {
RCTLogError(@"Unsupported image format: %@", format);
return;
}
// Save to a temp file
NSError *error = nil;
NSString *tempFilePath = RCTTempFilePath(format, &error);
if (tempFilePath) {
if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) {
resolve(tempFilePath);
return;
}
}
// If we reached here, something went wrong
reject(RCTErrorUnspecified, error.localizedDescription, error);
});
}];
}
/** /**
* JS sets what *it* considers to be the responder. Later, scroll views can use * JS sets what *it* considers to be the responder. Later, scroll views can use
* this in order to determine if scrolling is appropriate. * this in order to determine if scrolling is appropriate.

View File

@ -250,6 +250,7 @@ var apis = [
'../Libraries/StyleSheet/StyleSheet.js', '../Libraries/StyleSheet/StyleSheet.js',
'../Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js', '../Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js',
'../Libraries/Components/ToastAndroid/ToastAndroid.android.js', '../Libraries/Components/ToastAndroid/ToastAndroid.android.js',
'../Libraries/Utilities/UIManager.js',
'../Libraries/Vibration/VibrationIOS.ios.js', '../Libraries/Vibration/VibrationIOS.ios.js',
]; ];