├── LICENSE
├── SimpleViewEditor.js
├── ViewEditor.js
├── index.js
├── package.json
└── utilities.js
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Double Qliq, LLC
--------------------------------------------------------------------------------
/SimpleViewEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import {
3 | Dimensions,
4 | PanResponder,
5 | View,
6 | Animated,
7 | Easing,
8 | StyleSheet,
9 | } from 'react-native';
10 | import { distance, angle, center } from './utilities';
11 | const { width, height } = Dimensions.get('window');
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | overflow: 'hidden',
16 | },
17 | });
18 |
19 | export class ViewEditor extends Component {
20 | static propTypes = {
21 | style: View.propTypes.style,
22 | imageHeight: PropTypes.number.isRequired,
23 | imageWidth: PropTypes.number.isRequired,
24 | imageContainerHeight: PropTypes.number,
25 | imageContainerWidth: PropTypes.number,
26 | imageMask: PropTypes.any,
27 | maskHeight: PropTypes.number,
28 | maskWidth: PropTypes.number,
29 | maskPadding: PropTypes.number,
30 | initialOffsetX: PropTypes.number,
31 | initialOffsetY: PropTypes.number,
32 | maxZoomScale: PropTypes.number,
33 | children: PropTypes.any,
34 | rotate: PropTypes.bool,
35 | panning: PropTypes.bool,
36 | center: PropTypes.bool.isRequired,
37 | isLandscape: PropTypes.bool,
38 | isLong: PropTypes.bool,
39 | isWide: PropTypes.bool,
40 | // used for multi-images
41 | bigContainerWidth: PropTypes.number,
42 | // callbacks
43 | onZoomCallback: PropTypes.func,
44 | onSwipeDownCallback: PropTypes.func,
45 | };
46 |
47 | static defaultProps = {
48 | maskWidth: width,
49 | maskHeight: height,
50 | maskPadding: 0,
51 | imageContainerWidth: width,
52 | imageContainerHeight: height,
53 | initialOffsetX: 0,
54 | initialOffsetY: 0,
55 | maxZoomScale: 1,
56 | center: true,
57 | rotate: false,
58 | panning: true,
59 | };
60 |
61 | constructor(props, context) {
62 | super(props, context);
63 | const imageDim = (props.isLandscape || props.isLong) && !props.isWide ? props.imageHeight : props.imageWidth;
64 | const containerDim = props.isLong || props.isWide ? props.imageContainerHeight : props.imageContainerWidth;
65 | this.state = {
66 | scale: new Animated.Value(containerDim / imageDim),
67 | pan: new Animated.ValueXY(),
68 | angle: new Animated.Value('0deg'),
69 | animating: false,
70 | render: false,
71 | };
72 | this._panResponder = {};
73 | // panning variables
74 | this.panListener = null;
75 | this.currentPanValue = { x: 0, y: 0 };
76 | this._pan = { x: 0, y: 0 };
77 | // scaling variables
78 | this.scaleListener = null;
79 | this.currentScaleValue = 1;
80 | this._scale = containerDim / imageDim;
81 | // angle variables
82 | this.angleListener = null;
83 | this.currentAngleValue = 0;
84 | this._angle = 0;
85 | // used for multiTouch
86 | this._previousDistance = 0;
87 | this._previousAngle = 0;
88 | this._previousCenter = 0;
89 | this._multiTouch = false;
90 | // methods
91 | this._handlePanResponderMove = this._handlePanResponderMove.bind(this);
92 | this._handlePanResponderEnd = this._handlePanResponderEnd.bind(this);
93 | this._updatePosition = this._updatePosition.bind(this);
94 | this._updateSize = this._updateSize.bind(this);
95 | this._checkAdjustment = this._checkAdjustment.bind(this);
96 | this._updatePanState = this._updatePanState.bind(this);
97 | // callbacks
98 | this._onZoomCallbackSuccess = false;
99 | this._initialAdjustmentPerformed = false;
100 | }
101 |
102 | componentWillMount() {
103 | this._panResponder = PanResponder.create({
104 | onStartShouldSetPanResponder: () => !this.state.animating && this.props.panning,
105 | onMoveShouldSetPanResponder: () => !this.state.animating && this.props.panning,
106 | onPanResponderMove: this._handlePanResponderMove,
107 | onPanResponderRelease: this._handlePanResponderEnd,
108 | onPanResponderTerminate: this._handlePanResponderEnd,
109 | });
110 | }
111 |
112 | componentDidMount() {
113 | this.panListener = this.state.pan.addListener(value => this.currentPanValue = value);
114 | this.scaleListener = this.state.scale.addListener(value => this.currentScaleValue = value);
115 | this.angleListener = this.state.angle.addListener(value => this.currentAngleValue = value);
116 | this._checkAdjustment();
117 | this.state.pan.setOffset({ x: this.props.initialOffsetX, y: this.props.initialOffsetY });
118 | }
119 |
120 | componentDidUpdate(prevProps) {
121 | const {
122 | imageHeight,
123 | imageWidth,
124 | imageContainerWidth,
125 | imageContainerHeight,
126 | } = this.props;
127 | const {
128 | imageHeight: prevImageHeight,
129 | imageWidth: prevImageWidth,
130 | imageContainerWidth: prevImageContainerWidth,
131 | imageContainerHeight: prevImageContainerHeight,
132 | } = prevProps;
133 | if (
134 | imageHeight !== prevImageHeight ||
135 | imageWidth !== prevImageWidth ||
136 | imageContainerWidth !== prevImageContainerWidth ||
137 | imageContainerHeight !== prevImageContainerHeight
138 | ) {
139 | this._checkAdjustment();
140 | }
141 | }
142 |
143 | componentWillUnmount() {
144 | this.state.pan.removeListener(this.panListener);
145 | this.state.scale.removeListener(this.scaleListener);
146 | this.state.angle.removeListener(this.angleListener);
147 | }
148 |
149 | _updatePosition(x, y) {
150 | this.setState({ animating: true }, () => {
151 | Animated.timing(
152 | this.state.pan, {
153 | toValue: { x, y },
154 | easing: Easing.elastic(1),
155 | duration: 250
156 | }
157 | ).start(() => this._updatePanState())
158 | });
159 | }
160 |
161 | _updateSize(scale) {
162 | this.setState({ animating: true }, () => {
163 | Animated.timing(
164 | this.state.scale, {
165 | toValue: scale,
166 | easing: Easing.elastic(1),
167 | duration: 250
168 | }
169 | ).start(() => {
170 | this.setState({ animating: false });
171 | this._scale = this.currentScaleValue.value;
172 | });
173 | });
174 | }
175 |
176 | _updatePanState(x = this.currentPanValue.x, y = this.currentPanValue.y) {
177 | this.state.pan.setOffset({ x, y });
178 | this.state.pan.setValue({ x: 0, y: 0 });
179 | this.setState({ animating: false, render: true });
180 | }
181 |
182 | _handlePanResponderMove(e, gestureState) {
183 | const { imageContainerWidth, imageWidth, imageHeight } = this.props;
184 | if (gestureState.numberActiveTouches === 1 && !this._multiTouch) {
185 | return Animated.event([
186 | null, { dx: this.state.pan.x, dy: this.state.pan.y }
187 | ])(e, gestureState);
188 | } else if (gestureState.numberActiveTouches !== 1) {
189 | if (!this._onZoomCallbackSuccess && this.props.onZoomCallback) {
190 | this._onZoomCallbackSuccess = true;
191 | this.props.onZoomCallback(true);
192 | }
193 | this._multiTouch = true;
194 | // set the intial values
195 | this._previousDistance = this._previousDistance === 0 ?
196 | distance(e.nativeEvent.touches) : this._previousDistance;
197 | this._previousAngle = this._previousAngle === 0 ?
198 | angle(e.nativeEvent.touches) : this._previousAngle;
199 | this._previousCenter = this._previousCenter === 0 ?
200 | center(e.nativeEvent.touches) : this._previousCenter;
201 | // angle calculations
202 | const angleChange = angle(e.nativeEvent.touches) - this._previousAngle;
203 | this.state.angle.setValue(
204 | `${parseFloat(this._angle) + angleChange}deg`
205 | );
206 | // zoom calculations
207 | const currentDistance = distance(e.nativeEvent.touches);
208 | const newScale = ((currentDistance - this._previousDistance + imageContainerWidth) / imageContainerWidth) * this._scale;
209 | this.state.scale.setValue(newScale);
210 | // zoom to the center of the touches
211 | // const currentCenter = center(e.nativeEvent.touches);
212 | // const newWidth = newScale * imageWidth;
213 | // const newHeight = newScale * imageHeight;
214 | // const currentX = this._pan.x > 0 || newWidth < imageWidth ?
215 | // 0 : this._pan.x;
216 | // const currentY = this._pan.y > 0 || newHeight < imageHeight ?
217 | // 0 : this._pan.y;
218 | // console.log('pan', this._pan);
219 | // const x = currentCenter.x - this._previousCenter.x + currentX;
220 | // const y = currentCenter.y - this._previousCenter.y + currentY;
221 | // this.state.pan.setOffset({ x, y });
222 | // return Animated.event([
223 | // null, { dx: this.state.pan.x, dy: this.state.pan.y }
224 | // ])(e, gestureState);
225 | }
226 | }
227 |
228 | _handlePanResponderEnd(e) {
229 | const { imageWidth, imageHeight, isLandscape, isLong, isWide, maskWidth, maskHeight, maxZoomScale } = this.props;
230 | const imageDim = (isLandscape || isLong) && !isWide ? imageHeight : imageWidth;
231 | const maskDim = isLong || isWide ? maskHeight : maskWidth;
232 | this._pan = this.currentPanValue;
233 | this._updatePanState();
234 | if (this._multiTouch) {
235 | this._scale = this.currentScaleValue.value;
236 | this._angle = this.currentAngleValue.value;
237 | this._multiTouch = false;
238 | this._previousDistance = 0;
239 | this._previousAngle = 0;
240 | this._previousCenter = 0;
241 | if (imageDim * this._scale < maskDim) {
242 | if (this.props.onZoomCallback) {
243 | this.props.onZoomCallback(false);
244 | }
245 | this._updateSize(maskDim / imageDim);
246 | } else if (this._scale > maxZoomScale) {
247 | if (this.props.onZoomCallback) {
248 | this.props.onZoomCallback(false);
249 | }
250 | this._updateSize(maxZoomScale);
251 | } else {
252 | if (this.props.onZoomCallback) {
253 | this.props.onZoomCallback(true)
254 | }
255 | }
256 | }
257 | this._checkAdjustment(e);
258 | }
259 |
260 | _checkAdjustment(e) {
261 | const { imageContainerHeight, imageContainerWidth, maskPadding, imageHeight: tempHeight, imageWidth: tempWidth, center, isLandscape } = this.props;
262 | const imageHeight = isLandscape ? tempWidth : tempHeight;
263 | const imageWidth = isLandscape ? tempHeight : tempWidth;
264 | const widthDiff = this._scale * imageWidth - imageContainerWidth;
265 | const heightDiff = this._scale * imageHeight - imageContainerHeight;
266 | const maskPaddingDiffX = widthDiff < 0 && center ? -widthDiff / 2 : maskPadding;
267 | const maskPaddingDiffY = heightDiff < 0 && center ? -heightDiff / 2 : maskPadding;
268 | const positionUpdate = { x: 0, y: 0 };
269 | const imageLeft = this.currentPanValue.x + widthDiff + maskPaddingDiffX;
270 | const imageAbove = this.currentPanValue.y + heightDiff + maskPaddingDiffY;
271 | const additionalWidth = (tempWidth - this._scale * imageWidth) / 2;
272 | const additionalHeight = (tempHeight - this._scale * imageHeight) / 2;
273 | if (this.currentPanValue.x > maskPaddingDiffX - additionalWidth) {
274 | positionUpdate.x = -this.currentPanValue.x - additionalWidth + maskPaddingDiffX;
275 | }
276 | if (this.currentPanValue.y > maskPaddingDiffY - additionalHeight) {
277 |
278 | positionUpdate.y = -this.currentPanValue.y - additionalHeight + maskPaddingDiffY;
279 | if (!this._initialAdjustmentPerformed) {
280 | this._initialAdjustmentPerformed = true;
281 | } else if (this.props.onSwipeDownCallback) {
282 | this.props.onSwipeDownCallback(positionUpdate, e);
283 | }
284 | }
285 | if (imageAbove < -additionalHeight) {
286 | positionUpdate.y = -imageAbove - additionalHeight;
287 | }
288 | if (imageLeft < -additionalWidth) {
289 | positionUpdate.x = -imageLeft - additionalWidth;
290 | }
291 | this._updatePosition(positionUpdate.x, positionUpdate.y);
292 | }
293 |
294 | render() {
295 | const { pan, scale, render } = this.state;
296 | const {
297 | imageWidth,
298 | imageHeight,
299 | imageContainerWidth,
300 | imageContainerHeight,
301 | imageMask,
302 | children,
303 | rotate,
304 | style,
305 | panning,
306 | } = this.props;
307 | const layout = pan.getLayout();
308 | const animatedStyle = {
309 | height: imageHeight,
310 | width: imageWidth,
311 | transform: [
312 | { translateX: layout.left },
313 | { translateY: layout.top },
314 | { scale }
315 | ]
316 | };
317 | if (rotate) {
318 | animatedStyle.transform.push({ rotate: this.state.angle });
319 | }
320 | return (
321 |
329 |
332 | {render && children}
333 |
334 | {imageMask}
335 |
336 | );
337 | }
338 | }
339 |
--------------------------------------------------------------------------------
/ViewEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes, cloneElement } from 'react';
2 | import {
3 | Dimensions,
4 | PanResponder,
5 | View,
6 | Animated,
7 | Easing,
8 | StyleSheet,
9 | ImageEditor,
10 | Image,
11 | } from 'react-native';
12 | import RNFS from 'react-native-fs';
13 | import { AnimatedSurface } from 'gl-react-native';
14 | import { takeSnapshot } from 'react-native-view-shot';
15 | import { distance, angle, center } from './utilities';
16 | const { width, height } = Dimensions.get('window');
17 |
18 | const styles = StyleSheet.create({
19 | container: {
20 | overflow: 'hidden',
21 | },
22 | });
23 |
24 | export class ViewEditor extends Component {
25 | static propTypes = {
26 | style: View.propTypes.style,
27 | imageHeight: PropTypes.number.isRequired,
28 | imageWidth: PropTypes.number.isRequired,
29 | imageContainerHeight: PropTypes.number,
30 | imageContainerWidth: PropTypes.number,
31 | imageMask: PropTypes.any,
32 | maskHeight: PropTypes.number,
33 | maskWidth: PropTypes.number,
34 | maskPadding: PropTypes.number,
35 | children: PropTypes.any,
36 | rotate: PropTypes.bool,
37 | panning: PropTypes.bool,
38 | center: PropTypes.bool.isRequired,
39 | croppingRequired: PropTypes.bool.isRequired,
40 | imageMaskShown: PropTypes.bool.isRequired,
41 | showAtTop: PropTypes.bool,
42 | useCustomContent: PropTypes.bool,
43 | // used for multi-images
44 | bigContainerWidth: PropTypes.number,
45 | bigContainerHeight: PropTypes.number,
46 | requiresMinScale: PropTypes.bool,
47 | initialScale: PropTypes.number,
48 | initialPan: PropTypes.object,
49 | initialRotate: PropTypes.string,
50 | onPressCallback: PropTypes.func,
51 | onLongPressCallback: PropTypes.func,
52 | onLongPressReleaseCallback: PropTypes.func,
53 | onMoveCallback: PropTypes.func,
54 | onEndCallback: PropTypes.func,
55 | onLoad: PropTypes.func,
56 | }
57 |
58 | static defaultProps = {
59 | maskWidth: width,
60 | maskHeight: height,
61 | maskPadding: 0,
62 | imageMaskShown: false,
63 | imageContainerWidth: width,
64 | imageContainerHeight: height,
65 | center: true,
66 | rotate: false,
67 | panning: true,
68 | croppingRequired: false,
69 | requiresMinScale: false,
70 | initialScale: null,
71 | initialPan: null,
72 | showAtTop: false,
73 | useCustomContent: false,
74 | }
75 |
76 | constructor(props, context) {
77 | super(props, context);
78 | const relativeWidth = props.bigContainerWidth || props.imageContainerWidth;
79 | const relativeHeight = props.bigContainerHeight || props.imageContainerHeight;
80 | if (props.requiresMinScale) {
81 | this._minScale = relativeHeight / props.imageHeight < relativeWidth / props.imageWidth ?
82 | relativeWidth / props.imageWidth :
83 | relativeHeight / props.imageHeight;
84 | } else {
85 | this._minScale = relativeHeight / props.imageHeight > relativeWidth / props.imageWidth ?
86 | relativeWidth / props.imageWidth :
87 | relativeHeight / props.imageHeight;
88 | }
89 | this._scale = this._minScale;
90 | this.state = {
91 | scale: new Animated.Value(this._scale),
92 | pan: new Animated.ValueXY(),
93 | angle: new Animated.Value('0deg'),
94 | animating: false,
95 | render: false,
96 | };
97 | // ref of the surface to capture
98 | this.surface = null;
99 | // ref of view to capture
100 | this.viewRef = null;
101 | // panning variables
102 | this.panListener = null;
103 | this.currentPanValue = { x: 0, y: 0 };
104 | this._pan = { x: 0, y: 0 };
105 | // scaling variables
106 | this.scaleListener = null;
107 | this.currentScaleValue = 1;
108 | // angle variables
109 | this.angleListener = null;
110 | this.currentAngleValue = 0;
111 | this._angle = 0;
112 | // used for multiTouch
113 | this._previousDistance = 0;
114 | this._previousAngle = 0;
115 | this._previousCenter = 0;
116 | this._multiTouch = false;
117 | // used for callbacks
118 | this._onPress = null;
119 | this._onLongPress = null;
120 | this._totalMovedX = 0;
121 | this._totalMovedY = 0;
122 | this._onLongPressSuccess = false;
123 | this._onMoveCallbackSucess = false;
124 |
125 | // methods
126 | this._handlePanResponderGrant = this._handlePanResponderGrant.bind(this);
127 | this._handlePanResponderMove = this._handlePanResponderMove.bind(this);
128 | this._handlePanResponderEnd = this._handlePanResponderEnd.bind(this);
129 | this._updatePosition = this._updatePosition.bind(this);
130 | this._updateSize = this._updateSize.bind(this);
131 | this._checkAdjustment = this._checkAdjustment.bind(this);
132 | this._updatePanState = this._updatePanState.bind(this);
133 | this.getScaledDims = this.getScaledDims.bind(this);
134 | this.captureFrameAndCrop = this.captureFrameAndCrop.bind(this);
135 | this.getCurrentState = this.getCurrentState.bind(this);
136 | // the PanResponder
137 | this._panResponder = PanResponder.create({
138 | onStartShouldSetPanResponder: (e, g) => !this.state.animating && this.props.panning,
139 | onMoveShouldSetPanResponder: (e, g) => !this.state.animating && this.props.panning,
140 | onPanResponderGrant: this._handlePanResponderGrant,
141 | onPanResponderMove: this._handlePanResponderMove,
142 | onPanResponderRelease: this._handlePanResponderEnd,
143 | onPanResponderTerminate: this._handlePanResponderEnd,
144 | });
145 | }
146 |
147 | componentDidMount() {
148 | const { initialPan, initialScale, croppingRequired, onLoad } = this.props;
149 | this.panListener = this.state.pan.addListener(value => this.currentPanValue = value);
150 | this.scaleListener = this.state.scale.addListener(value => this.currentScaleValue = value);
151 | this.angleListener = this.state.angle.addListener(value => this.currentAngleValue = value);
152 | if (initialScale) {
153 | this._updateSize(initialScale, initialPan);
154 | } else {
155 | this._checkAdjustment();
156 | }
157 | if (!croppingRequired && typeof onLoad === 'function') {
158 | onLoad();
159 | }
160 | }
161 |
162 | componentDidUpdate(prevProps) {
163 | const {
164 | imageHeight,
165 | imageWidth,
166 | imageContainerWidth,
167 | imageContainerHeight,
168 | requiresMinScale,
169 | initialRotate
170 | } = this.props;
171 | const {
172 | imageHeight: prevImageHeight,
173 | imageWidth: prevImageWidth,
174 | imageContainerWidth: prevImageContainerWidth,
175 | imageContainerHeight: prevImageContainerHeight,
176 | requiresMinScale: prevRequiresMinScale,
177 | initialRotate: prevInitialRotate,
178 | } = prevProps;
179 | if (
180 | imageHeight !== prevImageHeight ||
181 | imageWidth !== prevImageWidth ||
182 | imageContainerWidth !== prevImageContainerWidth ||
183 | imageContainerHeight !== prevImageContainerHeight ||
184 | initialRotate !== prevInitialRotate
185 | ) {
186 | const relativeWidth = this.props.bigContainerWidth || this.props.imageContainerWidth;
187 | const relativeHeight = this.props.bigContainerHeight || this.props.imageContainerHeight;
188 | if (requiresMinScale) {
189 | this._minScale = relativeHeight / this.props.imageHeight < relativeWidth / this.props.imageWidth ?
190 | relativeWidth / this.props.imageWidth :
191 | relativeHeight / this.props.imageHeight;
192 | this._updateSize(this._minScale, false);
193 | } else {
194 | this._minScale = relativeHeight / this.props.imageHeight > relativeWidth / this.props.imageWidth ?
195 | relativeWidth / this.props.imageWidth :
196 | relativeHeight / this.props.imageHeight;
197 | this._updateSize(this._minScale, false);
198 | }
199 | this._checkAdjustment(this._minScale);
200 | }
201 | }
202 |
203 | componentWillUnmount() {
204 | this.state.pan.removeListener(this.panListener);
205 | this.state.scale.removeListener(this.scaleListener);
206 | this.state.angle.removeListener(this.angleListener);
207 | if (this._onPress) {
208 | clearTimeout(this._onPress);
209 | }
210 | if (this._onLongPress) {
211 | clearTimeout(this._onLongPress);
212 | }
213 | }
214 |
215 | _updatePosition(x, y) {
216 | this.setState({ animating: true }, () => {
217 | Animated.timing(
218 | this.state.pan, {
219 | toValue: { x, y },
220 | easing: Easing.elastic(1),
221 | duration: 250
222 | }
223 | ).start(() => this._updatePanState());
224 | });
225 | }
226 |
227 | _updateSize(scale, initialPan = false) {
228 | this.setState({ animating: true }, () => {
229 | Animated.timing(
230 | this.state.scale, {
231 | toValue: scale,
232 | easing: Easing.elastic(1),
233 | duration: 250
234 | }
235 | ).start(() => {
236 | this.setState({ animating: false });
237 | this._scale = this.currentScaleValue.value;
238 | if (initialPan) {
239 | const { showAtTop, imageHeight } = this.props;
240 | const pan = Object.assign({}, initialPan);
241 | if (showAtTop) {
242 | const additionalHeight = (imageHeight - this._scale * imageHeight) / 2;
243 | pan.y = -additionalHeight;
244 | }
245 | this._updatePosition(pan.x, pan.y);
246 | }
247 | });
248 | });
249 | }
250 |
251 | _updatePanState(x = this.currentPanValue.x, y = this.currentPanValue.y) {
252 | this.state.pan.setOffset({ x, y });
253 | this.state.pan.setValue({ x: 0, y: 0 });
254 | this.setState({ animating: false, render: true });
255 | }
256 |
257 | _handlePanResponderGrant(e, gestureState) {
258 | const { onPressCallback, onLongPressCallback } = this.props;
259 | if (onPressCallback) {
260 | this._onPress = setTimeout(() => {
261 | clearTimeout(this._onPress);
262 | this._onPress = null;
263 | }, 200);
264 | }
265 | if (onLongPressCallback) {
266 | this._onLongPress = setTimeout(() => {
267 | clearTimeout(this._onLongPress);
268 | this._onLongPress = null;
269 | }, 500);
270 | }
271 | }
272 |
273 | _handlePanResponderMove(e, gestureState) {
274 | const { imageContainerWidth, imageWidth, imageHeight, onLongPressCallback, onMoveCallback } = this.props;
275 | if (gestureState.numberActiveTouches === 1 && !this._multiTouch) {
276 | this._totalMovedX += Math.abs(gestureState.dx);
277 | this._totalMovedY += Math.abs(gestureState.dy);
278 | if (
279 | !this._onLongPress && onLongPressCallback &&
280 | (this._totalMovedX < 50 && this._totalMovedY < 50) &&
281 | !this._onLongPressSuccess
282 | ) {
283 | this._onLongPressSuccess = true;
284 | return onLongPressCallback();
285 | } else if (onMoveCallback && !this._onMoveCallbackSucess) {
286 | this._onMoveCallbackSucess = true;
287 | onMoveCallback();
288 | }
289 | return Animated.event([
290 | null, { dx: this.state.pan.x, dy: this.state.pan.y }
291 | ])(e, gestureState);
292 | } else if (gestureState.numberActiveTouches !== 1) {
293 | if (onMoveCallback && !this._onMoveCallbackSucess) {
294 | this._onMoveCallbackSucess = true;
295 | onMoveCallback();
296 | }
297 | this._multiTouch = true;
298 | // set the intial values
299 | this._previousDistance = this._previousDistance === 0 ?
300 | distance(e.nativeEvent.touches) : this._previousDistance;
301 | this._previousAngle = this._previousAngle === 0 ?
302 | angle(e.nativeEvent.touches) : this._previousAngle;
303 | this._previousCenter = this._previousCenter === 0 ?
304 | center(e.nativeEvent.touches) : this._previousCenter;
305 | // angle calculations
306 | const angleChange = angle(e.nativeEvent.touches) - this._previousAngle;
307 | this.state.angle.setValue(
308 | `${parseFloat(this._angle) + angleChange}deg`
309 | );
310 | // zoom calculations
311 | const currentDistance = distance(e.nativeEvent.touches);
312 | const newScale = ((currentDistance - this._previousDistance + imageContainerWidth) / imageContainerWidth) * this._scale;
313 | this.state.scale.setValue(newScale);
314 | // zoom to the center of the touches
315 | // const currentCenter = center(e.nativeEvent.touches);
316 | // const newWidth = newScale * imageWidth;
317 | // const newHeight = newScale * imageHeight;
318 | // const currentX = this._pan.x > 0 || newWidth < imageWidth ?
319 | // 0 : this._pan.x;
320 | // const currentY = this._pan.y > 0 || newHeight < imageHeight ?
321 | // 0 : this._pan.y;
322 | // console.log('pan', this._pan);
323 | // const x = currentCenter.x - this._previousCenter.x + currentX;
324 | // const y = currentCenter.y - this._previousCenter.y + currentY;
325 | // this.state.pan.setOffset({ x, y });
326 | // return Animated.event([
327 | // null, { dx: this.state.pan.x, dy: this.state.pan.y }
328 | // ])(e, gestureState);
329 | }
330 | }
331 |
332 | _handlePanResponderEnd() {
333 | if (this._onPress && (
334 | (this._totalMovedX < 30 && this._totalMovedY < 30)
335 | )) {
336 | clearTimeout(this._onPress);
337 | this._onPress = null;
338 | this.props.onPressCallback();
339 | }
340 | if (this._onLongPress) {
341 | clearTimeout(this._onLongPress);
342 | this.onLongPress = null;
343 | }
344 | if (this._onLongPressSuccess) {
345 | this._onLongPressSuccess = false;
346 | if (this.props.onLongPressReleaseCallback) {
347 | this.props.onLongPressReleaseCallback();
348 | }
349 | }
350 | if (this._onMoveCallbackSucess) {
351 | this._onMoveCallbackSucess = false;
352 | if (this.props.onEndCallback) {
353 | this.props.onEndCallback();
354 | }
355 | }
356 | this._onMoveCallbackSucess = false;
357 | this._totalMovedX = 0;
358 | this._totalMovedY = 0;
359 | const { imageWidth, imageHeight, imageContainerWidth, imageContainerHeight } = this.props;
360 | this._pan = this.currentPanValue;
361 | this._updatePanState();
362 | if (this._multiTouch) {
363 | this._scale = this.currentScaleValue.value;
364 | this._angle = this.currentAngleValue.value;
365 | this._multiTouch = false;
366 | this._previousDistance = 0;
367 | this._previousAngle = 0;
368 | this._previousCenter = 0;
369 | const { maskWidth, maskHeight } = this.props;
370 | if (this._minScale > this._scale) {
371 | this._updateSize(this._minScale);
372 | } else if (this._scale > 1) {
373 | this._updateSize(this._scale);
374 | }
375 | }
376 | this._checkAdjustment();
377 | }
378 |
379 | _checkAdjustment(withScale = this._scale) {
380 | const { imageContainerHeight, imageContainerWidth, maskPadding, imageHeight, imageWidth, center, initialRotate } = this.props;
381 | const widthDiff = withScale * imageWidth - imageContainerWidth;
382 | const heightDiff = withScale * imageHeight - imageContainerHeight;
383 | const maskPaddingDiffX = widthDiff < 0 && center ? -widthDiff / 2 : maskPadding;
384 | const maskPaddingDiffY = heightDiff < 0 && center ? -heightDiff / 2 : maskPadding;
385 | const positionUpdate = { x: 0, y: 0 };
386 | const imageLeft = this.currentPanValue.x + widthDiff + maskPaddingDiffX;
387 | const imageAbove = this.currentPanValue.y + heightDiff + maskPaddingDiffY;
388 | const additionalWidth = (imageWidth - withScale * imageWidth) / 2;
389 | const additionalHeight = (imageHeight - withScale * imageHeight) / 2;
390 | if (this.currentPanValue.x > maskPaddingDiffX - additionalWidth) {
391 | positionUpdate.x = -this.currentPanValue.x - additionalWidth + maskPaddingDiffX;
392 | }
393 | if (this.currentPanValue.y > maskPaddingDiffY - additionalHeight) {
394 | positionUpdate.y = -this.currentPanValue.y - additionalHeight + maskPaddingDiffY;
395 | }
396 | if (imageAbove < -additionalHeight) {
397 | positionUpdate.y = -imageAbove - additionalHeight;
398 | }
399 | if (imageLeft < -additionalWidth) {
400 | positionUpdate.x = -imageLeft - additionalWidth;
401 | }
402 | this._updatePosition(positionUpdate.x, positionUpdate.y);
403 | }
404 |
405 | getScaledDims() {
406 | return {
407 | top: this._scale * this.props.imageHeight + this.currentPanValue.y,
408 | left: this._scale * this.props.imageWidth + this.currentPanValue.x,
409 | };
410 | }
411 |
412 | getPanAndScale() {
413 | return {
414 | pan: this.currentPanValue,
415 | scale: this._scale,
416 | };
417 | }
418 |
419 | captureFrameAndCrop(captureProperties) {
420 | const properties = this.getCurrentState(captureProperties);
421 | const cropImage = (image) => new Promise(resolve =>
422 | ImageEditor.cropImage(image, properties, uri => resolve(uri), () => null)
423 | );
424 | const { croppingRequired, useCustomContent, imageWidth, imageHeight } = this.props;
425 |
426 | const getSize = (url) => new Promise((resolve, reject) =>
427 | Image.getSize(url,
428 | (imgWidth, imgHeight) => resolve({ width: imgWidth, height: imgHeight, url }),
429 | (err) => reject(err))
430 | );
431 | if (useCustomContent && !croppingRequired) {
432 | return takeSnapshot(this.viewRef, {
433 | quality: 1,
434 | result: 'file',
435 | format: 'jpg',
436 | width: undefined,
437 | height: undefined
438 | })
439 | .then(url => getSize(url))
440 | .then(image => {
441 | // because of takeSnapshot resizes image size
442 | properties.size.height *= image.height / imageHeight;
443 | properties.size.width *= image.width / imageWidth;
444 | return cropImage(image.url);
445 | })
446 | .then(uri => uri)
447 | .catch(err => console.log(err));
448 | }
449 |
450 | return this.surface.captureFrame({
451 | quality: 1,
452 | format: 'file',
453 | type: 'jpg',
454 | filePath: `${RNFS.DocumentDirectoryPath}/${new Date().getTime()}.jpg`
455 | })
456 | .then(image => cropImage(image))
457 | .then(uri => uri)
458 | .catch(error => console.log(error));
459 | }
460 |
461 | getCurrentState({ pan, scale, layout, imageLength }) {
462 | const {
463 | imageWidth,
464 | imageHeight,
465 | imageContainerWidth,
466 | imageContainerHeight,
467 | bigContainerWidth,
468 | bigContainerHeight,
469 | initialRotate,
470 | } = this.props;
471 | const containerWidth = bigContainerWidth || imageContainerWidth;
472 | const containerHeight = bigContainerHeight || imageContainerHeight;
473 | const ogScaleX = (containerWidth / imageWidth);
474 | const ogScaleY = (containerHeight / imageHeight);
475 | const scaleChangeX = (scale - ogScaleX) / scale;
476 | const scaleChangeY = (scale - ogScaleY) / scale;
477 | const roundWidth = Math.floor(scale * imageWidth < containerWidth
478 | ? imageWidth
479 | : containerWidth / scale);
480 | const roundHeight = Math.floor(scale * imageHeight < containerHeight
481 | ? imageHeight
482 | : containerHeight / scale);
483 | const ogPanX = (containerWidth - imageWidth) / 2;
484 | const ogPanY = (containerHeight - imageHeight) / 2;
485 | const xZoomOffset = imageWidth * scaleChangeX / 2 - (containerWidth - imageWidth * ogScaleX) < 0
486 | ? 0
487 | : imageWidth * scaleChangeX / 2 - (containerWidth - imageWidth * ogScaleX) / 2;
488 | const yZoomOffset = imageHeight * scaleChangeY / 2 - (containerHeight - imageHeight * ogScaleY) < 0
489 | ? 0
490 | : imageHeight * scaleChangeY / 2 - (containerHeight - imageHeight * ogScaleY) / 2;
491 | const xPanOffset = (ogPanX - pan.x) / scale;
492 | const yPanOffset = (ogPanY - pan.y) / scale;
493 |
494 | // amount image top left corner has moved from zooming
495 | const zoomOffset = {
496 | x: xZoomOffset,
497 | y: yZoomOffset,
498 | };
499 |
500 | // amount image top left corner has moved from panning
501 | const panOffset = {
502 | x: xPanOffset,
503 | y: yPanOffset
504 | };
505 |
506 | // total offset of top left corner from original state.
507 | const offset = {
508 | x: zoomOffset.x + panOffset.x,
509 | y: zoomOffset.y + panOffset.y
510 | };
511 |
512 | return {
513 | offset,
514 | size: {
515 | width: roundWidth,
516 | height: roundHeight,
517 | },
518 | };
519 | }
520 |
521 | render() {
522 | const { pan, scale, render } = this.state;
523 | const {
524 | imageWidth,
525 | imageHeight,
526 | imageMask,
527 | children,
528 | rotate,
529 | style,
530 | initialRotate,
531 | croppingRequired,
532 | imageMaskShown,
533 | onLoad,
534 | useCustomContent
535 | } = this.props;
536 |
537 | const layout = pan.getLayout();
538 | const animatedStyle = {
539 | transform: [
540 | { translateX: layout.left },
541 | { translateY: layout.top },
542 | { scale },
543 | ]
544 | };
545 |
546 | if (initialRotate) {
547 | animatedStyle.transform.push({ rotate: initialRotate });
548 | } else if (rotate) {
549 | animatedStyle.transform.push({ rotate: this.state.angle });
550 | }
551 |
552 | const wrapStyle = [
553 | style,
554 | styles.container,
555 | ];
556 |
557 | if (!render) {
558 | return null;
559 | }
560 |
561 | if (croppingRequired) {
562 | return (
563 |
564 | this.surface = ref}
566 | width={imageWidth}
567 | height={imageHeight}
568 | style={animatedStyle}
569 | pixelRatio={1}
570 | visibleContent={true}
571 | onLoad={onLoad}
572 | preload={true}
573 | {...this._panResponder.panHandlers}
574 | >
575 | {children}
576 |
577 | {imageMaskShown && imageMask}
578 |
579 | );
580 | }
581 | if (useCustomContent) {
582 | const { style: contentStyle } = children.props;
583 | return (
584 |
588 | {cloneElement(children, {
589 | style: [animatedStyle, contentStyle],
590 | ref: (ref) => this.viewRef = ref,
591 | })}
592 | {imageMaskShown && imageMask}
593 |
594 | );
595 | }
596 |
597 | return (
598 |
599 |
600 | {children}
601 |
602 | {imageMaskShown && imageMask}
603 |
604 | );
605 | }
606 | }
607 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { ViewEditor as default } from './ViewEditor';
2 | export { ViewEditor as SimpleViewEditor } from './SimpleViewEditor';
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-view-editor-2",
3 | "version": "1.0.0",
4 | "description": "A simple wrapper for being able to rotate, pan, and resize a child view or image with animations",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/doubleqliq/react-native-view-editor.git"
8 | },
9 | "main": "index.js",
10 | "scripts": {
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "keywords": [
14 | "pannable",
15 | "image"
16 | ],
17 | "author": "DoubleQliq",
18 | "license": "ISC"
19 | }
20 |
--------------------------------------------------------------------------------
/utilities.js:
--------------------------------------------------------------------------------
1 | function pow2abs(a, b) {
2 | return Math.pow(Math.abs(a - b), 2);
3 | }
4 |
5 | export function center(touches) {
6 | const a = touches[0];
7 | const b = touches[1];
8 | return {
9 | x: (a.pageX + b.pageX) / 2,
10 | y: (a.pageY + b.pageY) / 2,
11 | };
12 | }
13 |
14 | export function distance(touches) {
15 | const a = touches[0];
16 | const b = touches[1];
17 |
18 | return Math.sqrt(
19 | pow2abs(a.pageX, b.pageX) +
20 | pow2abs(a.pageY, b.pageY),
21 | 2);
22 | }
23 |
24 | function toDeg(rad) {
25 | return rad * 180 / Math.PI;
26 | }
27 |
28 | export function angle(touches) {
29 | const a = touches[0];
30 | const b = touches[1];
31 | let deg = toDeg(Math.atan2(b.pageY - a.pageY, b.pageX - a.pageX));
32 | if (deg < 0) {
33 | deg += 360;
34 | }
35 | return deg;
36 | }
37 |
--------------------------------------------------------------------------------