From 256c92bcb5002a95e88adb68bedacf948b6e6ac7 Mon Sep 17 00:00:00 2001 From: Simon Auer Date: Fri, 31 May 2019 19:14:56 +0200 Subject: [PATCH] merged longpress and initial zoom values in --- .gitignore | 5 + package.json | 2 +- src/ReactNativeZoomableView.js | 417 ++++++++++++--------------------- 3 files changed, 151 insertions(+), 273 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1502864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.vscode +node_modules +yarn.lock +package-lock.json diff --git a/package.json b/package.json index 614ec7e..ef77495 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dudigital/react-native-zoomable-view", - "version": "1.0.12", + "version": "1.0.13", "description": "A view component for react-native with pinch to zoom, tap to move and double tap to zoom capability.", "main": "index.js", "scripts": { diff --git a/src/ReactNativeZoomableView.js b/src/ReactNativeZoomableView.js index 8c37184..26bcae6 100644 --- a/src/ReactNativeZoomableView.js +++ b/src/ReactNativeZoomableView.js @@ -1,6 +1,10 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { View, StyleSheet, PanResponder } from "react-native"; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + View, + StyleSheet, + PanResponder, +} from 'react-native'; const initialState = { lastZoomLevel: 1, @@ -30,10 +34,8 @@ class ReactNativeZoomableView extends Component { distanceLeft: 0, distanceRight: 0, distanceTop: 0, - distanceBottom: 0 + distanceBottom: 0, }; - - this.longPressTimeout = null; } componentWillMount() { @@ -44,7 +46,7 @@ class ReactNativeZoomableView extends Component { onPanResponderMove: this._handlePanResponderMove, onPanResponderRelease: this._handlePanResponderEnd, onPanResponderTerminationRequest: evt => false, - onShouldBlockNativeResponder: evt => false + onShouldBlockNativeResponder: evt => false, }); } @@ -57,7 +59,13 @@ class ReactNativeZoomableView extends Component { }); } } - + + /** + * Last press time (used to evaluate whether user double tapped) + * @type {number} + */ + longPressTimeout = null; + /** * Current position of zoom center * @type { x: number, y: number } @@ -74,7 +82,7 @@ class ReactNativeZoomableView extends Component { return { ...this.state, ...this.contextState, - ...overwriteObj + ...overwriteObj, }; } @@ -85,12 +93,12 @@ class ReactNativeZoomableView extends Component { * @param layoutEvent * @private */ - _getBoxDimensions = layoutEvent => { + _getBoxDimensions = (layoutEvent) => { const { x, y, height, width } = layoutEvent.nativeEvent.layout; this.setState({ originalWidth: width, - originalHeight: height + originalHeight: height, }); }; @@ -107,15 +115,10 @@ class ReactNativeZoomableView extends Component { this._doubleTapCheck(e, gestureState); if (this.props.onStartShouldSetPanResponder) { - this.props.onStartShouldSetPanResponder( - e, - gestureState, - this._getZoomableViewEventObject(), - false - ); + this.props.onStartShouldSetPanResponder(e, gestureState, this._getZoomableViewEventObject(), false); } - return this.props.zoomEnabled; + return false; }; /** @@ -127,23 +130,24 @@ class ReactNativeZoomableView extends Component { */ _handleMoveShouldSetPanResponder = (e, gestureState) => { let baseComponentResult = - this.props.zoomEnabled && - (Math.abs(gestureState.dx) > 2 || - Math.abs(gestureState.dy) > 2 || - gestureState.numberActiveTouches === 2); + this.props.zoomEnabled && + (Math.abs(gestureState.dx) > 2 || + Math.abs(gestureState.dy) > 2 || + gestureState.numberActiveTouches === 2); if (this.props.onMoveShouldSetPanResponder) { baseComponentResult = this.props.onMoveShouldSetPanResponder( - e, - gestureState, - this._getZoomableViewEventObject(), - baseComponentResult + e, + gestureState, + this._getZoomableViewEventObject(), + baseComponentResult ); } return baseComponentResult; }; + /** * Calculates pinch distance * @@ -153,16 +157,13 @@ class ReactNativeZoomableView extends Component { */ _handlePanResponderGrant = (e, gestureState) => { if (gestureState.numberActiveTouches === 2) { - let dx = Math.abs( - e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX - ); - let dy = Math.abs( - e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY - ); + let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX); + let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY); let distant = Math.sqrt(dx * dx + dy * dy); this.distance = distant; } + if (this.props.onLongPress) { this.longPressTimeout = setTimeout(() => { if (this.props.onLongPress) { @@ -171,12 +172,9 @@ class ReactNativeZoomableView extends Component { } }, this.props.longPressDuration); } + if (this.props.onPanResponderGrant) { - this.props.onPanResponderGrant( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onPanResponderGrant(e, gestureState, this._getZoomableViewEventObject()); } }; @@ -192,38 +190,28 @@ class ReactNativeZoomableView extends Component { this.setState({ lastX: this.state.offsetX, lastY: this.state.offsetY, - lastZoomLevel: this.state.zoomLevel + lastZoomLevel: this.state.zoomLevel, }); + if (this.longPressTimeout) { clearTimeout(this.longPressTimeout); this.longPressTimeout = null; } + this.lastPressHolder = null; if (this.props.onPanResponderEnd) { - this.props.onPanResponderEnd( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onPanResponderEnd(e, gestureState, this._getZoomableViewEventObject()); } - if (this.gestureType === "pinch") { + if (this.gestureType === 'pinch') { this.pinchZoomPosition = null; if (this.props.onZoomEnd) { - this.props.onZoomEnd( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onZoomEnd(e, gestureState, this._getZoomableViewEventObject()); } - } else if (this.gestureType === "shift") { + } else if (this.gestureType === 'shift') { if (this.props.onShiftingEnd) { - this.props.onShiftingEnd( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onShiftingEnd(e, gestureState, this._getZoomableViewEventObject()); } } @@ -241,25 +229,16 @@ class ReactNativeZoomableView extends Component { * * @returns {number} */ - _getBoundOffsetValue( - axis: "x" | "y", - offsetValue: number, - containerSize: number, - elementSize: number, - zoomLevel: number - ) { - const zoomLevelOffsetValue = zoomLevel * offsetValue; - - const containerToScaledElementRatioSub = 1 - containerSize / elementSize; - const halfLengthPlusScaledHalf = 0.5 + 0.5 / zoomLevel; - const startBorder = - containerSize * - containerToScaledElementRatioSub * - halfLengthPlusScaledHalf; + _getBoundOffsetValue(axis: 'x'|'y', offsetValue: number, containerSize: number, elementSize: number, zoomLevel: number) { + const zoomLevelOffsetValue = (zoomLevel * offsetValue); + + const containerToScaledElementRatioSub = 1 - (containerSize / elementSize); + const halfLengthPlusScaledHalf = 0.5 + (0.5 / zoomLevel); + const startBorder = containerSize * containerToScaledElementRatioSub * halfLengthPlusScaledHalf; const endBorder = (containerSize + startBorder - containerSize) * -1; // calculate distance to start and end borders - const distanceToStart = offsetValue - startBorder; + const distanceToStart = (offsetValue - startBorder); const distanceToEnd = (offsetValue + startBorder) * -1; // set context for callback events @@ -269,7 +248,7 @@ class ReactNativeZoomableView extends Component { // => (our zoomed content is smaller than the frame) // => so center it if (containerSize > elementSize) { - return containerSize / 2 - elementSize / 2 / zoomLevel; + return ((containerSize / 2) - (elementSize / 2) / zoomLevel); } // if everything above failed @@ -300,12 +279,8 @@ class ReactNativeZoomableView extends Component { * @param distanceToEnd * @private */ - _setContextStateDistances( - axis: "x" | "y", - distanceToStart: number, - distanceToEnd: number - ) { - if (axis === "x") { + _setContextStateDistances(axis: 'x'|'y', distanceToStart: number, distanceToEnd: number) { + if (axis === 'x') { this.contextState.distanceLeft = distanceToStart; this.contextState.distanceRight = distanceToEnd; return; @@ -330,10 +305,8 @@ class ReactNativeZoomableView extends Component { */ _bindOffsetValuesToBorders(changeObj, bindToBorders = null) { // if bindToBorders is disabled -> nothing do here - if ( - bindToBorders === false || - (bindToBorders === null && !this.props.bindToBorders) - ) { + if (bindToBorders === false || + (bindToBorders === null && !this.props.bindToBorders)) { return changeObj; } @@ -343,22 +316,10 @@ class ReactNativeZoomableView extends Component { const currentElementHeight = originalHeight * changeObj.zoomLevel; // make sure that view doesn't go out of borders - const offsetXBound = this._getBoundOffsetValue( - "x", - changeObj.offsetX, - originalWidth, - currentElementWidth, - changeObj.zoomLevel - ); + const offsetXBound = this._getBoundOffsetValue('x', changeObj.offsetX, originalWidth, currentElementWidth, changeObj.zoomLevel); changeObj.offsetX = offsetXBound; - const offsetYBound = this._getBoundOffsetValue( - "y", - changeObj.offsetY, - originalHeight, - currentElementHeight, - changeObj.zoomLevel - ); + const offsetYBound = this._getBoundOffsetValue('y', changeObj.offsetY, originalHeight, currentElementHeight, changeObj.zoomLevel); changeObj.offsetY = offsetYBound; return changeObj; @@ -374,33 +335,31 @@ class ReactNativeZoomableView extends Component { */ _handlePanResponderMove = (e, gestureState) => { if (this.props.onPanResponderMove) { - if ( - this.props.onPanResponderMove( - e, - gestureState, - this._getZoomableViewEventObject() - ) - ) { + if (this.props.onPanResponderMove(e, gestureState, this._getZoomableViewEventObject())) { return false; } } + if (gestureState.numberActiveTouches === 2) { if (this.longPressTimeout) { clearTimeout(this.longPressTimeout); this.longPressTimeout = null; } - this.gestureType = "pinch"; + + this.gestureType = 'pinch'; this._handlePinching(e, gestureState); - } else if (gestureState.numberActiveTouches === 1) { + } + else if (gestureState.numberActiveTouches === 1) { if ( - this.longPressTimeout && - (Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5) + this.longPressTimeout && + (Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5) ) { clearTimeout(this.longPressTimeout); this.longPressTimeout = null; } - if (this.gestureType !== "pinch") { - this.gestureType = "shift"; + + if (this.gestureType !== 'pinch') { + this.gestureType = 'shift'; } this._handleMovement(e, gestureState); } @@ -415,44 +374,22 @@ class ReactNativeZoomableView extends Component { * @private */ _handlePinching = (e, gestureState) => { - const { - maxZoom, - minZoom, - zoomCenteringLevelDistance, - pinchToZoomInSensitivity, - pinchToZoomOutSensitivity - } = this.props; - - let dx = Math.abs( - e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX - ); - let dy = Math.abs( - e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY - ); + const { maxZoom, minZoom, zoomCenteringLevelDistance, pinchToZoomInSensitivity, pinchToZoomOutSensitivity } = this.props; + + let dx = Math.abs(e.nativeEvent.touches[0].pageX - e.nativeEvent.touches[1].pageX); + let dy = Math.abs(e.nativeEvent.touches[0].pageY - e.nativeEvent.touches[1].pageY); let distant = Math.sqrt(dx * dx + dy * dy); if (this.props.onZoomBefore) { - if ( - this.props.onZoomBefore( - e, - gestureState, - this._getZoomableViewEventObject() - ) - ) { + if (this.props.onZoomBefore(e, gestureState, this._getZoomableViewEventObject())) { return false; } } // define the new zoom level and take zoom level sensitivity into consideration - const zoomChangeFromStartOfPinch = distant / this.distance; - const pinchToZoomSensitivity = - zoomChangeFromStartOfPinch < 1 - ? pinchToZoomOutSensitivity - : pinchToZoomInSensitivity; - let zoomLevel = - (zoomChangeFromStartOfPinch * this.state.lastZoomLevel + - this.state.lastZoomLevel * pinchToZoomSensitivity) / - (pinchToZoomSensitivity + 1); + const zoomChangeFromStartOfPinch = (distant / this.distance); + const pinchToZoomSensitivity = (zoomChangeFromStartOfPinch < 1) ? pinchToZoomOutSensitivity : pinchToZoomInSensitivity; + let zoomLevel = ((zoomChangeFromStartOfPinch * this.state.lastZoomLevel) + this.state.lastZoomLevel * pinchToZoomSensitivity) / (pinchToZoomSensitivity + 1); // make sure max and min zoom levels are respected if (maxZoom !== null && zoomLevel > maxZoom) { @@ -465,53 +402,29 @@ class ReactNativeZoomableView extends Component { // only use the first position we get by pinching, or the screen will "wobble" during zoom action if (this.pinchZoomPosition === null) { - const pinchToZoomCenterX = - Math.min( - e.nativeEvent.touches[0].locationX, - e.nativeEvent.touches[1].locationX - ) + - dx / 2; - const pinchToZoomCenterY = - Math.min( - e.nativeEvent.touches[0].locationY, - e.nativeEvent.touches[1].locationY - ) + - dy / 2; - - this.pinchZoomPosition = this._getOffsetAdjustedPosition( - pinchToZoomCenterX, - pinchToZoomCenterY - ); + const pinchToZoomCenterX = Math.min(e.nativeEvent.touches[ 0 ].pageX, e.nativeEvent.touches[ 1 ].pageX) + ( dx / 2 ); + const pinchToZoomCenterY = Math.min(e.nativeEvent.touches[ 0 ].pageY, e.nativeEvent.touches[ 1 ].pageY) + ( dy / 2 ); + + this.pinchZoomPosition = this._getOffsetAdjustedPosition(pinchToZoomCenterX, pinchToZoomCenterY); } // make sure we shift the layer slowly during our zoom movement - const zoomStage = - Math.abs(zoomLevel - this.state.lastZoomLevel) / - zoomCenteringLevelDistance; + const zoomStage = Math.abs(zoomLevel - this.state.lastZoomLevel) / zoomCenteringLevelDistance; - const ratioOffsetX = - this.state.lastX + zoomStage * this.pinchZoomPosition.x; - const ratioOffsetY = - this.state.lastY + zoomStage * this.pinchZoomPosition.y; + const ratioOffsetX = this.state.lastX + zoomStage * this.pinchZoomPosition.x; + const ratioOffsetY = this.state.lastY + zoomStage * this.pinchZoomPosition.y; // define the changeObject and make sure the offset values are bound to view - const changeStateObj = this._bindOffsetValuesToBorders( - { - zoomLevel, - lastMovePinch: true, - offsetX: ratioOffsetX, - offsetY: ratioOffsetY - }, - null - ); + const changeStateObj = this._bindOffsetValuesToBorders({ + zoomLevel, + lastMovePinch: true, + offsetX: ratioOffsetX, + offsetY: ratioOffsetY, + }, null); this.setState(changeStateObj, () => { if (this.props.onZoomAfter) { - this.props.onZoomAfter( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onZoomAfter(e, gestureState, this._getZoomableViewEventObject()); } }); }; @@ -532,44 +445,25 @@ class ReactNativeZoomableView extends Component { return; } - let offsetX = - this.state.lastX + - gestureState.dx / this.state.zoomLevel / movementSensibility; - let offsetY = - this.state.lastY + - gestureState.dy / this.state.zoomLevel / movementSensibility; + let offsetX = this.state.lastX + gestureState.dx / this.state.zoomLevel / movementSensibility; + let offsetY = this.state.lastY + gestureState.dy / this.state.zoomLevel / movementSensibility; if (this.props.onShiftingBefore) { - if ( - this.props.onShiftingBefore( - e, - gestureState, - this._getZoomableViewEventObject() - ) - ) { + if (this.props.onShiftingBefore(e, gestureState, this._getZoomableViewEventObject())) { return false; } } - const changeStateObj = this._bindOffsetValuesToBorders( - { - lastMovePinch: false, - zoomLevel: this.state.zoomLevel, - offsetX, - offsetY - }, - null - ); + const changeStateObj = this._bindOffsetValuesToBorders({ + lastMovePinch: false, + zoomLevel: this.state.zoomLevel, + offsetX, + offsetY, + }, null); this.setState(changeStateObj, () => { if (this.props.onShiftingAfter) { - if ( - this.props.onShiftingAfter( - e, - gestureState, - this._getZoomableViewEventObject() - ) - ) { + if (this.props.onShiftingAfter(e, gestureState, this._getZoomableViewEventObject())) { return false; } } @@ -587,10 +481,7 @@ class ReactNativeZoomableView extends Component { _doubleTapCheck(e, gestureState) { const now = new Date().getTime(); - if ( - this.lastPressHolder && - now - this.lastPressHolder < this.props.doubleTapDelay - ) { + if (this.lastPressHolder && (now - this.lastPressHolder) < this.props.doubleTapDelay) { delete this.lastPressHolder; this._handleDoubleTap(e, gestureState); } else { @@ -613,33 +504,27 @@ class ReactNativeZoomableView extends Component { } if (this.props.onDoubleTap) { - this.props.onDoubleTapBefore( - e, - gestureState, - this._getZoomableViewEventObject() - ); + this.props.onDoubleTapBefore(e, gestureState, this._getZoomableViewEventObject()); } const nextZoomStep = this._getNextZoomStep(); this._zoomToLocation( - e.nativeEvent.locationX, - e.nativeEvent.locationY, - nextZoomStep, - true + e.nativeEvent.locationX, + e.nativeEvent.locationY, + nextZoomStep, + true ); if (this.props.onDoubleTapAfter) { - this.props.onDoubleTapAfter( - e, - gestureState, - this._getZoomableViewEventObject({ - zoomLevel: nextZoomStep - }) - ); + this.props.onDoubleTapAfter(e, gestureState, this._getZoomableViewEventObject({ + zoomLevel: nextZoomStep, + })); } + } + /** * Returns the next zoom step based on current step and zoomStep property. * If we are zoomed all the way in -> return to initialzoom @@ -654,7 +539,7 @@ class ReactNativeZoomableView extends Component { return initialZoom; } - let nextZoomStep = zoomLevel + zoomLevel * zoomStep; + let nextZoomStep = zoomLevel + (zoomLevel * zoomStep); if (maxZoom !== null && nextZoomStep > maxZoom) { return maxZoom; } @@ -678,8 +563,8 @@ class ReactNativeZoomableView extends Component { const currentElementHeight = originalHeight; const returnObj = { - x: -x + currentElementWidth / 2, - y: -y + currentElementHeight / 2 + x: (-x + (currentElementWidth / 2)), + y: (-y + (currentElementHeight / 2)), }; return returnObj; @@ -696,27 +581,18 @@ class ReactNativeZoomableView extends Component { * * @private */ - _zoomToLocation( - x: number, - y: number, - newZoomLevel: number, - bindToBorders = true, - callbk = null - ) { + _zoomToLocation(x: number, y: number, newZoomLevel: number, bindToBorders = true, callbk = null) { const offsetAdjustedPosition = this._getOffsetAdjustedPosition(x, y); // define the changeObject and make sure the offset values are bound to view - const changeStateObj = this._bindOffsetValuesToBorders( - { - zoomLevel: newZoomLevel, - offsetX: offsetAdjustedPosition.x, - offsetY: offsetAdjustedPosition.y, - lastZoomLevel: newZoomLevel, - lastX: offsetAdjustedPosition.x, - lastY: offsetAdjustedPosition.y - }, - bindToBorders - ); + const changeStateObj = this._bindOffsetValuesToBorders({ + zoomLevel: newZoomLevel, + offsetX: offsetAdjustedPosition.x, + offsetY: offsetAdjustedPosition.y, + lastZoomLevel: newZoomLevel, + lastX: offsetAdjustedPosition.x, + lastY: offsetAdjustedPosition.y, + }, bindToBorders); this.setState(changeStateObj, () => { if (callbk) { @@ -727,28 +603,24 @@ class ReactNativeZoomableView extends Component { render() { return ( - - {this.props.children} + + {this.props.children} + - ); } } @@ -806,15 +678,16 @@ ReactNativeZoomableView.defaultProps = { const styles = StyleSheet.create({ wrapper: { flex: 1, - width: "100%", - justifyContent: "center" + width: '100%', + justifyContent: 'center', }, container: { flex: 1, - justifyContent: "center", - alignItems: "center", - position: "relative" - } + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + }, }); export default ReactNativeZoomableView; +