rn-zoomable
Version:
A zoomable image component like Instagram
192 lines (172 loc) • 4.63 kB
JavaScript
// @flow
import React, { Component } from 'react';
import autobind from 'class-autobind';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import { View, Text, Image, ActivityIndicator, TouchableOpacity, Animated } from 'react-native';
type Cancellable = {
cancel: () => void,
};
type Props = {
source: number | { uri: string, width?: number, height?: number },
style?: StyleType,
loadingComponent?: ReactNode,
onPress?: () => void,
thumbnail?: number | { uri: string, width?: number, height?: number },
loadingMethod?: 'spinner' | 'progressive',
};
type State = {
isLoading: boolean,
ratio: ?number,
error: ?string,
thumbnailOpacity: Animated.Value,
};
export default class FlexImage extends Component<Props, State> {
_pendingGetSize: ?Cancellable;
constructor(props: Props, ...args: Array<mixed>) {
super(props, ...args);
autobind(this);
let { source, thumbnail, loadingMethod } = props;
let ratio;
let error;
let isLoading = true;
let src = thumbnail && loadingMethod === 'progressive' ? thumbnail : source;
if (typeof src === 'number') {
let imageSource = resolveAssetSource(src);
if (imageSource) {
let { width, height } = imageSource;
if (width && height) {
ratio = width / height;
}
} else {
error = 'Error: Failed to retrieve width and height of the image';
}
isLoading = false;
} else {
this._pendingGetSize = getImageSize(src, this._onLoadSuccess, this._onLoadFail);
}
this.state = {
ratio,
isLoading,
error,
thumbnailOpacity: new Animated.Value(0),
};
}
componentWillUnmount() {
if (this._pendingGetSize) {
this._pendingGetSize.cancel();
}
}
render() {
let {
source,
style,
onPress,
loadingComponent,
thumbnail,
loadingMethod,
...otherProps
} = this.props;
let { isLoading, ratio, error, thumbnailOpacity } = this.state;
if (isLoading && loadingMethod !== 'progressive') {
let loadingIndicator = loadingComponent || <ActivityIndicator size="large" />;
return (
<View style={[{ justifyContent: 'center', alignItems: 'center' }, style]}>
{loadingIndicator}
</View>
);
}
if (error) {
return (
<View style={[{ justifyContent: 'center', alignItems: 'center' }, style]}>
<Text>{error}</Text>
</View>
);
}
let imageSource;
if (typeof source === 'number') {
imageSource = source;
} else {
// eslint-disable-next-line no-unused-vars
let { uri, width, height, ...other } = source;
imageSource = { uri, ...other };
}
return (
<TouchableOpacity
onPress={onPress}
disabled={!onPress}
style={[{ aspectRatio: ratio }, style]}>
{thumbnail && loadingMethod === 'progressive' && (
<Animated.Image
{...otherProps}
source={thumbnail}
style={{
width: '100%',
height: '100%',
opacity: thumbnailOpacity,
zIndex: 1,
}}
onLoad={this._onThumbnailLoad}
testID="progressiveThumbnail"
/>
)}
<Animated.Image
{...otherProps}
source={imageSource}
style={{ width: '100%', height: '100%', position: 'absolute' }}
onLoad={this._onLoad}
/>
</TouchableOpacity>
);
}
_onThumbnailLoad = () => {
Animated.timing(this.state.thumbnailOpacity, {
toValue: 1,
duration: 250,
}).start();
};
_onLoad = () => {
Animated.timing(this.state.thumbnailOpacity, {
toValue: 0,
duration: 250,
}).start();
};
_onLoadSuccess(width: number, height: number) {
let ratio = width / height;
this.setState({
isLoading: false,
ratio,
});
}
_onLoadFail(error: Error) {
this.setState({
isLoading: false,
error: 'Error: ' + error.message,
});
}
}
// A cancellable version of Image.getSize
export function getImageSize(
source: { uri: string },
onSuccess: (width: number, height: number) => void,
onFail: (error: Error) => void
) {
let isCancelled = false;
Image.getSize(
source.uri,
(width: number, height: number) => {
if (!isCancelled) {
onSuccess(width, height);
}
},
(error: Error) => {
if (!isCancelled) {
onFail(error);
}
}
);
return {
cancel: () => {
isCancelled = true;
},
};
}