mirror of
https://github.com/status-im/react-native.git
synced 2025-01-17 04:50:59 +00:00
a322ab61fb
Summary: public A fix added to make the ImageEditor example usable on Android inadvertantly broke the cropping logic on iOS, so that zoomed images were cropped incorrectly. This diff fixes that, and also sets the min/max zoom scale correctly based on the image scale, instead of using a hard-coded value. Reviewed By: bestander Differential Revision: D2874576 fb-gh-sync-id: 02c62940c24f4d4266655de6ddbf4fe3bc9c13ce
325 lines
8.4 KiB
JavaScript
325 lines
8.4 KiB
JavaScript
|
|
/**
|
|
* 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 {
|
|
CameraRoll,
|
|
Image,
|
|
ImageEditor,
|
|
NativeModules,
|
|
Platform,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableHighlight,
|
|
UIManager,
|
|
View,
|
|
} = React;
|
|
|
|
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 {
|
|
_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 (
|
|
<View
|
|
style={styles.container}
|
|
onLayout={(event) => {
|
|
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 (
|
|
<View style={styles.container} />
|
|
);
|
|
}
|
|
var error = null;
|
|
if (this.state.cropError) {
|
|
error = (
|
|
<Text>{this.state.cropError.message}</Text>
|
|
);
|
|
}
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text>Drag the image within the square to crop:</Text>
|
|
<ImageCropper
|
|
image={this.state.randomPhoto}
|
|
size={this.state.measuredSize}
|
|
style={[styles.imageCropper, this.state.measuredSize]}
|
|
onTransformDataChange={(data) => this._transformData = data}
|
|
/>
|
|
<TouchableHighlight
|
|
style={styles.cropButtonTouchable}
|
|
onPress={this._crop.bind(this)}>
|
|
<View style={styles.cropButton}>
|
|
<Text style={styles.cropButtonLabel}>
|
|
Crop
|
|
</Text>
|
|
</View>
|
|
</TouchableHighlight>
|
|
{error}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
_renderCroppedImage() {
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text>Here is the cropped image:</Text>
|
|
<Image
|
|
source={{uri: this.state.croppedImageURI}}
|
|
style={[styles.imageCropper, this.state.measuredSize]}
|
|
/>
|
|
<TouchableHighlight
|
|
style={styles.cropButtonTouchable}
|
|
onPress={this._reset.bind(this)}>
|
|
<View style={styles.cropButton}>
|
|
<Text style={styles.cropButtonLabel}>
|
|
Try again
|
|
</Text>
|
|
</View>
|
|
</TouchableHighlight>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
_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 {
|
|
_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 (
|
|
<ScrollView
|
|
alwaysBounceVertical={true}
|
|
automaticallyAdjustContentInsets={false}
|
|
contentOffset={this._contentOffset}
|
|
decelerationRate="fast"
|
|
horizontal={this._horizontal}
|
|
maximumZoomScale={this._maximumZoomScale}
|
|
minimumZoomScale={this._minimumZoomScale}
|
|
onMomentumScrollEnd={this._onScroll.bind(this)}
|
|
onScrollEndDrag={this._onScroll.bind(this)}
|
|
showsHorizontalScrollIndicator={false}
|
|
showsVerticalScrollIndicator={false}
|
|
style={this.props.style}
|
|
scrollEventThrottle={16}>
|
|
<Image source={this.props.image} style={this._scaledImageSize} />
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
exports.framework = 'React';
|
|
exports.title = 'ImageEditor';
|
|
exports.description = 'Cropping and scaling with ImageEditor';
|
|
exports.examples = [{
|
|
title: 'Image Cropping',
|
|
render() {
|
|
return <SquareImageCropper/>;
|
|
}
|
|
}];
|
|
|
|
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',
|
|
},
|
|
});
|