- support capture/retake mode on the full screen camera

- make the shouldSaveToCameraRoll camera capture flag usable (on iOS for now) so it calls the completion if it’s not enabled
- expose a new method saveImageURLToCameraRoll that is used in the retake mode so the image can be saved to the gallery
This commit is contained in:
Artal Druk 2017-03-21 15:24:19 +02:00
parent 7ea04a2277
commit 9ca33bfdc8
4 changed files with 172 additions and 142 deletions

View File

@ -12,7 +12,7 @@
#import "UIView+React.h"
#import "RCTConvert.h"
#import "CKCameraOverlayView.h"
#import "CKGalleryManager.h"
static void * CapturingStillImageContext = &CapturingStillImageContext;
static void * SessionRunningContext = &SessionRunningContext;
@ -505,31 +505,23 @@ RCT_ENUM_CONVERTER(CKCameraZoomMode, (@{
imageInfoDict[@"size"] = [NSNumber numberWithInteger:imageData.length];
if (shouldSaveToCameraRoll) {
[self saveImageToCameraRoll:imageData temporaryFileURL:temporaryFileURL block:^(BOOL success) {
[CKGalleryManager saveImageToCameraRoll:imageData temporaryFileURL:temporaryFileURL fetchOptions:self.fetchOptions block:^(BOOL success, NSString *localIdentifier) {
if (success) {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:self.fetchOptions];
PHAsset *lastImageAsset = [fetchResult firstObject];
if (lastImageAsset.localIdentifier) {
imageInfoDict[@"id"] = lastImageAsset.localIdentifier;
if (localIdentifier) {
imageInfoDict[@"id"] = localIdentifier;
}
if (block) {
block(imageInfoDict);
}
}
else {
//NSLog( @"Could not save to camera roll");
}
}];
} else if (block) {
block(imageInfoDict);
}
}
}];
}
@ -614,54 +606,6 @@ RCT_ENUM_CONVERTER(CKCameraZoomMode, (@{
} );
}
-(void)saveImageToCameraRoll:(NSData*)imageData
temporaryFileURL:(NSURL*)temporaryFileURL
block:(CallbackBlock)block{
// To preserve the metadata, we create an asset from the JPEG NSData representation.
// Note that creating an asset from a UIImage discards the metadata.
// In iOS 9, we can use -[PHAssetCreationRequest addResourceWithType:data:options].
// In iOS 8, we save the image to a temporary file and use +[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:].
if ( [PHAssetCreationRequest class] ) {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[[PHAssetCreationRequest creationRequestForAsset] addResourceWithType:PHAssetResourceTypePhoto data:imageData options:nil];
} completionHandler:^( BOOL success, NSError *error ) {
if ( ! success ) {
//NSLog( @"Error occurred while saving image to photo library: %@", error );
block(NO);
}
else {
block(YES);
}
}];
}
else {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
NSError *error = nil;
if ( error ) {
//NSLog( @"Error occured while writing image data to a temporary file: %@", error );
}
else {
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:temporaryFileURL];
}
} completionHandler:^( BOOL success, NSError *error ) {
if ( ! success ) {
//NSLog( @"Error occurred while saving image to photo library: %@", error );
block(NO);
}
else {
block(YES);
}
}];
}
}
-(NSURL*)saveToTmpFolder:(NSData*)data {
NSString *temporaryFileName = [NSProcessInfo processInfo].globallyUniqueString;
NSString *temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[temporaryFileName stringByAppendingPathExtension:@"jpg"]];

View File

@ -7,15 +7,17 @@
//
#import <Foundation/Foundation.h>
#import <Photos/Photos.h>
#import "RCTBridgeModule.h"
#import "CKCamera.h"
typedef void (^CallbackGalleryBlock)(BOOL success, NSString *encodeImage);
typedef void (^CallbackGalleryAuthorizationStatus)(BOOL isAuthorized);
typedef void (^SaveBlock)(BOOL success, NSString *localIdentifier);
@interface CKGalleryManager : NSObject <RCTBridgeModule>
+(void)deviceGalleryAuthorizationStatus:(CallbackGalleryAuthorizationStatus)callback;
+(void)saveImageToCameraRoll:(NSData*)imageData temporaryFileURL:(NSURL*)temporaryFileURL fetchOptions:(PHFetchOptions*)fetchOptions block:(SaveBlock)block;
@end

View File

