diff --git a/js/ImageEditingExample.js b/js/ImageEditingExample.js index 830e1276ef6..0bc25ce26d7 100644 --- a/js/ImageEditingExample.js +++ b/js/ImageEditingExample.js @@ -5,5 +5,306 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow strict + * @flow */ +'use strict'; + +var React = require('react'); +var ReactNative = require('react-native'); +var { + CameraRoll, + Image, + ImageEditor, + Platform, + ScrollView, + StyleSheet, + Text, + TouchableHighlight, + View, +} = ReactNative; + +var PAGE_SIZE = 20; + +type ImageOffset = { + x: number, + y: number, +}; + +type ImageSize = { + width: number, + height: number, +}; + +type ImageCropData = { + offset: ImageOffset, + size: ImageSize, + displaySize?: ?ImageSize, + resizeMode?: ?any, +}; + +class SquareImageCropper extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + state: any; + _isMounted: boolean; + _transformData: ImageCropData; + + constructor(props) { + super(props); + this._isMounted = true; + this.state = { + randomPhoto: null, + measuredSize: null, + croppedImageURI: null, + cropError: null, + }; + this._fetchRandomPhoto(); + } + + async _fetchRandomPhoto() { + try { + const data = await CameraRoll.getPhotos({first: PAGE_SIZE}); + if (!this._isMounted) { + return; + } + var edges = data.edges; + var edge = edges[Math.floor(Math.random() * edges.length)]; + var randomPhoto = edge && edge.node && edge.node.image; + if (randomPhoto) { + this.setState({randomPhoto}); + } + } catch (error) { + console.warn("Can't get a photo from camera roll", error); + } + } + + componentWillUnmount() { + this._isMounted = false; + } + + render() { + if (!this.state.measuredSize) { + return ( + { + var measuredWidth = event.nativeEvent.layout.width; + if (!measuredWidth) { + return; + } + this.setState({ + measuredSize: {width: measuredWidth, height: measuredWidth}, + }); + }} + /> + ); + } + + if (!this.state.croppedImageURI) { + return this._renderImageCropper(); + } + return this._renderCroppedImage(); + } + + _renderImageCropper() { + if (!this.state.randomPhoto) { + return ; + } + var error = null; + if (this.state.cropError) { + error = {this.state.cropError.message}; + } + return ( + + Drag the image within the square to crop: + (this._transformData = data)} + /> + + + Crop + + + {error} + + ); + } + + _renderCroppedImage() { + return ( + + Here is the cropped image: + + + + Try again + + + + ); + } + + _crop() { + ImageEditor.cropImage( + this.state.randomPhoto.uri, + this._transformData, + croppedImageURI => this.setState({croppedImageURI}), + cropError => this.setState({cropError}), + ); + } + + _reset() { + this.setState({ + randomPhoto: null, + croppedImageURI: null, + cropError: null, + }); + this._fetchRandomPhoto(); + } +} + +class ImageCropper extends React.Component<$FlowFixMeProps, $FlowFixMeState> { + _contentOffset: ImageOffset; + _maximumZoomScale: number; + _minimumZoomScale: number; + _scaledImageSize: ImageSize; + _horizontal: boolean; + + componentWillMount() { + // Scale an image to the minimum size that is large enough to completely + // fill the crop box. + var widthRatio = this.props.image.width / this.props.size.width; + var heightRatio = this.props.image.height / this.props.size.height; + this._horizontal = widthRatio > heightRatio; + if (this._horizontal) { + this._scaledImageSize = { + width: this.props.image.width / heightRatio, + height: this.props.size.height, + }; + } else { + this._scaledImageSize = { + width: this.props.size.width, + height: this.props.image.height / widthRatio, + }; + if (Platform.OS === 'android') { + // hack to work around Android ScrollView a) not supporting zoom, and + // b) not supporting vertical scrolling when nested inside another + // vertical ScrollView (which it is, when displayed inside UIExplorer) + this._scaledImageSize.width *= 2; + this._scaledImageSize.height *= 2; + this._horizontal = true; + } + } + this._contentOffset = { + x: (this._scaledImageSize.width - this.props.size.width) / 2, + y: (this._scaledImageSize.height - this.props.size.height) / 2, + }; + this._maximumZoomScale = Math.min( + this.props.image.width / this._scaledImageSize.width, + this.props.image.height / this._scaledImageSize.height, + ); + this._minimumZoomScale = Math.max( + this.props.size.width / this._scaledImageSize.width, + this.props.size.height / this._scaledImageSize.height, + ); + this._updateTransformData( + this._contentOffset, + this._scaledImageSize, + this.props.size, + ); + } + + _onScroll(event) { + this._updateTransformData( + event.nativeEvent.contentOffset, + event.nativeEvent.contentSize, + event.nativeEvent.layoutMeasurement, + ); + } + + _updateTransformData(offset, scaledImageSize, croppedImageSize) { + var offsetRatioX = offset.x / scaledImageSize.width; + var offsetRatioY = offset.y / scaledImageSize.height; + var sizeRatioX = croppedImageSize.width / scaledImageSize.width; + var sizeRatioY = croppedImageSize.height / scaledImageSize.height; + + var cropData: ImageCropData = { + offset: { + x: this.props.image.width * offsetRatioX, + y: this.props.image.height * offsetRatioY, + }, + size: { + width: this.props.image.width * sizeRatioX, + height: this.props.image.height * sizeRatioY, + }, + }; + this.props.onTransformDataChange && + this.props.onTransformDataChange(cropData); + } + + render() { + return ( + + + + ); + } +} + +exports.framework = 'React'; +exports.title = 'ImageEditor'; +exports.description = 'Cropping and scaling with ImageEditor'; +exports.examples = [ + { + title: 'Image Cropping', + render() { + return ; + }, + }, +]; + +var styles = StyleSheet.create({ + container: { + flex: 1, + alignSelf: 'stretch', + }, + imageCropper: { + alignSelf: 'center', + marginTop: 12, + }, + cropButtonTouchable: { + alignSelf: 'center', + marginTop: 12, + }, + cropButton: { + padding: 12, + backgroundColor: 'blue', + borderRadius: 4, + }, + cropButtonLabel: { + color: 'white', + fontSize: 16, + fontWeight: '500', + }, +});