Make the image preview smoother (#2147)

* Make the image preview smoother
This commit is contained in:
Elias Nahum 2018-09-24 04:23:40 -03:00 committed by Sudheer
parent 8884293b18
commit 0edcc139f4
4 changed files with 360 additions and 83 deletions

View file

@ -0,0 +1,257 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImagePreview should match snapshot 1`] = `
<AnimatedComponent
style={
Array [
Object {
"backgroundColor": "#000",
"flex": 1,
},
Object {
"opacity": NaN,
},
]
}
>
<AnimatedComponent
style={
Object {
"backgroundColor": "#000",
"flex": 1,
}
}
>
<Gallery
errorComponent={[Function]}
flatListProps={
Object {
"windowSize": 3,
}
}
imageComponent={[Function]}
images={
Array [
Object {
"caption": "Caption 1",
"data": "data",
"source": "source",
},
Object {
"caption": "Caption 2",
"data": "data",
"source": "source",
},
]
}
initialPage={0}
onPageSelected={[Function]}
onSingleTapConfirmed={[Function]}
onSwipedVertical={[Function]}
pageMargin={2}
removeClippedSubviews={true}
scrollViewStyle={Object {}}
style={
Object {
"flex": 1,
}
}
/>
<AnimatedComponent
style={
Array [
Object {
"height": 48,
"overflow": "hidden",
"position": "absolute",
"width": "100%",
},
Object {
"opacity": 1,
"top": 0,
},
]
}
>
<Component
style={
Object {
"backgroundColor": "rgba(0, 0, 0, 0.8)",
"height": 48,
"left": 0,
"position": "absolute",
"top": 0,
"width": "100%",
}
}
>
<Component
style={
Object {
"alignItems": "center",
"flexDirection": "row",
"justifyContent": "space-around",
}
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 44,
"justifyContent": "center",
"width": 48,
}
}
>
<Icon
allowFontScaling={false}
color="#fff"
name="md-close"
size={26}
/>
</TouchableOpacity>
<Component
style={
Object {
"color": "white",
"flex": 1,
"fontSize": 15,
"marginHorizontal": 10,
"textAlign": "center",
}
}
>
1/2
</Component>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 44,
"justifyContent": "center",
"width": 48,
}
}
>
<Icon
allowFontScaling={false}
color="#fff"
name="ios-download"
size={26}
/>
</TouchableOpacity>
</Component>
</Component>
</AnimatedComponent>
<AnimatedComponent
style={
Array [
Object {
"bottom": 0,
"opacity": 1,
},
Object {
"height": 64,
"justifyContent": "center",
"overflow": "hidden",
"position": "absolute",
"width": "100%",
},
]
}
>
<LinearGradient
colors={
Array [
"transparent",
"#000000",
]
}
end={
Object {
"x": 0,
"y": 0.9,
}
}
pointerEvents="none"
start={
Object {
"x": 0,
"y": 0,
}
}
style={
Object {
"bottom": 0,
"height": 64,
"justifyContent": "flex-end",
"left": 0,
"paddingBottom": 5,
"paddingHorizontal": 24,
"position": "absolute",
"right": 0,
}
}
>
<Component
style={
Object {
"color": "white",
"fontSize": 14,
"marginBottom": 10,
}
}
>
Caption 1
</Component>
</LinearGradient>
</AnimatedComponent>
</AnimatedComponent>
<Downloader
deviceHeight={400}
deviceWidth={300}
file={
Object {
"caption": "Caption 1",
"data": "data",
"source": "source",
}
}
force={false}
onCancelPress={[Function]}
onDownloadCancel={[Function]}
onDownloadStart={[Function]}
onDownloadSuccess={[Function]}
prompt={false}
saveToCameraRoll={true}
show={false}
/>
</AnimatedComponent>
`;
exports[`ImagePreview should match snapshot, renderDownloadButton 1`] = `
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 44,
"justifyContent": "center",
"width": 48,
}
}
>
<Icon
allowFontScaling={false}
color="#fff"
name="ios-download"
size={26}
/>
</TouchableOpacity>
`;

View file

@ -9,7 +9,6 @@ import {
Image,
Platform,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
@ -27,7 +26,6 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import FileAttachmentDocument from 'app/components/file_attachment_list/file_attachment_document';
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import ProgressiveImage from 'app/components/progressive_image';
import {DeviceTypes, NavigationTypes, PermissionTypes} from 'app/constants';
import {getLocalFilePathFromFile, isDocument, isVideo} from 'app/utils/file';
import {emptyFunction} from 'app/utils/general';
@ -104,7 +102,7 @@ export default class ImagePreview extends PureComponent {
const {getItemMeasures, navigator} = this.props;
const {index} = this.state;
this.setState({animating: true, gallery: false, hide: false});
this.setState({animating: true});
navigator.setStyle({
screenBackgroundColor: 'transparent',
});
@ -124,10 +122,6 @@ export default class ImagePreview extends PureComponent {
this.setState({index});
};
handleGalleryLayout = () => {
this.setState({hide: true});
};
handleSwipedVertical = (evt, gestureState) => {
if (Math.abs(gestureState.dy) > 150) {
this.close();
@ -178,44 +172,6 @@ export default class ImagePreview extends PureComponent {
};
};
getSwipeableStyle = () => {
const {deviceHeight, deviceWidth, files} = this.props;
const {origin, target, index} = this.state;
const inputRange = [0, 1];
let toHeight = deviceHeight;
let toWidth = deviceWidth;
if (files[index].dimensions) {
const {height, width} = files[index].dimensions;
if (width < deviceWidth) {
toWidth = width;
}
if (height < deviceWidth) {
toHeight = height;
}
}
return {
left: this.openAnim.interpolate({
inputRange,
outputRange: [origin.x, target.x],
}),
top: this.openAnim.interpolate({
inputRange,
outputRange: [origin.y, target.y],
}),
width: this.openAnim.interpolate({
inputRange,
outputRange: [origin.width, toWidth],
}),
height: this.openAnim.interpolate({
inputRange,
outputRange: [origin.height, toHeight],
}),
};
};
renderAttachmentDocument = (file) => {
const {canDownloadFiles, theme, navigator} = this.props;
@ -294,7 +250,7 @@ export default class ImagePreview extends PureComponent {
return null;
};
renderDownloader() {
renderDownloader = () => {
const {deviceHeight, deviceWidth} = this.props;
const file = this.getCurrentFile();
@ -310,9 +266,9 @@ export default class ImagePreview extends PureComponent {
onDownloadSuccess={this.hideDownloader}
/>
);
}
};
renderFooter() {
renderFooter = () => {
const {files} = this.props;
const {index} = this.state;
const footer = this.getHeaderFooterStyle();
@ -331,16 +287,15 @@ export default class ImagePreview extends PureComponent {
</LinearGradient>
</Animated.View>
);
}
};
renderGallery() {
renderGallery = () => {
return (
<Gallery
errorComponent={this.renderOtherItems}
imageComponent={this.renderImageComponent}
images={this.props.files}
initialPage={this.state.index}
onLayout={this.handleGalleryLayout}
onPageSelected={this.handleChangeImage}
onSingleTapConfirmed={this.handleTapped}
onSwipedVertical={this.handleSwipedVertical}
@ -348,9 +303,9 @@ export default class ImagePreview extends PureComponent {
style={style.flex}
/>
);
}
};
renderHeader() {
renderHeader = () => {
const {files} = this.props;
const {index} = this.state;
const header = this.getHeaderFooterStyle();
@ -377,7 +332,7 @@ export default class ImagePreview extends PureComponent {
</View>
</AnimatedView>
);
}
};
renderImageComponent = (imageProps, imageDimensions) => {
if (imageDimensions) {
@ -398,7 +353,7 @@ export default class ImagePreview extends PureComponent {
}
return null;
}
};
renderOtherItems = (index) => {
const {files} = this.props;
@ -417,28 +372,6 @@ export default class ImagePreview extends PureComponent {
return <View/>;
};
renderSelectedItem = () => {
const {hide} = this.state;
const file = this.getCurrentFile();
if (hide || !file || !file.source || !file.source.uri || isDocument(file.data) || isVideo(file.data)) {
return null;
}
const containerStyle = this.getSwipeableStyle();
return (
<ScrollView scrollEnabled={false}>
<Animated.View style={[style.center, style.flex, containerStyle]}>
<ProgressiveImage
imageUri={file.source.uri}
style={[StyleSheet.absoluteFill, style.fullWidth]}
resizeMode='contain'
/>
</Animated.View>
</ScrollView>
);
};
renderVideoPreview = (file) => {
const {deviceHeight, deviceWidth, theme} = this.props;
@ -611,9 +544,7 @@ export default class ImagePreview extends PureComponent {
};
startOpenAnimation = () => {
this.animateOpenAnimToValue(1, () => {
this.setState({gallery: true});
});
this.animateOpenAnimToValue(1);
};
render() {
@ -622,8 +553,7 @@ export default class ImagePreview extends PureComponent {
return (
<AnimatedSafeAreaView style={[style.container, opacity]}>
<AnimatedView style={style.container}>
{this.renderSelectedItem()}
{this.state.gallery && this.renderGallery()}
{this.renderGallery()}
{this.renderHeader()}
{this.renderFooter()}
</AnimatedView>

View file

@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {
TouchableOpacity,
} from 'react-native';
import Preferences from 'mattermost-redux/constants/preferences';
import ImagePreview from './image_preview';
jest.useFakeTimers();
jest.mock('react-intl');
jest.mock('react-native-doc-viewer', () => {
return {
OpenFile: jest.fn(),
};
});
describe('ImagePreview', () => {
const baseProps = {
canDownloadFiles: true,
deviceHeight: 400,
deviceWidth: 300,
files: [
{caption: 'Caption 1', source: 'source', data: 'data'},
{caption: 'Caption 2', source: 'source', data: 'data'},
],
getItemMeasures: jest.fn(),
index: 0,
navigator: {setStyle: jest.fn()},
origin: {},
target: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallow(
<ImagePreview {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper.find(TouchableOpacity).first().exists()).toEqual(true);
});
test('should match snapshot, renderDownloadButton', () => {
const wrapper = shallow(
<ImagePreview {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.instance().renderDownloadButton()).toMatchSnapshot();
});
test('should match state on handleChangeImage', () => {
const wrapper = shallow(
<ImagePreview {...baseProps}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.setState({index: 0});
wrapper.instance().handleChangeImage(1);
expect(wrapper.state('index')).toEqual(1);
});
test('should match call getItemMeasures & navigator.setStyle on close', () => {
const getItemMeasures = jest.fn();
const navigator = {setStyle: jest.fn()};
const wrapper = shallow(
<ImagePreview
{...baseProps}
getItemMeasures={getItemMeasures}
navigator={navigator}
/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.instance().close();
expect(navigator.setStyle).toHaveBeenCalledTimes(2);
expect(navigator.setStyle).toBeCalledWith({screenBackgroundColor: 'transparent'});
});
});

View file

@ -127,6 +127,9 @@
"setupTestFrameworkScriptFile": "<rootDir>/test/setup.js",
"testPathIgnorePatterns": [
"/node_modules/"
]
],
"moduleNameMapper": {
"assets/images/video_player/(.*).png": "<rootDir>/dist/assets/images/video_player/$1@2x.png"
}
}
}