@ -7,7 +7,6 @@
//
#import "CKGalleryManager.h"
#import <Photos/Photos.h>
#import "RCTConvert.h"
#import "CKGalleryViewManager.h"
@ -289,6 +288,66 @@ RCT_EXPORT_METHOD(requestDevicePhotosAuthorization:(RCTPromiseResolveBlock)resol
return @NO;
}
+(NSString*)getImageLocalIdentifierForFetchOptions:(PHFetchOptions*)fetchOption {
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOption];
PHAsset *lastImageAsset = [fetchResult firstObject];
return lastImageAsset.localIdentifier;
}
+(void)saveImageToCameraRoll:(NSData*)imageData temporaryFileURL:(NSURL*)temporaryFileURL fetchOptions:(PHFetchOptions*)fetchOptions block:(SaveBlock)block {
// To preserve the metadata, we create an asset from the JPEG NSData representation.
// Note that creating an asset from a UIImage discards the metadata.
// In iOS 9, we can use -[PHAssetCreationRequest addResourceWithType:data:options].
// In iOS 8, we save the image to a temporary file and use +[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:].
if ( [PHAssetCreationRequest class] && imageData) {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[[PHAssetCreationRequest creationRequestForAsset] addResourceWithType:PHAssetResourceTypePhoto data:imageData options:nil];
} completionHandler:^( BOOL success, NSError *error ) {
NSString *localIdentifier = [CKGalleryManager getImageLocalIdentifierForFetchOptions:fetchOptions];
if ( ! success ) {
block(NO, localIdentifier);
}
else {
block(YES, localIdentifier);
}
}];
}
else {
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:temporaryFileURL];
} completionHandler:^( BOOL success, NSError *error ) {
NSString *localIdentifier = [CKGalleryManager getImageLocalIdentifierForFetchOptions:fetchOptions];
if ( ! success ) {
block(NO, localIdentifier);
}
else {
block(YES, localIdentifier);
}
}];
}
}
+(void)saveImageURLToCameraRoll:(NSString*)temporaryFileURL fetchOptions:(PHFetchOptions*)fetchOptions block:(SaveBlock)block {
NSURL *imageURL = [NSURL URLWithString:temporaryFileURL];
if(!imageURL) {
block(NO, nil);
return;
}
[CKGalleryManager saveImageToCameraRoll:nil temporaryFileURL:imageURL fetchOptions:fetchOptions block:block];
}
RCT_EXPORT_METHOD(saveImageURLToCameraRoll:(NSString*)imageURL
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
[CKGalleryManager saveImageURLToCameraRoll:imageURL fetchOptions:self.fetchOptions block:^(BOOL success, NSString *localIdentifier) {
if (resolve) {
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:@{@"success": @(success)}];
if(localIdentifier) {
result[@"id"] = localIdentifier;
}
resolve(result);
}
}];
}
@end

View File

