Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add PreviewImage touchZoom #279

Closed
wants to merge 14 commits into from
Closed
26 changes: 25 additions & 1 deletion src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import getFixScaleEleTransPosition from './getFixScaleEleTransPosition';
import type { TransformAction, TransformType } from './hooks/useImageTransform';
import useImageTransform from './hooks/useImageTransform';
import useStatus from './hooks/useStatus';
import useTouchZoom from './hooks/useTouchZoom';
import Operations from './Operations';
import { BASE_SCALE_RATIO, WHEEL_MAX_SCALE_RATIO } from './previewConfig';

Expand Down Expand Up @@ -149,6 +150,15 @@ const Preview: React.FC<PreviewProps> = props => {
[`${prefixCls}-moving`]: isMoving,
});

// touch
const { touchPointInfo, onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = useTouchZoom(
updateTransform,
dispatchZoomChange,
transform,
visible,
imgRef,
);

useEffect(() => {
if (!enableTransition) {
setEnableTransition(true);
Expand Down Expand Up @@ -233,6 +243,8 @@ const Preview: React.FC<PreviewProps> = props => {
};

const onMouseDown: React.MouseEventHandler<HTMLDivElement> = event => {
if (touchPointInfo.eventType !== 'init') return;

// Only allow main button
if (!movable || event.button !== 0) return;
event.preventDefault();
Expand Down Expand Up @@ -283,6 +295,8 @@ const Preview: React.FC<PreviewProps> = props => {
};

const onDoubleClick = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => {
if (touchPointInfo.eventType !== 'init') return;

if (visible) {
if (scale !== 1) {
updateTransform({ x: 0, y: 0, scale: 1 }, 'doubleClick');
Expand Down Expand Up @@ -350,13 +364,23 @@ const Preview: React.FC<PreviewProps> = props => {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0) scale3d(${
transform.flipX ? '-' : ''
}${scale}, ${transform.flipY ? '-' : ''}${scale}, 1) rotate(${rotate}deg)`,
transitionDuration: !enableTransition && '0s',
transitionDuration:
!enableTransition || touchPointInfo.eventType !== 'init'
? !enableTransition
? '0'
: '0.1s'
: undefined,
}}
fallback={fallback}
src={src}
onWheel={onWheel}
onMouseDown={onMouseDown}
onDoubleClick={onDoubleClick}
// touch
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
/>
);

Expand Down
14 changes: 12 additions & 2 deletions src/hooks/useImageTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export type TransformType = {
flipY: boolean;
};

export type Transform = {
x: number;
y: number;
rotate: number;
scale: number;
flipX: boolean;
flipY: boolean;
}

export type TransformAction =
| 'flipY'
| 'flipX'
Expand All @@ -25,9 +34,10 @@ export type TransformAction =
| 'wheel'
| 'doubleClick'
| 'move'
| 'dragRebound';
| 'dragRebound'
| 'touchZoom';

const initialTransform = {
const initialTransform: Transform = {
x: 0,
y: 0,
rotate: 0,
Expand Down
227 changes: 227 additions & 0 deletions src/hooks/useTouchZoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { useCallback, useEffect, useRef } from 'react';
import type { Transform, TransformAction, TransformType } from './useImageTransform';

type Point = {
x: number;
y: number;
};
type EventType = 'init' | 'zoom' | 'move';

let lastTouchEnd = 0;
const initPoint = { x: 0, y: 0 };
const originalStyle = { position: '', top: '', left: '', width: '', overflow: '' };

function getDistance(a: Point, b: Point) {
const x = a.x - b.x;
const y = a.y - b.y;
return Math.hypot(x, y);
}
function getCenter(a: Point, b: Point) {
const x = (a.x + b.x) / 2;
const y = (a.y + b.y) / 2;
return [x, y];
}

function touchstart(event: TouchEvent) {
if (event.touches.length > 1) {
event.preventDefault();
}
}
function touchend(event: TouchEvent) {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}

/** Prohibit WeChat sliding & Prohibit browser scaling */
function slidingControl(stop: boolean) {
const body = document.getElementsByTagName('body')[0];

if (stop) {
body.style.position = 'fixed';
body.style.top = '0';
body.style.left = '0';
body.style.width = '100vw';
body.style.overflow = 'hidden';
wanpan11 marked this conversation as resolved.
Show resolved Hide resolved

document.addEventListener('touchstart', touchstart, {
passive: false,
});
document.addEventListener('touchend', touchend, {
passive: false,
});
} else {
const { position, top, left, width, overflow } = originalStyle;
body.style.position = position;
body.style.top = top;
body.style.left = left;
body.style.width = width;
body.style.overflow = overflow;

document.removeEventListener('touchstart', touchstart);
document.removeEventListener('touchend', touchend);
}
}
/** save original body style */
function getOriginalStyle() {
const body = document.getElementsByTagName('body')[0];
const { position, top, left, width, overflow } = body.style;
originalStyle.position = position;
originalStyle.top = top;
originalStyle.left = left;
originalStyle.width = width;
originalStyle.overflow = overflow;
}
getOriginalStyle();

/** Pinch-to-zoom & Move image after zooming in */
export default function useTouchZoom(
updateTransform: (newTransform: Partial<TransformType>, action: TransformAction) => void,
dispatchZoomChange: (
ratio: number,
action: TransformAction,
clientX?: number,
clientY?: number,
) => void,
transform: Transform,
visible: boolean,
imgRef: React.MutableRefObject<HTMLImageElement>,
) {
const touchPointInfo = useRef<{ touchOne: Point; touchTwo: Point; eventType: EventType }>({
touchOne: { ...initPoint },
touchTwo: { ...initPoint },
eventType: 'init',
});

const setTouchPoint = useCallback((a: Point, b: Point, eventType: EventType) => {
touchPointInfo.current.touchOne = a;
touchPointInfo.current.touchTwo = b;
touchPointInfo.current.eventType = eventType;
}, []);

const restTouchPoint = (event: React.TouchEvent<HTMLImageElement>) => {
const { touches = [] } = event;
if (touches.length) return;
setTouchPoint({ ...initPoint }, { ...initPoint }, 'init');
};

const onTouchStart = useCallback(
(event: React.TouchEvent<HTMLImageElement>) => {
const { touches = [] } = event;
if (touches.length > 1) {
// touch zoom
setTouchPoint(
{ x: touches[0].pageX, y: touches[0].pageY },
{ x: touches[1].pageX, y: touches[1].pageY },
'zoom',
);
} else {
// touch move
setTouchPoint(
{
x: touches[0].pageX - transform.x,
y: touches[0].pageY - transform.y,
},
{ ...initPoint },
'move',
);
}
},
[setTouchPoint, transform],
);

const onTouchMove = (event: React.TouchEvent<HTMLImageElement>) => {
const { touches = [] } = event;
const { touchOne, touchTwo, eventType } = touchPointInfo.current;

const oldPoint = {
a: { x: touchOne.x, y: touchOne.y },
b: { x: touchTwo.x, y: touchTwo.y },
};
const newPoint = {
a: { x: touches[0]?.pageX, y: touches[0]?.pageY },
b: { x: touches[1]?.pageX, y: touches[1]?.pageY },
};

if (eventType === 'zoom') {
const [x, y] = getCenter(newPoint.a, newPoint.b);
const ratio = getDistance(newPoint.a, newPoint.b) / getDistance(oldPoint.a, oldPoint.b);

if (ratio > 0.2) {
dispatchZoomChange(ratio, 'touchZoom', x, y);
setTouchPoint(newPoint.a, newPoint.b, 'zoom');
}
} else if (eventType === 'move' && transform.scale > 1) {
const { width, height } = imgRef.current;
const { x, y, scale } = transform;

let newX = x;
let newY = y;

if (width * scale > document.documentElement.clientWidth) {
newX = newPoint.a.x - oldPoint.a.x;
}

if (height * scale > document.documentElement.clientHeight) {
newY = newPoint.a.y - oldPoint.a.y;
}

updateTransform(
{
x: newX,
y: newY,
},
'move',
);
}
};

const onTouchEnd = (event: React.TouchEvent<HTMLImageElement>) => {
const { x, y, scale } = transform;
const { width, height } = imgRef.current;

let newX = x;
let newY = y;

if (width * scale > document.documentElement.clientWidth) {
const offset = (width * (scale - 1)) / 2;

if (x > offset) {
newX = offset;
} else if (x < -offset) {
newX = -offset;
}
}

if (height * scale > document.documentElement.clientHeight) {
const offset = (height * scale - document.documentElement.clientHeight) / 2;

if (y > offset) {
newY = offset;
} else if (y < -offset) {
newY = -offset;
}
}

updateTransform({ x: newX, y: newY }, 'move');
restTouchPoint(event);
};

useEffect(() => {
if (visible) {
slidingControl(true);
} else {
slidingControl(false);
}
}, [visible]);

return {
touchPointInfo: touchPointInfo.current,
onTouchStart,
onTouchMove,
onTouchEnd,
onTouchCancel: restTouchPoint,
};
}
Loading