UNPKG

22.7 kBJavaScriptView Raw
1import React, { Component, } from 'react';
2import { View, PanResponder, Animated, Platform, } from 'react-native';
3export class PropsGaea {
4 constructor() {
5 this.gaeaName = '图片手势操作';
6 this.gaeaIcon = 'square-o';
7 this.gaeaUniqueKey = 'nt-image-zoom';
8 }
9}
10export class Props extends PropsGaea {
11 constructor() {
12 super(...arguments);
13 this.onClick = () => {
14 };
15 this.onLongPress = () => {
16 };
17 this.panToMove = true;
18 this.pinchToZoom = true;
19 this.cropWidth = 100;
20 this.cropHeight = 100;
21 this.imageWidth = 100;
22 this.imageHeight = 100;
23 this.source = '';
24 this.longPressTime = 800;
25 this.leaveStayTime = 100;
26 this.leaveDistance = 10;
27 this.maxOverflow = 100;
28 this.horizontalOuterRangeOffset = () => {
29 };
30 this.responderRelease = () => {
31 };
32 this.onDoubleClick = () => {
33 };
34 }
35}
36export class State {
37 constructor() {
38 this.centerX = 0.5;
39 this.centerY = 0.5;
40 }
41}
42/**
43 * 样式表
44 */
45const styles = {
46 container: {
47 justifyContent: 'center',
48 alignItems: 'center',
49 overflow: 'hidden',
50 // fix 0.36 bug, see: https://github.com/facebook/react-native/issues/10782
51 backgroundColor: 'transparent',
52 },
53};
54const isMobile = () => {
55 if (Platform.OS === 'web') {
56 return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
57 .test(navigator.userAgent);
58 }
59 else {
60 return true;
61 }
62};
63//
64export default class ImageViewer extends Component {
65 constructor() {
66 super(...arguments);
67 this.state = new State();
68 // 上次/当前/动画 x 位移
69 this.lastPositionX = null;
70 this.positionX = 0;
71 this.animatedPositionX = new Animated.Value(0);
72 // 上次/当前/动画 y 位移
73 this.lastPositionY = null;
74 this.positionY = 0;
75 this.animatedPositionY = new Animated.Value(0);
76 // 缩放大小
77 this.scale = 1;
78 this.animatedScale = new Animated.Value(1);
79 this.zoomLastDistance = null;
80 this.zoomCurrentDistance = 0;
81 // 滑动过程中,整体横向过界偏移量
82 this.horizontalWholeOuterCounter = 0;
83 // 滑动过程中,x y的总位移
84 this.horizontalWholeCounter = 0;
85 this.verticalWholeCounter = 0;
86 // 两手距离中心点位置
87 this.centerDiffX = 0;
88 this.centerDiffY = 0;
89 // 上一次点击的时间
90 this.lastClickTime = 0;
91 // 双击时的位置
92 this.doubleClickX = 0;
93 this.doubleClickY = 0;
94 // 是否双击缩放了
95 this.isDoubleClickScale = false;
96 }
97 componentWillMount() {
98 const setResponder = isMobile();
99 this.imagePanResponder = PanResponder.create({
100 // 要求成为响应者:
101 onStartShouldSetPanResponder: () => setResponder,
102 onStartShouldSetPanResponderCapture: (event, gestureState) => {
103 // 这个event判断其实没什么卵用, 主要是为了clear掉tslint的报错
104 return event && setResponder && gestureState.dx !== 0 && gestureState.dy !== 0;
105 },
106 onMoveShouldSetPanResponder: () => setResponder,
107 onMoveShouldSetPanResponderCapture: (event, gestureState) => {
108 // 这个event判断其实没什么卵用, 主要是为了clear掉tslint的报错
109 return event && setResponder && gestureState.dx !== 0 && gestureState.dy !== 0;
110 },
111 onPanResponderTerminationRequest: () => false,
112 onPanResponderGrant: (evt) => {
113 // 开始手势操作
114 this.lastPositionX = null;
115 this.lastPositionY = null;
116 this.zoomLastDistance = null;
117 this.horizontalWholeCounter = 0;
118 this.verticalWholeCounter = 0;
119 this.lastTouchStartTime = new Date().getTime();
120 this.isDoubleClickScale = false;
121 if (evt.nativeEvent.changedTouches.length > 1) {
122 this.centerDiffX = (evt.nativeEvent.changedTouches[0].pageX
123 + evt.nativeEvent.changedTouches[1].pageX) / 2 - this.props.cropWidth / 2;
124 this.centerDiffY = (evt.nativeEvent.changedTouches[0].pageY
125 + evt.nativeEvent.changedTouches[1].pageY) / 2 - this.props.cropHeight / 2;
126 }
127 // 计算长按
128 if (this.longPressTimeout) {
129 clearTimeout(this.longPressTimeout);
130 }
131 this.longPressTimeout = setTimeout(() => {
132 this.props.onLongPress();
133 }, this.props.longPressTime);
134 if (evt.nativeEvent.changedTouches.length <= 1) {
135 // 一个手指的情况
136 if (new Date().getTime() - this.lastClickTime < 150) {
137 // 认为触发了双击
138 this.lastClickTime = 0;
139 this.props.onDoubleClick();
140 // 取消长按
141 clearTimeout(this.longPressTimeout);
142 // 因为可能触发放大,因此记录双击时的坐标位置
143 this.doubleClickX = evt.nativeEvent.changedTouches[0].pageX;
144 this.doubleClickY = evt.nativeEvent.changedTouches[0].pageY;
145 // 缩放
146 this.isDoubleClickScale = true;
147 if (this.scale > 1 || this.scale < 1) {
148 // 回归原位
149 this.scale = 1;
150 this.positionX = 0;
151 this.positionY = 0;
152 }
153 else {
154 // 开始在位移地点缩放
155 // 记录之前缩放比例
156 // 此时 this.scale 一定为 1
157 const beforeScale = this.scale;
158 // 开始缩放
159 this.scale = 2;
160 // 缩放 diff
161 const diffScale = this.scale - beforeScale;
162 // 找到两手中心点距离页面中心的位移
163 // 移动位置
164 this.positionX = (this.props.cropWidth / 2
165 - this.doubleClickX) * diffScale / this.scale;
166 this.positionY = (this.props.cropHeight / 2
167 - this.doubleClickY) * diffScale / this.scale;
168 }
169 Animated.parallel([
170 Animated.timing(this.animatedScale, {
171 toValue: this.scale,
172 duration: 100,
173 }),
174 Animated.timing(this.animatedPositionX, {
175 toValue: this.positionX,
176 duration: 100,
177 }),
178 Animated.timing(this.animatedPositionY, {
179 toValue: this.positionY,
180 duration: 100,
181 }),
182 ]).start();
183 }
184 else {
185 this.lastClickTime = new Date().getTime();
186 }
187 }
188 },
189 onPanResponderMove: (evt, gestureState) => {
190 if (evt.nativeEvent.changedTouches.length <= 1) {
191 // x 位移
192 let diffX = gestureState.dx - this.lastPositionX;
193 if (this.lastPositionX === null) {
194 diffX = 0;
195 }
196 // y 位移
197 let diffY = gestureState.dy - this.lastPositionY;
198 if (this.lastPositionY === null) {
199 diffY = 0;
200 }
201 // 保留这一次位移作为下次的上一次位移
202 this.lastPositionX = gestureState.dx;
203 this.lastPositionY = gestureState.dy;
204 this.horizontalWholeCounter += diffX;
205 this.verticalWholeCounter += diffY;
206 if (Math.abs(this.horizontalWholeCounter) > 5 ||
207 Math.abs(this.verticalWholeCounter) > 5) {
208 // 如果位移超出手指范围,取消长按监听
209 clearTimeout(this.longPressTimeout);
210 }
211 if (this.props.panToMove) {
212 // diffX > 0 表示手往右滑,图往左移动,反之同理
213 // horizontalWholeOuterCounter > 0 表示溢出在左侧,反之在右侧,绝对值越大溢出越多
214 if (this.props.imageWidth * this.scale > this.props.cropWidth) {
215 // 没有溢出偏移量或者这次位移完全收回了偏移量才能拖拽
216 if (this.horizontalWholeOuterCounter > 0) {
217 if (diffX < 0) {
218 if (this.horizontalWholeOuterCounter > Math.abs(diffX)) {
219 // 偏移量还没有用完
220 this.horizontalWholeOuterCounter += diffX;
221 diffX = 0;
222 }
223 else {
224 // 溢出量置为0,偏移量减去剩余溢出量,并且可以被拖动
225 diffX += this.horizontalWholeOuterCounter;
226 this.horizontalWholeOuterCounter = 0;
227 this.props.horizontalOuterRangeOffset(0);
228 }
229 }
230 else {
231 this.horizontalWholeOuterCounter += diffX;
232 }
233 }
234 else if (this.horizontalWholeOuterCounter < 0) {
235 if (diffX > 0) {
236 if (Math.abs(this.horizontalWholeOuterCounter) > diffX) {
237 // 偏移量还没有用完
238 this.horizontalWholeOuterCounter += diffX;
239 diffX = 0;
240 }
241 else {
242 // 溢出量置为0,偏移量减去剩余溢出量,并且可以被拖动
243 diffX += this.horizontalWholeOuterCounter;
244 this.horizontalWholeOuterCounter = 0;
245 this.props.horizontalOuterRangeOffset(0);
246 }
247 }
248 else {
249 this.horizontalWholeOuterCounter += diffX;
250 }
251 }
252 else {
253 // 溢出偏移量为0,正常移动
254 }
255 // 产生位移
256 this.positionX += diffX / this.scale;
257 // 但是横向不能出现黑边
258 // 横向能容忍的绝对值
259 const horizontalMax = (this.props.imageWidth *
260 this.scale - this.props.cropWidth) / 2 / this.scale;
261 if (this.positionX < -horizontalMax) {
262 this.positionX = -horizontalMax;
263 // 让其产生细微位移,偏离轨道
264 this.horizontalWholeOuterCounter += -1 / 1e10;
265 }
266 else if (this.positionX > horizontalMax) {
267 this.positionX = horizontalMax;
268 // 让其产生细微位移,偏离轨道
269 this.horizontalWholeOuterCounter += 1 / 1e10;
270 }
271 this.animatedPositionX.setValue(this.positionX);
272 }
273 else {
274 // 不能横向拖拽,全部算做溢出偏移量
275 this.horizontalWholeOuterCounter += diffX;
276 }
277 // 溢出量不会超过设定界限
278 if (this.horizontalWholeOuterCounter > this.props.maxOverflow) {
279 this.horizontalWholeOuterCounter = this.props.maxOverflow;
280 }
281 else if (this.horizontalWholeOuterCounter < -this.props.maxOverflow) {
282 this.horizontalWholeOuterCounter = -this.props.maxOverflow;
283 }
284 if (this.horizontalWholeOuterCounter !== 0) {
285 // 如果溢出偏移量不是0,执行溢出回调
286 this.props.horizontalOuterRangeOffset(this.horizontalWholeOuterCounter);
287 }
288 if (this.props.imageHeight * this.scale > this.props.cropHeight) {
289 // 如果图片高度大图盒子高度, 可以纵向拖拽
290 this.positionY += diffY / this.scale;
291 this.animatedPositionY.setValue(this.positionY);
292 }
293 }
294 }
295 else {
296 // 多个手指的情况
297 // 取消长按状态
298 if (this.longPressTimeout) {
299 clearTimeout(this.longPressTimeout);
300 }
301 if (this.props.pinchToZoom) {
302 // 找最小的 x 和最大的 x
303 let minX;
304 let maxX;
305 if (evt.nativeEvent.changedTouches[0].locationX >
306 evt.nativeEvent.changedTouches[1].locationX) {
307 minX = evt.nativeEvent.changedTouches[1].pageX;
308 maxX = evt.nativeEvent.changedTouches[0].pageX;
309 }
310 else {
311 minX = evt.nativeEvent.changedTouches[0].pageX;
312 maxX = evt.nativeEvent.changedTouches[1].pageX;
313 }
314 let minY;
315 let maxY;
316 if (evt.nativeEvent.changedTouches[0].locationY >
317 evt.nativeEvent.changedTouches[1].locationY) {
318 minY = evt.nativeEvent.changedTouches[1].pageY;
319 maxY = evt.nativeEvent.changedTouches[0].pageY;
320 }
321 else {
322 minY = evt.nativeEvent.changedTouches[0].pageY;
323 maxY = evt.nativeEvent.changedTouches[1].pageY;
324 }
325 const widthDistance = maxX - minX;
326 const heightDistance = maxY - minY;
327 const diagonalDistance = Math.sqrt(widthDistance * widthDistance
328 + heightDistance * heightDistance);
329 this.zoomCurrentDistance = Number(diagonalDistance.toFixed(1));
330 if (this.zoomLastDistance !== null) {
331 const distanceDiff = (this.zoomCurrentDistance - this.zoomLastDistance) / 200;
332 let zoom = this.scale + distanceDiff;
333 if (zoom < 0.6) {
334 zoom = 0.6;
335 }
336 if (zoom > 10) {
337 zoom = 10;
338 }
339 // 记录之前缩放比例
340 const beforeScale = this.scale;
341 // 开始缩放
342 this.scale = zoom;
343 this.animatedScale.setValue(this.scale);
344 // 图片要慢慢往两个手指的中心点移动
345 // 缩放 diff
346 const diffScale = this.scale - beforeScale;
347 // 找到两手中心点距离页面中心的位移
348 // 移动位置
349 this.positionX -= this.centerDiffX * diffScale / this.scale;
350 this.positionY -= this.centerDiffY * diffScale / this.scale;
351 this.animatedPositionX.setValue(this.positionX);
352 this.animatedPositionY.setValue(this.positionY);
353 }
354 this.zoomLastDistance = this.zoomCurrentDistance;
355 }
356 }
357 },
358 onPanResponderRelease: (evt, gestureState) => {
359 // 双击缩放了,结束手势就不需要操作了
360 if (this.isDoubleClickScale) {
361 return;
362 }
363 if (this.scale < 1) {
364 // 如果缩放小于1,强制重置为 1
365 this.scale = 1;
366 Animated.timing(this.animatedScale, {
367 toValue: this.scale,
368 duration: 100,
369 }).start();
370 }
371 if (this.props.imageWidth * this.scale <= this.props.cropWidth) {
372 // 如果图片宽度小于盒子宽度,横向位置重置
373 this.positionX = 0;
374 Animated.timing(this.animatedPositionX, {
375 toValue: this.positionX,
376 duration: 100,
377 }).start();
378 }
379 if (this.props.imageHeight * this.scale <= this.props.cropHeight) {
380 // 如果图片高度小于盒子高度,纵向位置重置
381 this.positionY = 0;
382 Animated.timing(this.animatedPositionY, {
383 toValue: this.positionY,
384 duration: 100,
385 }).start();
386 }
387 // 横向肯定不会超出范围,由拖拽时控制
388 // 如果图片高度大于盒子高度,纵向不能出现黑边
389 if (this.props.imageHeight * this.scale > this.props.cropHeight) {
390 // 纵向能容忍的绝对值
391 const verticalMax = (this.props.imageHeight *
392 this.scale - this.props.cropHeight) / 2 / this.scale;
393 if (this.positionY < -verticalMax) {
394 this.positionY = -verticalMax;
395 }
396 else if (this.positionY > verticalMax) {
397 this.positionY = verticalMax;
398 }
399 Animated.timing(this.animatedPositionY, {
400 toValue: this.positionY,
401 duration: 100,
402 }).start();
403 }
404 // 拖拽正常结束后,如果没有缩放,直接回到0,0点
405 if (this.scale === 1) {
406 this.positionX = 0;
407 this.positionY = 0;
408 Animated.timing(this.animatedPositionX, {
409 toValue: this.positionX,
410 duration: 100,
411 }).start();
412 Animated.timing(this.animatedPositionY, {
413 toValue: this.positionY,
414 duration: 100,
415 }).start();
416 }
417 // 水平溢出量置空
418 this.horizontalWholeOuterCounter = 0;
419 // 取消长按
420 if (this.longPressTimeout) {
421 clearTimeout(this.longPressTimeout);
422 }
423 // 手势完成,如果是单个手指、距离上次按住只有预设秒、滑动距离小于预设值,认为是单击
424 const stayTime = new Date().getTime() - this.lastTouchStartTime;
425 const moveDistance = Math.sqrt(gestureState.dx
426 * gestureState.dx + gestureState.dy * gestureState.dy);
427 if (evt.nativeEvent.changedTouches.length === 1 && stayTime < this.props.leaveStayTime && moveDistance < this.props.leaveDistance) {
428 this.props.onClick();
429 }
430 else {
431 this.props.responderRelease(gestureState.vx, this.scale);
432 }
433 },
434 onPanResponderTerminate: () => {
435 },
436 });
437 }
438 /**
439 * 重置大小和位置
440 */
441 reset() {
442 this.scale = 1;
443 this.animatedScale.setValue(this.scale);
444 this.positionX = 0;
445 this.animatedPositionX.setValue(this.positionX);
446 this.positionY = 0;
447 this.animatedPositionY.setValue(this.positionY);
448 }
449 render() {
450 const animateConf = {
451 transform: [{
452 scale: this.animatedScale,
453 }, {
454 translateX: this.animatedPositionX,
455 }, {
456 translateY: this.animatedPositionY,
457 }],
458 };
459 return (React.createElement(View, Object.assign({ style: [styles.container, { width: this.props.cropWidth, height: this.props.cropHeight, backgroundColor: '#000' }] }, this.imagePanResponder.panHandlers),
460 React.createElement(Animated.View, { style: animateConf },
461 React.createElement(View, { onLayout: this.handleLayout, style: { width: this.props.imageWidth, height: this.props.imageHeight } }, this.props.children))));
462 }
463}
464ImageViewer.defaultProps = new Props();
465//# sourceMappingURL=index.js.map
\No newline at end of file