@ -1,24 +1,33 @@
import React, { Component } from 'react';
import React, { Component, PropTypes } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Image
Image,
NativeModules,
Platform
} from 'react-native';
import _ from 'lodash';
import CameraKitCamera from './../CameraKitCamera';
const IsIOS = Platform.OS === 'ios';
const CKGallery = IsIOS ? NativeModules.CKGalleryManager : null;
const FLASH_MODE_AUTO = 'auto';
const FLASH_MODE_ON = 'on';
const FLASH_MODE_OFF = 'off';
export default class CameraScreenBase extends Component {
static propTypes = {
allowCaptureRetake: PropTypes.bool,
};
static defaultProps = {
allowCaptureRetake: false,
};
constructor(props) {
super(props);
this.currentFlashArrayPosition = 0;
@ -46,7 +55,6 @@ export default class CameraScreenBase extends Component {
};
this.onSetFlash = this.onSetFlash.bind(this);
this.onSwitchCameraPressed = this.onSwitchCameraPressed.bind(this);
}
componentDidMount() {
@ -62,6 +70,10 @@ export default class CameraScreenBase extends Component {
});
}
isCaptureRetakeMode() {
return !!(this.props.allowCaptureRetake && !_.isUndefined(this.state.imageCaptured));
}
getCameraOptions() {
const cameraOptions = {
flashMode: 'auto',
@ -81,7 +93,7 @@ export default class CameraScreenBase extends Component {
}
renderFlashButton() {
return (
return !this.isCaptureRetakeMode() &&
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={() => this.onSetFlash(FLASH_MODE_AUTO)}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
@ -89,25 +101,17 @@ export default class CameraScreenBase extends Component {
resizeMode={Image.resizeMode.contain}
/>
</TouchableOpacity>
);
}
renderSwitchCameraButton() {
if (this.props.cameraFlipImage) {
return (
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={this.onSwitchCameraPressed}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.props.cameraFlipImage}
resizeMode={Image.resizeMode.contain}
/>
</TouchableOpacity>
);
}
return null;
return (this.props.cameraFlipImage && !this.isCaptureRetakeMode()) &&
<TouchableOpacity style={{ paddingHorizontal: 15 }} onPress={this.onSwitchCameraPressed}>
<Image
style={{ flex: 1, justifyContent: 'center' }}
source={this.props.cameraFlipImage}
resizeMode={Image.resizeMode.contain}
/>
</TouchableOpacity>
}
renderTopButtons() {
@ -122,11 +126,18 @@ export default class CameraScreenBase extends Component {
renderCamera() {
return (
<View style={styles.cameraContainer}>
<CameraKitCamera
ref={(cam) => this.camera = cam}
style={{ flex: 1, justifyContent: 'flex-end' }}
cameraOptions={this.state.cameraOptions}
/>
{
this.isCaptureRetakeMode() ?
<Image
style={{ flex: 1, justifyContent: 'flex-end'}}
source={{uri: this.state.imageCaptured.uri}}
/> :
<CameraKitCamera
ref={(cam) => this.camera = cam}
style={{ flex: 1, justifyContent: 'flex-end' }}
cameraOptions={this.state.cameraOptions}
/>
}
</View>
);
}
@ -142,39 +153,23 @@ export default class CameraScreenBase extends Component {
}
}
renderCaptureButton() {
if (this.props.captureButtonImage) {
return (
<View style={styles.captureButtonContainer}>
<TouchableOpacity
onPress={() => this.onCaptureImagePressed()}
return (this.props.captureButtonImage && !this.isCaptureRetakeMode()) &&
<View style={styles.captureButtonContainer}>
<TouchableOpacity
onPress={() => this.onCaptureImagePressed()}
>
<Image
style={styles.captureButton}
source={this.props.captureButtonImage}
resizeMode={'contain'}
>
<Image
style={styles.captureButton}
source={this.props.captureButtonImage}
resizeMode={'contain'}>
<Text style={styles.captureNumber}>
{this.numberOfImagesTaken()}
</Text>
</Image>
</TouchableOpacity>
</View >
);
}
return null;
}
renderBootomContainerGap() {
return (
<View
style={styles.bottomContainerGap}
>
</View>
);
<Text style={styles.captureNumber}>
{this.numberOfImagesTaken()}
</Text>
</Image>
</TouchableOpacity>
</View >
}
renderRatioStrip() {
@ -196,26 +191,56 @@ export default class CameraScreenBase extends Component {
);
}
onButtonPressed(type) {
this.props.onBottomButtonPressed({ type, captureImages: this.state.captureImages })
sendBottomButtonPressedAction(type, captureRetakeMode, image) {
if(this.props.onBottomButtonPressed) {
this.props.onBottomButtonPressed({ type, captureImages: this.state.captureImages, captureRetakeMode, image })
}
}
async onButtonPressed(type) {
const captureRetakeMode = this.isCaptureRetakeMode();
if (captureRetakeMode) {
if(type === 'left') {
this.setState({imageCaptured: undefined});
}
if(type === 'right') {
if(CKGallery !== null) {
const result = await CKGallery.saveImageURLToCameraRoll(this.state.imageCaptured.uri);
const savedImage = {...this.state.imageCaptured, id: result.id};
this.setState({imageCaptured: undefined, captureImages: _.concat(this.state.captureImages, savedImage)}, () => {
this.sendBottomButtonPressedAction(type, captureRetakeMode);
});
} else {
console.warn('Not implemented on Android yet');
this.sendBottomButtonPressedAction(type, captureRetakeMode);
}
}
} else {
this.sendBottomButtonPressedAction(type, captureRetakeMode);
}
}
renderBottomButton(type) {
const buttonText = _(this.props).get(`actions.${type}ButtonText`)
if (buttonText) {
let showButton = true;
if (type === 'right') {
showButton = this.state.captureImages.length || this.isCaptureRetakeMode();
}
if (showButton) {
const buttonNameSuffix = this.isCaptureRetakeMode() ? 'CaptureRetakeButtonText' : 'ButtonText';
const buttonText = _(this.props).get(`actions.${type}${buttonNameSuffix}`)
return (
<TouchableOpacity
style={[styles.bottomButton, { justifyContent: type === 'left' ? 'flex-start' : 'flex-end' }]}
onPress={() => this.onButtonPressed(type)}
>
<Text style={styles.textStyle}>{_.get(this.props, `actions.${type}ButtonText`, type)}</Text>
<Text style={styles.textStyle}>{buttonText}</Text>
</TouchableOpacity>
);
} else {
return (
<View style={styles.bottomContainerGap} />
);
}
return (
this.renderBootomContainerGap()
);
}
renderBottomButtons() {
@ -228,7 +253,6 @@ export default class CameraScreenBase extends Component {
);
}
onSwitchCameraPressed() {
this.camera.changeCamera();
}
@ -241,16 +265,17 @@ export default class CameraScreenBase extends Component {
}
async onCaptureImagePressed() {
const image = await this.camera.capture(true);
const shouldSaveToCameraRoll = !this.props.allowCaptureRetake;
const image = await this.camera.capture(shouldSaveToCameraRoll);
if (image) {
this.setState({ captured: true, imageCaptured: image, captureImages: _.concat(this.state.captureImages, image) });
if (this.props.allowCaptureRetake) {
this.setState({ imageCaptured: image });
} else {
if (image) {
this.setState({ captured: true, imageCaptured: image, captureImages: _.concat(this.state.captureImages, image) });
}
this.sendBottomButtonPressedAction('capture', false, image);
}
if (this.props.onBottomButtonPressed) {
this.props.onBottomButtonPressed({ type: 'capture', image })
}
}
onRatioButtonPressed() {