diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js index 538ca792b233..c92bd7738253 100644 --- a/src/components/ImageView/index.js +++ b/src/components/ImageView/index.js @@ -1,11 +1,10 @@ -import React, {PureComponent} from 'react'; +import React, {useState, useEffect, useRef, useCallback} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import Image from '../Image'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import FullscreenLoadingIndicator from '../FullscreenLoadingIndicator'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import CONST from '../../CONST'; @@ -23,159 +22,93 @@ const propTypes = { /** image file name */ fileName: PropTypes.string.isRequired, - - ...windowDimensionsPropTypes, }; const defaultProps = { isAuthTokenRequired: false, }; -class ImageView extends PureComponent { - constructor(props) { - super(props); - this.scrollableRef = null; - this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - this.onContainerLayoutChanged = this.onContainerLayoutChanged.bind(this); - this.onContainerPressIn = this.onContainerPressIn.bind(this); - this.onContainerPress = this.onContainerPress.bind(this); - this.imageLoad = this.imageLoad.bind(this); - this.imageLoadingStart = this.imageLoadingStart.bind(this); - this.trackMovement = this.trackMovement.bind(this); - this.trackPointerPosition = this.trackPointerPosition.bind(this); - - this.state = { - isLoading: true, - containerHeight: 0, - containerWidth: 0, - isZoomed: false, - isDragging: false, - isMouseDown: false, - initialScrollLeft: 0, - initialScrollTop: 0, - initialX: 0, - initialY: 0, - imgWidth: 0, - imgHeight: 0, - zoomScale: 0, - }; - } - - componentDidMount() { - if (this.canUseTouchScreen) { - return; - } - - document.addEventListener('mousemove', this.trackMovement); - document.addEventListener('mouseup', this.trackPointerPosition); - } - - componentDidUpdate(prevProps) { - if (prevProps.url === this.props.url || this.state.isLoading) { - return; - } - - this.imageLoadingStart(); - } +function ImageView({isAuthTokenRequired, url, fileName}) { + const [isLoading, setIsLoading] = useState(true); + const [containerHeight, setContainerHeight] = useState(0); + const [containerWidth, setContainerWidth] = useState(0); + const [isZoomed, setIsZoomed] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [isMouseDown, setIsMouseDown] = useState(false); + const [initialScrollLeft, setInitialScrollLeft] = useState(0); + const [initialScrollTop, setInitialScrollTop] = useState(0); + const [initialX, setInitialX] = useState(0); + const [initialY, setInitialY] = useState(0); + const [imgWidth, setImgWidth] = useState(0); + const [imgHeight, setImgHeight] = useState(0); + const [zoomScale, setZoomScale] = useState(0); + const [zoomDelta, setZoomDelta] = useState({offsetX: 0, offsetY: 0}); + + const scrollableRef = useRef(null); + const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - componentWillUnmount() { - if (this.canUseTouchScreen) { + /** + * @param {Number} newContainerWidth + * @param {Number} newContainerHeight + * @param {Number} newImageWidth + * @param {Number} newImageHeight + */ + const setScale = (newContainerWidth, newContainerHeight, newImageWidth, newImageHeight) => { + if (!newContainerWidth || !newImageWidth || !newContainerHeight || !newImageHeight) { return; } - - document.removeEventListener('mousemove', this.trackMovement); - document.removeEventListener('mouseup', this.trackPointerPosition); - } + const newZoomScale = Math.min(newContainerWidth / newImageWidth, newContainerHeight / newImageHeight); + setZoomScale(newZoomScale); + }; /** * @param {SyntheticEvent} e */ - onContainerLayoutChanged(e) { + const onContainerLayoutChanged = (e) => { const {width, height} = e.nativeEvent.layout; - this.setScale(width, height, this.state.imgWidth, this.state.imgHeight); - this.setState({ - containerHeight: height, - containerWidth: width, - }); - } + setScale(width, height, imgWidth, imgHeight); - /** - * @param {SyntheticEvent} e - */ - onContainerPressIn(e) { - const {pageX, pageY} = e.nativeEvent; - this.setState({ - isMouseDown: true, - initialX: pageX, - initialY: pageY, - initialScrollLeft: this.scrollableRef.scrollLeft, - initialScrollTop: this.scrollableRef.scrollTop, - }); - } - - /** - * @param {SyntheticEvent} e - */ - onContainerPress(e) { - let scrollX; - let scrollY; - if (!this.state.isZoomed && !this.state.isDragging) { - const {offsetX, offsetY} = e.nativeEvent; - - // Dividing clicked positions by the zoom scale to get coordinates - // so that once we zoom we will scroll to the clicked location. - const delta = this.getScrollOffset(offsetX / this.state.zoomScale, offsetY / this.state.zoomScale); - scrollX = delta.offsetX; - scrollY = delta.offsetY; - } - - if (this.state.isZoomed && this.state.isDragging && this.state.isMouseDown) { - this.setState({isDragging: false, isMouseDown: false}); - } else { - // We first zoom and once its done then we scroll to the location the user clicked. - this.setState( - (prevState) => ({ - isZoomed: !prevState.isZoomed, - isMouseDown: false, - }), - () => { - this.scrollableRef.scrollTop = scrollY; - this.scrollableRef.scrollLeft = scrollX; - }, - ); - } - } + setContainerHeight(height); + setContainerWidth(width); + }; /** * When open image, set image width, height. * @param {Number} imageWidth * @param {Number} imageHeight */ - setImageRegion(imageWidth, imageHeight) { + const setImageRegion = (imageWidth, imageHeight) => { if (imageHeight <= 0) { return; } - - this.setScale(this.state.containerWidth, this.state.containerHeight, imageWidth, imageHeight); - this.setState({ - imgWidth: imageWidth, - imgHeight: imageHeight, - }); - } + setScale(containerWidth, containerHeight, imageWidth, imageHeight); + setImgWidth(imageWidth); + setImgHeight(imageHeight); + }; + + const imageLoadingStart = () => { + if (!isLoading) return; + setIsLoading(true); + setZoomScale(0); + setIsZoomed(false); + }; + + const imageLoad = ({nativeEvent}) => { + setImageRegion(nativeEvent.width, nativeEvent.height); + setIsLoading(false); + }; /** - * @param {Number} containerWidth - * @param {Number} containerHeight - * @param {Number} imageWidth - * @param {Number} imageHeight + * @param {SyntheticEvent} e */ - setScale(containerWidth, containerHeight, imageWidth, imageHeight) { - if (!containerWidth || !imageWidth || !containerHeight || !imageHeight) { - return; - } - const newZoomScale = Math.min(containerWidth / imageWidth, containerHeight / imageHeight); - this.setState({zoomScale: newZoomScale}); - } + const onContainerPressIn = (e) => { + const {pageX, pageY} = e.nativeEvent; + setIsMouseDown(true); + setInitialX(pageX); + setInitialY(pageY); + setInitialScrollLeft(scrollableRef.current.scrollLeft); + setInitialScrollTop(scrollableRef.current.scrollTop); + }; /** * Convert touch point to zoomed point @@ -183,131 +116,161 @@ class ImageView extends PureComponent { * @param {Boolean} y y point when click zoom * @returns {Object} converted touch point */ - getScrollOffset(x, y) { + const getScrollOffset = (x, y) => { let offsetX; let offsetY; // Container size bigger than clicked position offset - if (x <= this.state.containerWidth / 2) { + if (x <= containerWidth / 2) { offsetX = 0; - } else if (x > this.state.containerWidth / 2) { + } else if (x > containerWidth / 2) { // Minus half of container size because we want to be center clicked position - offsetX = x - this.state.containerWidth / 2; + offsetX = x - containerWidth / 2; } - if (y <= this.state.containerHeight / 2) { + if (y <= containerHeight / 2) { offsetY = 0; - } else if (y > this.state.containerHeight / 2) { + } else if (y > containerHeight / 2) { // Minus half of container size because we want to be center clicked position - offsetY = y - this.state.containerHeight / 2; + offsetY = y - containerHeight / 2; } return {offsetX, offsetY}; - } + }; /** * @param {SyntheticEvent} e */ - trackPointerPosition(e) { - // Whether the pointer is released inside the ImageView - const isInsideImageView = this.scrollableRef.contains(e.nativeEvent.target); - - if (!isInsideImageView && this.state.isZoomed && this.state.isDragging && this.state.isMouseDown) { - this.setState({isDragging: false, isMouseDown: false}); + const onContainerPress = (e) => { + if (!isZoomed && !isDragging) { + const {offsetX, offsetY} = e.nativeEvent; + // Dividing clicked positions by the zoom scale to get coordinates + // so that once we zoom we will scroll to the clicked location. + const delta = getScrollOffset(offsetX / zoomScale, offsetY / zoomScale); + setZoomDelta(delta); } - } - trackMovement(e) { - if (!this.state.isZoomed) { - return; + if (isZoomed && isDragging && isMouseDown) { + setIsDragging(false); + setIsMouseDown(false); + } else { + // We first zoom and once its done then we scroll to the location the user clicked. + setIsZoomed(!isZoomed); + setIsMouseDown(false); } + }; - 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; + /** + * @param {SyntheticEvent} e + */ + const trackPointerPosition = useCallback( + (e) => { + // Whether the pointer is released inside the ImageView + const isInsideImageView = scrollableRef.current.contains(e.nativeEvent.target); + + if (!isInsideImageView && isZoomed && isDragging && isMouseDown) { + setIsDragging(false); + setIsMouseDown(false); + } + }, + [isDragging, isMouseDown, isZoomed], + ); + + const trackMovement = useCallback( + (e) => { + if (!isZoomed) { + return; + } + + if (isDragging && isMouseDown) { + const x = e.nativeEvent.x; + const y = e.nativeEvent.y; + const moveX = initialX - x; + const moveY = initialY - y; + scrollableRef.current.scrollLeft = initialScrollLeft + moveX; + scrollableRef.current.scrollTop = initialScrollTop + moveY; + } + + setIsDragging(isMouseDown); + }, + [initialScrollLeft, initialScrollTop, initialX, initialY, isDragging, isMouseDown, isZoomed], + ); + + useEffect(() => { + if (!isZoomed || !zoomDelta || !scrollableRef.current) { + return; } + scrollableRef.current.scrollLeft = zoomDelta.offsetX; + scrollableRef.current.scrollTop = zoomDelta.offsetY; + }, [zoomDelta, isZoomed]); - this.setState((prevState) => ({isDragging: prevState.isMouseDown})); - } - - imageLoad({nativeEvent}) { - this.setImageRegion(nativeEvent.width, nativeEvent.height); - this.setState({isLoading: false}); - } - - imageLoadingStart() { - if (this.state.isLoading) { + useEffect(() => { + if (canUseTouchScreen) { return; } - this.setState({isLoading: true, zoomScale: 0, isZoomed: false}); - } + document.addEventListener('mousemove', trackMovement); + document.addEventListener('mouseup', trackPointerPosition); - render() { - if (this.canUseTouchScreen) { - return ( - - 1 ? Image.resizeMode.center : Image.resizeMode.contain} - onLoadStart={this.imageLoadingStart} - onLoad={this.imageLoad} - /> - {this.state.isLoading && } - - ); - } + return () => { + document.removeEventListener('mousemove', trackMovement); + document.removeEventListener('mouseup', trackPointerPosition); + }; + }, [canUseTouchScreen, trackMovement, trackPointerPosition]); + + if (canUseTouchScreen) { return ( (this.scrollableRef = el)} - onLayout={this.onContainerLayoutChanged} - style={[styles.imageViewContainer, styles.overflowAuto, styles.pRelative]} + style={[styles.imageViewContainer, styles.overflowHidden]} + onLayout={onContainerLayoutChanged} > - = 1 ? styles.pRelative : styles.pAbsolute), - ...styles.flex1, - }} - onPressIn={this.onContainerPressIn} - onPress={this.onContainerPress} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGE} - accessibilityLabel={this.props.fileName} - > - - - - {this.state.isLoading && } + 1 ? Image.resizeMode.center : Image.resizeMode.contain} + onLoadStart={imageLoadingStart} + onLoad={imageLoad} + /> + {isLoading && } ); } + return ( + + = 1 ? styles.pRelative : styles.pAbsolute), + ...styles.flex1, + }} + onPressIn={onContainerPressIn} + onPress={onContainerPress} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGE} + accessibilityLabel={fileName} + > + + + + {isLoading && } + + ); } ImageView.propTypes = propTypes; ImageView.defaultProps = defaultProps; -export default withWindowDimensions(ImageView); +ImageView.displayName = 'ImageView'; + +export default ImageView;