Skip to content

Commit

Permalink
Merge pull request #2085 from Expensify/marcaaron-addZoomForWeb
Browse files Browse the repository at this point in the history
Add ability to zoom images on web/desktop
  • Loading branch information
roryabraham authored Mar 29, 2021
2 parents b5cee79 + 5723af2 commit 7d35d9d
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 22 deletions.
154 changes: 132 additions & 22 deletions src/components/ImageView/index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,145 @@
import React, {memo} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View, Image} from 'react-native';
import styles from '../../styles/styles';
import {View, Image, Pressable} from 'react-native';
import styles, {getZoomCursorStyle, getZoomSizingStyle} from '../../styles/styles';
import canUseTouchScreen from '../../libs/canUseTouchscreen';

const propTypes = {
// URL to full-sized image
url: PropTypes.string.isRequired,
};

const ImageView = props => (
<View
style={[
class ImageView extends PureComponent {
constructor(props) {
super(props);
this.scrollableRef = null;
this.canUseTouchScreen = canUseTouchScreen();
this.containerStyles = [
styles.w100,
styles.h100,
styles.alignItemsCenter,
styles.justifyContentCenter,
styles.overflowHidden,
]}
>
<Image
source={{uri: props.url}}
style={[
styles.w100,
styles.h100,
]}
resizeMode="center"
/>
</View>
);
];
this.state = {
isZoomed: false,
isDragging: false,
isMouseDown: false,
initialScrollLeft: 0,
initialScrollTop: 0,
initialX: 0,
initialY: 0,
};
}

ImageView.propTypes = propTypes;
ImageView.displayName = 'ImageView';
componentDidMount() {
if (this.canUseTouchScreen) {
return;
}

document.addEventListener('mousemove', this.trackMovement.bind(this));
}

componentWillUnmount() {
if (this.canUseTouchScreen) {
return;
}

document.removeEventListener('mousemove', this.trackMovement.bind(this));
}

trackMovement(e) {
if (!this.state.isZoomed) {
return;
}

if (this.state.isDragging && this.state.isMouseDown) {
const x = e.nativeEvent.x;
const y = e.nativeEvent.y;
const moveX = this.state.initialX - x;
const moveY = this.state.initialY - y;
this.scrollableRef.scrollLeft = this.state.initialScrollLeft + moveX;
this.scrollableRef.scrollTop = this.state.initialScrollTop + moveY;
}

export default memo(ImageView);
this.setState(prevState => ({isDragging: prevState.isMouseDown}));
}

render() {
if (this.canUseTouchScreen) {
return (
<View
style={[...this.containerStyles, styles.overflowHidden]}
>
<Image
source={{uri: this.props.url}}
style={[
styles.w100,
styles.h100,
]}
resizeMode="center"
/>
</View>
);
}

return (
<View
ref={el => this.scrollableRef = el}
style={[
...this.containerStyles,
styles.overflowScroll,
styles.noScrollbars,
]}
>
<Pressable
style={[
styles.w100,
styles.h100,
styles.flex1,
getZoomCursorStyle(this.state.isZoomed, this.state.isDragging),
]}
onPressIn={(e) => {
const {pageX, pageY} = e.nativeEvent;
this.setState({
isMouseDown: true,
initialX: pageX,
initialY: pageY,
initialScrollLeft: this.scrollableRef.scrollLeft,
initialScrollTop: this.scrollableRef.scrollTop,
});
}}
onPress={(e) => {
if (this.state.isZoomed && !this.state.isDragging) {
const {offsetX, offsetY} = e.nativeEvent;
this.scrollableRef.scrollTop = offsetY * 1.5;
this.scrollableRef.scrollLeft = offsetX * 1.5;
}

if (this.state.isZoomed && this.state.isDragging && this.state.isMouseDown) {
this.setState({isDragging: false, isMouseDown: false});
}
}}
onPressOut={() => {
if (this.state.isDragging) {
return;
}

this.setState(prevState => ({
isZoomed: !prevState.isZoomed,
isMouseDown: false,
}));
}}
>
<Image
source={{uri: this.props.url}}
style={getZoomSizingStyle(this.state.isZoomed)}
resizeMode="center"
/>
</Pressable>
</View>
);
}
}

ImageView.propTypes = propTypes;
export default ImageView;
32 changes: 32 additions & 0 deletions src/libs/canUseTouchscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Allows us to identify whether the platform has a touchscreen.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
*
* @returns {Boolean}
*/
function canUseTouchScreen() {
let hasTouchScreen = false;
if ('maxTouchPoints' in navigator) {
hasTouchScreen = navigator.maxTouchPoints > 0;
} else if ('msMaxTouchPoints' in navigator) {
hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
const mQ = window.matchMedia && matchMedia('(pointer:coarse)');
if (mQ && mQ.media === '(pointer:coarse)') {
hasTouchScreen = !!mQ.matches;
} else if ('orientation' in window) {
hasTouchScreen = true; // deprecated, but good fallback
} else {
// Only as a last resort, fall back to user agent sniffing
const UA = navigator.userAgent;
hasTouchScreen = (
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA)
|| /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
);
}
}
return hasTouchScreen;
}

export default canUseTouchScreen;
32 changes: 32 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,10 @@ const styles = {
fontWeight: fontWeightBold,
fontSize: variables.iouAmountTextSize,
}, 0),

noScrollbars: {
scrollbarWidth: 'none',
},
};

const baseCodeTagStyles = {
Expand Down Expand Up @@ -1374,6 +1378,32 @@ function getNavigationModalCardStyle(isSmallScreenWidth) {
};
}

/**
* @param {Boolean} isZoomed
* @param {Boolean} isDragging
* @return {Object}
*/
function getZoomCursorStyle(isZoomed, isDragging) {
if (!isZoomed) {
return {cursor: 'zoom-in'};
}

return {
cursor: isDragging ? 'grabbing' : 'zoom-out',
};
}

/**
* @param {Boolean} isZoomed
* @return {Object}
*/
function getZoomSizingStyle(isZoomed) {
return {
height: isZoomed ? '250%' : '100%',
width: isZoomed ? '250%' : '100%',
};
}

export default styles;
export {
getSafeAreaPadding,
Expand All @@ -1382,4 +1412,6 @@ export {
getNavigationDrawerStyle,
getNavigationDrawerType,
getNavigationModalCardStyle,
getZoomCursorStyle,
getZoomSizingStyle,
};

0 comments on commit 7d35d9d

Please sign in to comment.