@cometchat/chat-uikit-react-native
Version:
Ready-to-use Chat UI Components for React Native
748 lines • 30 kB
JavaScript
import React, { Component, createRef } from 'react';
import { Animated, InteractionManager, PanResponder, StyleSheet, View, } from 'react-native';
import { applyPanBoundariesToOffset, calcGestureTouchDistance, calcNewScaledOffsetForZoomCentering, getBoundaryCrossedAnim, getPanMomentumDecayAnim, getZoomToAnimation } from './helper';
const initialState = {
originalWidth: null,
originalHeight: null,
originalPageX: null,
originalPageY: null,
};
function calcGestureCenterPoint(e, gestureState) {
const touches = e?.nativeEvent?.touches;
if (!touches[0])
return null;
if (gestureState.numberActiveTouches === 2) {
if (!touches[1])
return null;
return {
x: (touches[0].pageX + touches[1].pageX) / 2,
y: (touches[0].pageY + touches[1].pageY) / 2,
};
}
if (gestureState.numberActiveTouches === 1) {
return {
x: touches[0].pageX,
y: touches[0].pageY,
};
}
return null;
}
class ReactNativeZoomableView extends Component {
zoomSubjectWrapperRef;
gestureHandlers;
doubleTapFirstTapReleaseTimestamp;
static defaultProps = {
zoomEnabled: true,
panEnabled: true,
initialZoom: 1,
initialOffsetX: 0,
initialOffsetY: 0,
maxZoom: 3.5,
minZoom: 1,
pinchToZoomInSensitivity: 1,
pinchToZoomOutSensitivity: 1,
movementSensibility: 1,
doubleTapDelay: 300,
bindToBorders: true,
zoomStep: 0.5,
onLongPress: null,
longPressDuration: 700,
contentWidth: undefined,
contentHeight: undefined,
panBoundaryPadding: 0,
disablePanOnInitialZoom: false,
};
panAnim = new Animated.ValueXY({ x: 0, y: 0 });
zoomAnim = new Animated.Value(1);
__offsets = {
x: {
value: 0,
boundaryCrossedAnimInEffect: false,
},
y: {
value: 0,
boundaryCrossedAnimInEffect: false,
},
};
zoomLevel = 1;
lastGestureCenterPosition = null;
lastGestureTouchDistance;
gestureType;
gestureStarted = false;
/**
* Last press time (used to evaluate whether user double tapped)
* @type {number}
*/
longPressTimeout = null;
onTransformInvocationInitialized;
singleTapTimeoutId;
touches = [];
doubleTapFirstTap;
measureZoomSubjectInterval;
constructor(props) {
super(props);
this.gestureHandlers = PanResponder.create({
onStartShouldSetPanResponder: this._handleStartShouldSetPanResponder,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminate: (evt, gestureState) => {
// We should also call _handlePanResponderEnd
// to properly perform cleanups when the gesture is terminated
// (aka gesture handling responsibility is taken over by another component).
// This also fixes a weird issue where
// on real device, sometimes onPanResponderRelease is not called when you lift 2 fingers up,
// but onPanResponderTerminate is called instead for no apparent reason.
this._handlePanResponderEnd(evt, gestureState);
this.props.onPanResponderTerminate?.(evt, gestureState, this._getZoomableViewEventObject());
},
onPanResponderTerminationRequest: (evt, gestureState) => !!this.props.onPanResponderTerminationRequest?.(evt, gestureState, this._getZoomableViewEventObject()),
// Defaults to true to prevent parent components, such as React Navigation's tab view, from taking over as responder.
onShouldBlockNativeResponder: (evt, gestureState) => this.props.onShouldBlockNativeResponder?.(evt, gestureState, this._getZoomableViewEventObject()) ?? true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => this.props.onStartShouldSetPanResponderCapture?.(evt, gestureState),
onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.props.onMoveShouldSetPanResponderCapture?.(evt, gestureState),
});
this.zoomSubjectWrapperRef = createRef();
if (this.props.zoomAnimatedValue)
this.zoomAnim = this.props.zoomAnimatedValue;
if (this.props.panAnimatedValueXY)
this.panAnim = this.props.panAnimatedValueXY;
this.zoomLevel = props.initialZoom;
this.offsetX = props.initialOffsetX;
this.offsetY = props.initialOffsetY;
this.panAnim.setValue({ x: this.offsetX, y: this.offsetY });
this.zoomAnim.setValue(this.zoomLevel);
this.panAnim.addListener(({ x, y }) => {
this.offsetX = x;
this.offsetY = y;
});
this.zoomAnim.addListener(({ value }) => {
this.zoomLevel = value;
});
this.state = {
...initialState,
};
this.lastGestureTouchDistance = 150;
this.gestureType = null;
}
set offsetX(x) {
this.__setOffset('x', x);
}
set offsetY(y) {
this.__setOffset('y', y);
}
get offsetX() {
return this.__getOffset('x');
}
get offsetY() {
return this.__getOffset('y');
}
__setOffset(axis, offset) {
const offsetState = this.__offsets[axis];
const animValue = this.panAnim?.[axis];
if (this.props.bindToBorders) {
const containerSize = axis === 'x' ? this.state?.originalWidth : this.state?.originalHeight;
const contentSize = axis === 'x'
? this.props.contentWidth || this.state?.originalWidth
: this.props.contentHeight || this.state?.originalHeight;
const boundOffset = contentSize && containerSize
? applyPanBoundariesToOffset(offset, containerSize, contentSize, this.zoomLevel, this.props.panBoundaryPadding)
: offset;
if (animValue &&
!this.gestureType &&
!offsetState.boundaryCrossedAnimInEffect) {
const boundariesApplied = boundOffset !== offset &&
boundOffset.toFixed(3) !== offset.toFixed(3);
if (boundariesApplied) {
offsetState.boundaryCrossedAnimInEffect = true;
getBoundaryCrossedAnim(this.panAnim[axis], boundOffset).start(() => {
offsetState.boundaryCrossedAnimInEffect = false;
});
return;
}
}
}
offsetState.value = offset;
}
__getOffset(axis) {
return this.__offsets[axis].value;
}
componentDidUpdate(prevProps, prevState) {
const { zoomEnabled, initialZoom } = this.props;
if (prevProps.zoomEnabled && !zoomEnabled) {
this.zoomLevel = initialZoom;
this.zoomAnim.setValue(this.zoomLevel);
}
if (!this.onTransformInvocationInitialized &&
this._invokeOnTransform().successful) {
this.panAnim.addListener(() => this._invokeOnTransform());
this.zoomAnim.addListener(() => this._invokeOnTransform());
this.onTransformInvocationInitialized = true;
}
const currState = this.state;
const originalMeasurementsChanged = currState.originalHeight !== prevState.originalHeight ||
currState.originalWidth !== prevState.originalWidth ||
currState.originalPageX !== prevState.originalPageX ||
currState.originalPageY !== prevState.originalPageY;
if (this.onTransformInvocationInitialized && originalMeasurementsChanged) {
this._invokeOnTransform();
}
}
componentDidMount() {
this.grabZoomSubjectOriginalMeasurements();
// We've already run `grabZoomSubjectOriginalMeasurements` at various events
// to make sure the measurements are promptly updated.
// However, there might be cases we haven't accounted for, especially when
// native processes are involved. To account for those cases,
// we'll use an interval here to ensure we're always up-to-date.
// The `setState` in `grabZoomSubjectOriginalMeasurements` won't trigger a rerender
// if the values given haven't changed, so we're not running performance risk here.
this.measureZoomSubjectInterval = setInterval(this.grabZoomSubjectOriginalMeasurements, 1e3);
}
componentWillUnmount() {
clearInterval(this.measureZoomSubjectInterval);
}
/**
* try to invoke onTransform
* @private
*/
_invokeOnTransform() {
const zoomableViewEvent = this._getZoomableViewEventObject();
if (!zoomableViewEvent.originalWidth || !zoomableViewEvent.originalHeight)
return { successful: false };
this.props.onTransform?.(zoomableViewEvent);
return { successful: true };
}
/**
* Returns additional information about components current state for external event hooks
*
* @returns {{}}
* @private
*/
_getZoomableViewEventObject(overwriteObj = {}) {
return {
zoomLevel: this.zoomLevel,
offsetX: this.offsetX,
offsetY: this.offsetY,
originalHeight: this.state.originalHeight,
originalWidth: this.state.originalWidth,
originalPageX: this.state.originalPageX,
originalPageY: this.state.originalPageY,
...overwriteObj,
};
}
/**
* Get the original box dimensions and save them for later use.
* (They will be used to calculate boxBorders)
*
* @private
*/
grabZoomSubjectOriginalMeasurements = () => {
// make sure we measure after animations are complete
InteractionManager.runAfterInteractions(() => {
// this setTimeout is here to fix a weird issue on iOS where the measurements are all `0`
// when navigating back (react-navigation stack) from another view
// while closing the keyboard at the same time
setTimeout(() => {
// In normal conditions, we're supposed to measure zoomSubject instead of its wrapper.
// However, our zoomSubject may have been transformed by an initial zoomLevel or offset,
// in which case these measurements will not represent the true "original" measurements.
// We just need to make sure the zoomSubjectWrapper perfectly aligns with the zoomSubject
// (no border, space, or anything between them)
const zoomSubjectWrapperRef = this.zoomSubjectWrapperRef;
// we don't wanna measure when zoomSubjectWrapperRef is not yet available or has been unmounted
zoomSubjectWrapperRef.current?.measureInWindow((x, y, width, height) => {
this.setState({
originalWidth: width,
originalHeight: height,
originalPageX: x,
originalPageY: y,
});
});
});
});
};
/**
* Handles the start of touch events and checks for taps
*
* @param e
* @param gestureState
* @returns {boolean}
*
* @private
*/
_handleStartShouldSetPanResponder = (e, gestureState) => {
if (this.props.onStartShouldSetPanResponder) {
this.props.onStartShouldSetPanResponder(e, gestureState, this._getZoomableViewEventObject(), false);
}
// Always set pan responder on start
// of gesture so we can handle tap.
// "Pan threshold validation" will be handled
// in `onPanResponderMove` instead of in `onMoveShouldSetPanResponder`
return true;
};
/**
* Calculates pinch distance
*
* @param e
* @param gestureState
* @private
*/
_handlePanResponderGrant = (e, gestureState) => {
if (this.props.onLongPress) {
this.longPressTimeout = setTimeout(() => {
this.props.onLongPress?.(e, gestureState, this._getZoomableViewEventObject());
this.longPressTimeout = null;
}, this.props.longPressDuration);
}
this.props.onPanResponderGrant?.(e, gestureState, this._getZoomableViewEventObject());
this.panAnim.stopAnimation();
this.zoomAnim.stopAnimation();
this.gestureStarted = true;
};
/**
* Handles the end of touch events
*
* @param e
* @param gestureState
*
* @private
*/
_handlePanResponderEnd = (e, gestureState) => {
if (!this.gestureType) {
this._resolveAndHandleTap(e);
}
this.lastGestureCenterPosition = null;
// Trigger final shift animation unless panEnabled is false or disablePanOnInitialZoom is true and we're on the initial zoom level
if (this.props.panEnabled &&
!(this.gestureType === 'shift' &&
this.props.disablePanOnInitialZoom &&
this.zoomLevel === this.props.initialZoom)) {
getPanMomentumDecayAnim(this.panAnim, {
x: gestureState.vx / this.zoomLevel,
y: gestureState.vy / this.zoomLevel,
}).start();
}
if (this.longPressTimeout) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = null;
}
this.props.onPanResponderEnd?.(e, gestureState, this._getZoomableViewEventObject());
if (this.gestureType === 'pinch') {
this.props.onZoomEnd?.(e, gestureState, this._getZoomableViewEventObject());
}
else if (this.gestureType === 'shift') {
this.props.onShiftingEnd?.(e, gestureState, this._getZoomableViewEventObject());
}
this.gestureType = null;
this.gestureStarted = false;
};
/**
* Handles the actual movement of our pan responder
*
* @param e
* @param gestureState
*
* @private
*/
_handlePanResponderMove = (e, gestureState) => {
if (this.props.onPanResponderMove) {
if (this.props.onPanResponderMove(e, gestureState, this._getZoomableViewEventObject())) {
return false;
}
}
// Only supports 2 touches and below,
// any invalid number will cause the gesture to end.
if (gestureState.numberActiveTouches <= 2) {
if (!this.gestureStarted) {
this._handlePanResponderGrant(e, gestureState);
}
}
else {
if (this.gestureStarted) {
this._handlePanResponderEnd(e, gestureState);
}
return true;
}
if (gestureState.numberActiveTouches === 2) {
if (this.longPressTimeout) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = null;
}
// change some measurement states when switching gesture to ensure a smooth transition
if (this.gestureType !== 'pinch') {
this.lastGestureCenterPosition = calcGestureCenterPoint(e, gestureState);
this.lastGestureTouchDistance = calcGestureTouchDistance(e, gestureState);
}
this.gestureType = 'pinch';
this._handlePinching(e, gestureState);
}
else if (gestureState.numberActiveTouches === 1) {
if (this.longPressTimeout &&
(Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5)) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = null;
}
// change some measurement states when switching gesture to ensure a smooth transition
if (this.gestureType !== 'shift') {
this.lastGestureCenterPosition = calcGestureCenterPoint(e, gestureState);
}
const { dx, dy } = gestureState;
const isShiftGesture = Math.abs(dx) > 2 || Math.abs(dy) > 2;
if (isShiftGesture) {
this.gestureType = 'shift';
this._handleShifting(gestureState);
}
}
return false;
};
/**
* Handles the pinch movement and zooming
*
* @param e
* @param gestureState
*
* @private
*/
_handlePinching(e, gestureState) {
if (!this.props.zoomEnabled)
return;
const { maxZoom, minZoom, pinchToZoomInSensitivity, pinchToZoomOutSensitivity, } = this.props;
const distance = calcGestureTouchDistance(e, gestureState);
if (this.props.onZoomBefore &&
this.props.onZoomBefore(e, gestureState, this._getZoomableViewEventObject())) {
return;
}
// define the new zoom level and take zoom level sensitivity into consideration
const zoomGrowthFromLastGestureState = distance / this.lastGestureTouchDistance;
this.lastGestureTouchDistance = distance;
const pinchToZoomSensitivity = zoomGrowthFromLastGestureState < 1
? pinchToZoomOutSensitivity
: pinchToZoomInSensitivity;
const deltaGrowth = zoomGrowthFromLastGestureState - 1;
// 0 - no resistance
// 10 - 90% resistance
const deltaGrowthAdjustedBySensitivity = deltaGrowth * (1 - (pinchToZoomSensitivity * 9) / 100);
let newZoomLevel = this.zoomLevel * (1 + deltaGrowthAdjustedBySensitivity);
// make sure max and min zoom levels are respected
if (maxZoom !== null && newZoomLevel > maxZoom) {
newZoomLevel = maxZoom;
}
if (newZoomLevel < minZoom) {
newZoomLevel = minZoom;
}
const gestureCenterPoint = calcGestureCenterPoint(e, gestureState);
if (!gestureCenterPoint)
return;
const zoomCenter = {
x: gestureCenterPoint.x - this.state.originalPageX,
y: gestureCenterPoint.y - this.state.originalPageY,
};
const { originalHeight, originalWidth } = this.state;
const oldOffsetX = this.offsetX;
const oldOffsetY = this.offsetY;
const oldScale = this.zoomLevel;
const newScale = newZoomLevel;
let offsetY = calcNewScaledOffsetForZoomCentering(oldOffsetY, originalHeight, oldScale, newScale, zoomCenter.y);
let offsetX = calcNewScaledOffsetForZoomCentering(oldOffsetX, originalWidth, oldScale, newScale, zoomCenter.x);
const offsetShift = this._calcOffsetShiftSinceLastGestureState(gestureCenterPoint);
if (offsetShift) {
offsetX += offsetShift.x;
offsetY += offsetShift.y;
}
this.offsetX = offsetX;
this.offsetY = offsetY;
this.zoomLevel = newScale;
this.panAnim.setValue({ x: this.offsetX, y: this.offsetY });
this.zoomAnim.setValue(this.zoomLevel);
this.props.onZoomAfter?.(e, gestureState, this._getZoomableViewEventObject());
}
/**
* Calculates the amount the offset should shift since the last position during panning
*
* @param {Vec2D} gestureCenterPoint
*
* @private
*/
_calcOffsetShiftSinceLastGestureState(gestureCenterPoint) {
const { movementSensibility } = this.props;
let shift = null;
if (this.lastGestureCenterPosition) {
const dx = gestureCenterPoint.x - this.lastGestureCenterPosition.x;
const dy = gestureCenterPoint.y - this.lastGestureCenterPosition.y;
const shiftX = dx / this.zoomLevel / movementSensibility;
const shiftY = dy / this.zoomLevel / movementSensibility;
shift = {
x: shiftX,
y: shiftY,
};
}
this.lastGestureCenterPosition = gestureCenterPoint;
return shift;
}
/**
* Handles movement by tap and move
*
* @param gestureState
*
* @private
*/
_handleShifting(gestureState) {
// Skips shifting if panEnabled is false or disablePanOnInitialZoom is true and we're on the initial zoom level
if (!this.props.panEnabled ||
(this.props.disablePanOnInitialZoom &&
this.zoomLevel === this.props.initialZoom)) {
return;
}
const shift = this._calcOffsetShiftSinceLastGestureState({
x: gestureState.moveX,
y: gestureState.moveY,
});
if (!shift)
return;
const offsetX = this.offsetX + shift.x;
const offsetY = this.offsetY + shift.y;
this._setNewOffsetPosition(offsetX, offsetY);
}
/**
* Set the state to offset moved
*
* @param {number} newOffsetX
* @param {number} newOffsetY
* @returns
*/
async _setNewOffsetPosition(newOffsetX, newOffsetY) {
const { onShiftingBefore, onShiftingAfter } = this.props;
if (onShiftingBefore?.(null, null, this._getZoomableViewEventObject())) {
return;
}
this.offsetX = newOffsetX;
this.offsetY = newOffsetY;
this.panAnim.setValue({ x: this.offsetX, y: this.offsetY });
this.zoomAnim.setValue(this.zoomLevel);
onShiftingAfter?.(null, null, this._getZoomableViewEventObject());
}
/**
* Check whether the press event is double tap
* or single tap and handle the event accordingly
*
* @param e
*
* @private
*/
_resolveAndHandleTap = (e) => {
const now = Date.now();
if (this.doubleTapFirstTapReleaseTimestamp &&
now - this.doubleTapFirstTapReleaseTimestamp < this.props.doubleTapDelay) {
this._addTouch({
...this.doubleTapFirstTap,
id: now.toString(),
isSecondTap: true,
});
clearTimeout(this.singleTapTimeoutId);
delete this.doubleTapFirstTapReleaseTimestamp;
delete this.singleTapTimeoutId;
delete this.doubleTapFirstTap;
this._handleDoubleTap(e);
}
else {
this.doubleTapFirstTapReleaseTimestamp = now;
this.doubleTapFirstTap = {
id: now.toString(),
x: e.nativeEvent.pageX - this.state.originalPageX,
y: e.nativeEvent.pageY - this.state.originalPageY,
};
this._addTouch(this.doubleTapFirstTap);
// persist event so e.nativeEvent is preserved after a timeout delay
e.persist();
this.singleTapTimeoutId = setTimeout(() => {
delete this.doubleTapFirstTapReleaseTimestamp;
delete this.singleTapTimeoutId;
this.props.onSingleTap?.(e, this._getZoomableViewEventObject());
}, this.props.doubleTapDelay);
}
};
_addTouch(touch) {
this.touches.push(touch);
this.setState({ touches: [...this.touches] });
}
_removeTouch(touch) {
this.touches.splice(this.touches.indexOf(touch), 1);
this.setState({ touches: [...this.touches] });
}
/**
* Handles the double tap event
*
* @param e
*
* @private
*/
_handleDoubleTap(e) {
const { onDoubleTapBefore, onDoubleTapAfter, doubleTapZoomToCenter } = this.props;
onDoubleTapBefore?.(e, this._getZoomableViewEventObject());
const nextZoomStep = this._getNextZoomStep();
const { originalPageX, originalPageY } = this.state;
// define new zoom position coordinates
const zoomPositionCoordinates = {
x: e.nativeEvent.pageX - originalPageX,
y: e.nativeEvent.pageY - originalPageY,
};
// if doubleTapZoomToCenter enabled -> always zoom to center instead
if (doubleTapZoomToCenter) {
zoomPositionCoordinates.x = 0;
zoomPositionCoordinates.y = 0;
}
this._zoomToLocation(zoomPositionCoordinates.x, zoomPositionCoordinates.y, nextZoomStep).then(() => {
onDoubleTapAfter?.(e, 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
*
* @returns {*}
*/
_getNextZoomStep() {
const { zoomStep, maxZoom, initialZoom } = this.props;
const { zoomLevel } = this;
if (zoomLevel.toFixed(2) === maxZoom.toFixed(2)) {
return initialZoom;
}
const nextZoomStep = zoomLevel * (1 + zoomStep);
if (nextZoomStep > maxZoom) {
return maxZoom;
}
return nextZoomStep;
}
/**
* Zooms to a specific location in our view
*
* @param x
* @param y
* @param newZoomLevel
*
* @private
*/
async _zoomToLocation(x, y, newZoomLevel) {
if (!this.props.zoomEnabled)
return;
this.props.onZoomBefore?.(null, null, this._getZoomableViewEventObject());
// == Perform Zoom Animation ==
// Calculates panAnim values based on changes in zoomAnim.
let prevScale = this.zoomLevel;
// Since zoomAnim is calculated in native driver,
// it will jitter panAnim once in a while,
// because here panAnim is being calculated in js.
// However the jittering should mostly occur in simulator.
const listenerId = this.zoomAnim.addListener(({ value: newScale }) => {
this.panAnim.setValue({
x: calcNewScaledOffsetForZoomCentering(this.offsetX, this.state.originalWidth, prevScale, newScale, x),
y: calcNewScaledOffsetForZoomCentering(this.offsetY, this.state.originalHeight, prevScale, newScale, y),
});
prevScale = newScale;
});
getZoomToAnimation(this.zoomAnim, newZoomLevel).start(() => {
this.zoomAnim.removeListener(listenerId);
});
// == Zoom Animation Ends ==
this.props.onZoomAfter?.(null, null, this._getZoomableViewEventObject());
}
/**
* Zooms to a specificied zoom level.
* Returns a promise if everything was updated and a boolean, whether it could be updated or if it exceeded the min/max zoom limits.
*
* @param {number} newZoomLevel
*
* @return {Promise<bool>}
*/
async zoomTo(newZoomLevel) {
if (
// if we would go out of our min/max limits -> abort
newZoomLevel > this.props.maxZoom ||
newZoomLevel < this.props.minZoom)
return false;
await this._zoomToLocation(0, 0, newZoomLevel);
return true;
}
/**
* Zooms in or out by a specified change level
* Use a positive number for `zoomLevelChange` to zoom in
* Use a negative number for `zoomLevelChange` to zoom out
*
* Returns a promise if everything was updated and a boolean, whether it could be updated or if it exceeded the min/max zoom limits.
*
* @param {number | null} zoomLevelChange
*
* @return {Promise<bool>}
*/
zoomBy(zoomLevelChange = null) {
// if no zoom level Change given -> just use zoom step
if (!zoomLevelChange) {
zoomLevelChange = this.props.zoomStep;
}
return this.zoomTo(this.zoomLevel + zoomLevelChange);
}
/**
* Moves the zoomed view to a specified position
* Returns a promise when finished
*
* @param {number} newOffsetX the new position we want to move it to (x-axis)
* @param {number} newOffsetY the new position we want to move it to (y-axis)
*
* @return {Promise<bool>}
*/
moveTo(newOffsetX, newOffsetY) {
const { originalWidth, originalHeight } = this.state;
const offsetX = (newOffsetX - originalWidth / 2) / this.zoomLevel;
const offsetY = (newOffsetY - originalHeight / 2) / this.zoomLevel;
return this._setNewOffsetPosition(-offsetX, -offsetY);
}
/**
* Moves the zoomed view by a certain amount.
*
* Returns a promise when finished
*
* @param {number} offsetChangeX the amount we want to move the offset by (x-axis)
* @param {number} offsetChangeY the amount we want to move the offset by (y-axis)
*
* @return {Promise<bool>}
*/
moveBy(offsetChangeX, offsetChangeY) {
const offsetX = (this.offsetX * this.zoomLevel - offsetChangeX) / this.zoomLevel;
const offsetY = (this.offsetY * this.zoomLevel - offsetChangeY) / this.zoomLevel;
return this._setNewOffsetPosition(offsetX, offsetY);
}
render() {
return (<View style={styles.container} {...this.gestureHandlers.panHandlers} ref={this.zoomSubjectWrapperRef} onLayout={this.grabZoomSubjectOriginalMeasurements}>
{<Animated.View style={[
styles.zoomSubject,
this.props.style,
{
transform: [
{ scale: this.zoomAnim },
...this.panAnim.getTranslateTransform(),
],
},
]}>
{this.props.children}
</Animated.View>}
</View>);
}
}
const styles = StyleSheet.create({
zoomSubject: {
flex: 1,
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
overflow: 'hidden',
},
});
export default ReactNativeZoomableView;
//# sourceMappingURL=ReactNativeZoomableView.js.map