Skip to content

Commit

Permalink
Added support for taking snapshots of the screen, window or individua…
Browse files Browse the repository at this point in the history
…l 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
  • Loading branch information
nicklockwood authored and facebook-github-bot-7 committed Feb 23, 2016
1 parent 0513d3a commit ac12f98
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 5 deletions.
57 changes: 53 additions & 4 deletions Examples/UIExplorer/ActionSheetIOSExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var {
ActionSheetIOS,
StyleSheet,
Text,
UIManager,
View,
} = React;

Expand Down Expand Up @@ -127,9 +128,7 @@ var ShareActionSheetExample = React.createClass({
'com.apple.UIKit.activity.PostToTwitter'
]
},
(error) => {
console.error(error);
},
(error) => alert(error),
(success, method) => {
var text;
if (success) {
Expand All @@ -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({
button: {
marginBottom: 10,
Expand All @@ -166,10 +209,16 @@ exports.examples = [
return <ShareActionSheetExample url="https://code.facebook.com" />;
}
},
{
{
title: 'Share Local Image',
render(): ReactElement {
return <ShareActionSheetExample url="bunny.png" />;
}
},
{
title: 'Share Screenshot',
render(): ReactElement {
return <ShareScreenshotExample />;
}
}
];
73 changes: 73 additions & 0 deletions Examples/UIExplorer/SnapshotExample.js
Original file line number Diff line number Diff line change
@@ -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 />; }
},
];
4 changes: 4 additions & 0 deletions Examples/UIExplorer/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ var APIExamples: Array<UIExplorerExample> = [
key: 'RCTRootViewIOSExample',
module: require('./RCTRootViewIOSExample'),
},
{
key: 'SnapshotExample',
module: require('./SnapshotExample'),
},
{
key: 'StatusBarIOSExample',
module: require('./StatusBarIOSExample'),
Expand Down
41 changes: 40 additions & 1 deletion Libraries/Utilities/UIManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
'use strict';

var UIManager = require('NativeModules').UIManager;
var findNodeHandle = require('findNodeHandle');

if (!UIManager.setChildren) {

Expand Down Expand Up @@ -39,7 +40,45 @@ if (!UIManager.setChildren) {
UIManager.setChildren = function(containerTag, createdTags) {
var indexes = this._cachedIndexArray(createdTags.length);
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;
3 changes: 3 additions & 0 deletions React/Base/RCTUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ RCT_EXTERN NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL);
// Determines if a given image URL actually refers to an XCAsset
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
RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);

Expand Down
45 changes: 45 additions & 0 deletions React/Base/RCTUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,51 @@ BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL)
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])
{
CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color));
Expand Down
69 changes: 69 additions & 0 deletions React/Modules/RCTUIManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,75 @@ static void RCTMeasureLayout(RCTShadowView *view,
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);

This comment has been minimized.

Copy link
@tadeuzagallo

tadeuzagallo Mar 5, 2016

Contributor

This should be

reject(@"...");

This comment has been minimized.

Copy link
@nicklockwood

nicklockwood Mar 5, 2016

Author Contributor

Possibly. I was thinking this was a programming error rather than runtime, but perhaps it should be both.

This comment has been minimized.

Copy link
@gre

gre Mar 9, 2016

Contributor

yeah I think you expect a reject, otherwise the promise never ends.

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) {

This comment has been minimized.

Copy link
@tadeuzagallo

tadeuzagallo Mar 5, 2016

Contributor

Why 0.1?

This comment has been minimized.

Copy link
@nicklockwood

nicklockwood Mar 5, 2016

Author Contributor

Floating point precision (but yeah, probably not neccesary in this case)

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);

This comment has been minimized.

Copy link
@tadeuzagallo

tadeuzagallo Mar 5, 2016

Contributor

It should be reject here again.

This comment has been minimized.

Copy link
@nicklockwood

nicklockwood Mar 5, 2016

Author Contributor

This is a programmer error - it needs to be handled at development time, not at runtime.

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
* this in order to determine if scrolling is appropriate.
Expand Down
1 change: 1 addition & 0 deletions website/server/extractDocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ var apis = [
'../Libraries/StyleSheet/StyleSheet.js',
'../Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js',
'../Libraries/Components/ToastAndroid/ToastAndroid.android.js',
'../Libraries/Utilities/UIManager.js',
'../Libraries/Vibration/VibrationIOS.ios.js',
];

Expand Down

4 comments on commit ac12f98

@bestander
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicklockwood you broke website generation script.

@bestander
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shame

@nicklockwood
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bestander 😔

@aqnaruto
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is great !

Please sign in to comment.