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:
parent
0513d3abb5
commit
ac12f98689
|
@ -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 />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -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 />; }
|
||||||
|
},
|
||||||
|
];
|
|
@ -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'),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue