Skip to content

Instantly share code, notes, and snippets.

@jtremback
Created July 31, 2015 00:08
Show Gist options
  • Save jtremback/d208224dbceea8f905c2 to your computer and use it in GitHub Desktop.
Save jtremback/d208224dbceea8f905c2 to your computer and use it in GitHub Desktop.
function calculateDistance (touchA, touchB) {
const diffX = touchA.pageX - touchB.pageX;
const diffY = touchA.pageY - touchB.pageY;
return Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2));
}
function calculateMiddlePoint ({x: aX, y: aY}, {x: bX, y: bY}) {
const x = (aX + bX) / 2;
const y = (aY + bY) / 2;
return {x, y};
}
function zoomCompensate (oldScale, newScale, pinchOrigin, scaleOrigin) {
const scaleChange = newScale - oldScale
return {
x: -(scaleChange * (pinchOrigin.x - scaleOrigin.x)),
y: -(scaleChange * (pinchOrigin.y - scaleOrigin.y)),
}
}
const React = require('react-native')
const { Component, View } = React
const makePoint = (touch, scope) => ({
x: touch[`${scope}X`],
y: touch[`${scope}Y`],
});
const initialOrigin = { x: 0, y: 0 };
const initialState = { scale: 1, translate: { x: 0, y: 0 }}
module.exports = ({ min, max }) => BaseComponent => {
return class extends Component {
constructor(props, context) {
super(props, context);
this._lastScale = 1;
this._globalOrigin = initialOrigin;
this._localOrigin = initialOrigin;
this.state = initialState;
}
componentWillReceiveProps(nextProps) {
if (nextProps.resetPinchZoom) {
this._lastScale = 1;
this._globalOrigin = initialOrigin;
this._localOrigin = initialOrigin;
this.setState(initialState);
}
}
onStartShouldSetResponder = ({ nativeEvent: { touches } }) => {
if (touches.length === 2) {
const [touchA, touchB] = touches;
const aGlobal = makePoint(touchA, 'page');
const aLocal = makePoint(touchA, 'location');
const bGlobal = makePoint(touchB, 'page');
const bLocal = makePoint(touchB, 'location');
this._globalOrigin = calculateMiddlePoint(aGlobal, bGlobal);
this._localOrigin = calculateMiddlePoint(aLocal, bLocal);
this._initialDistance = calculateDistance(touchA, touchB);
return true
}
}
onMoveShouldSetResponder(evt) {
return evt.nativeEvent.touches.length === 2;
}
onResponderGrant = ({ nativeEvent: { touches } }) => {
const { onPinchZoomBegin } = this.props;
onPinchZoomBegin && onPinchZoomBegin(this._localOrigin, this._globalOrigin);
}
onResponderMove = ({ nativeEvent: { touches } }) => {
if (touches.length === 2) {
const { onPinchZoom } = this.props;
const [touchA, touchB] = touches;
const { _lastScale, _initialDistance } = this;
const currentDistance = calculateDistance(touchA, touchB);
let scale = _lastScale * currentDistance / _initialDistance;
scale = scale > max ?
max :
scale < min ?
min :
scale;
let state = {
scale
}
if (this.state.dimensions) {
const scaleOrigin = {
x: this.state.dimensions.x / 2,
y: this.state.dimensions.y / 2
}
state.translate = zoomCompensate(_lastScale, scale, this._localOrigin, scaleOrigin)
}
this.setState(state);
onPinchZoom && onPinchZoom(scale, this._localOrigin, this._globalOrigin, scale > _lastScale);
}
}
onResponderTerminationRequest() {
return true;
}
handleTerminationAndRelease = () => {
const { onPinchZoomEnd } = this.props;
const lastScale = this._lastScale;
const nextLastFactor = this._lastScale = this.state.scale;
onPinchZoomEnd && onPinchZoomEnd(nextLastFactor, nextLastFactor > lastScale);
}
onLayout = ({ nativeEvent: { layout: { width, height } } }) => {
this.setState({
dimensions: {
x: width,
y: height,
}
})
}
render() {
const {
onPinchZoomBegin,
onPinchZoom,
onPinchZoomEnd,
pinchZoomDecoratorStyle,
...props
} = this.props;
const { scale, translate } = this.state
let transform = [
{ scale }
]
if (translate) {
transform.push({ translateX: translate.x })
transform.push({ translateY: translate.y })
}
const style = {
...pinchZoomDecoratorStyle,
alignSelf: 'flex-start',
transform: transform,
};
return (
<View
onStartShouldSetResponder={this.onStartShouldSetResponder}
onMoveShouldSetResponder={this.onMoveShouldSetResponder}
onResponderGrant={this.onResponderGrant}
onResponderMove={this.onResponderMove}
onResponderTerminationRequest={this.onResponderTerminationRequest}
onResponderRelease={this.handleTerminationAndRelease}
onResponderTerminate={this.handleTerminationAndRelease}
style={style}
onLayout={this.onLayout}
>
<BaseComponent
ref="decorated"
{...this.props}
{...this.state}
lastScale={this._lastScale}
globalScaleOrigin={this._globalOrigin}
localScaleOrigin={this._localOrigin}
/>
</View>
);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment