UNPKG

react-native-gesture-image-viewer

Version:

🖼️ A highly customizable and easy-to-use React Native image viewer with gesture support and external controls

320 lines (251 loc) 7.99 kB
import { type SharedValue, withTiming } from 'react-native-reanimated'; import type { GestureViewerControllerState, GestureViewerEventCallback, GestureViewerEventData, GestureViewerEventType, } from './types'; import { createBoundsConstraint, createScrollAction } from './utils'; class GestureViewerManager { private currentIndex = 0; private dataLength = 0; private width = 0; private height = 0; private maxZoomScale = 2; private enableSwipeGesture = true; private enableLoop = false; private listRef: any | null = null; private scale: SharedValue<number> | null = null; private rotation: SharedValue<number> | null = null; private translateX: SharedValue<number> | null = null; private translateY: SharedValue<number> | null = null; private loopCallback: (() => void) | null = null; private listeners = new Set<(state: GestureViewerControllerState) => void>(); private eventListeners = new Map<GestureViewerEventType, Set<(data: any) => void>>(); private notifyListeners() { const state = this.getState(); this.listeners.forEach((listener) => listener(state)); } subscribe(listener: (state: GestureViewerControllerState) => void) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; } addEventListener<T extends GestureViewerEventType>(eventType: T, callback: GestureViewerEventCallback<T>) { if (!this.eventListeners.has(eventType)) { this.eventListeners.set(eventType, new Set()); } this.eventListeners.get(eventType)?.add(callback); return () => { const listeners = this.eventListeners.get(eventType); if (listeners) { listeners.delete(callback); if (listeners.size === 0) { this.eventListeners.delete(eventType); } } }; } private emitEvent<T extends GestureViewerEventType>(eventType: T, data: GestureViewerEventData[T]) { const listeners = this.eventListeners.get(eventType); if (listeners) { listeners.forEach((callback) => callback(data)); } } emitZoomChange = (scale: number, previousScale: number | null) => { this.emitEvent('zoomChange', { scale, previousScale }); }; emitRotationChange = (rotation: number, previousRotation: number | null) => { this.emitEvent('rotationChange', { rotation, previousRotation }); }; getState() { return { currentIndex: this.currentIndex, totalCount: this.dataLength, }; } setEnableLoop(enabled: boolean) { this.enableLoop = enabled; } setWidth(width: number) { this.width = width; } setHeight(height: number) { this.height = height; } setListRef(ref: any) { this.listRef = ref; } setDataLength(length: number) { this.dataLength = length; } setEnableSwipeGesture(enabled: boolean) { this.enableSwipeGesture = enabled; } setCurrentIndex(index: number) { if (index !== this.currentIndex) { this.currentIndex = index; } } setZoomSharedValues( scale: SharedValue<number>, translateX: SharedValue<number>, translateY: SharedValue<number>, maxZoomScale: number, ) { this.scale = scale; this.translateX = translateX; this.translateY = translateY; this.maxZoomScale = maxZoomScale; } notifyStateChange() { this.notifyListeners(); } setRotation(rotation: SharedValue<number>) { this.rotation = rotation; } rotate = (angle: 0 | 90 | 180 | 270 | 360 = 90, clockwise = true) => { const MAX_ANGLE = 360; if ( !this.rotation || angle < 0 || angle > MAX_ANGLE || (angle !== 0 && this.rotation.value % angle !== 0 && angle !== 360) ) { return; } if (angle === 0) { const nextAngle = Math.floor(this.rotation.value / MAX_ANGLE) * MAX_ANGLE; this.rotation.value = withTiming(clockwise ? nextAngle : nextAngle - MAX_ANGLE); return; } if (angle === 360) { this.rotation.value = withTiming(clockwise ? this.rotation.value + MAX_ANGLE : this.rotation.value - MAX_ANGLE); return; } const nextAngle = clockwise ? this.rotation.value + angle : this.rotation.value - angle; this.rotation.value = withTiming(nextAngle); }; zoomIn = (multiplier = 0.25) => { if (!this.scale || !this.translateX || !this.translateY || multiplier < 0.01 || multiplier > 1) { return; } const nextScale = Math.min(this.scale.value * (1 + multiplier), this.maxZoomScale); this.scale.value = withTiming(nextScale); const { translateX, translateY } = createBoundsConstraint({ width: this.width, height: this.height, })({ translateX: this.translateX.value, translateY: this.translateY.value, scale: nextScale, }); this.translateX.value = withTiming(translateX); this.translateY.value = withTiming(translateY); }; zoomOut = (multiplier = 0.25) => { if (!this.scale || !this.translateX || !this.translateY || multiplier < 0.01 || multiplier > 1) { return; } const nextScale = Math.max(this.scale.value / (1 + multiplier), 1); this.scale.value = withTiming(nextScale); if (nextScale === 1) { this.translateX.value = withTiming(0); this.translateY.value = withTiming(0); return; } const { translateX, translateY } = createBoundsConstraint({ width: this.width, height: this.height, })({ translateX: this.translateX.value, translateY: this.translateY.value, scale: nextScale, }); this.translateX.value = withTiming(translateX); this.translateY.value = withTiming(translateY); }; resetZoom = (scale = 1) => { if (!this.scale || !this.translateX || !this.translateY || scale <= 0 || scale > this.maxZoomScale) { return; } this.scale.value = withTiming(scale); this.translateX.value = withTiming(0); this.translateY.value = withTiming(0); }; goToIndex = (index: number) => { if (!this.enableSwipeGesture || !this.listRef) { return; } this.loopCallback = null; const { scrollTo } = createScrollAction(this.listRef, this.width); if (this.enableLoop && this.dataLength > 1) { if (index < 0) { this.loopCallback = () => { scrollTo(this.dataLength, false); this.updateCurrentIndex(this.dataLength - 1); this.loopCallback = null; }; scrollTo(0, true); return; } if (index >= this.dataLength) { this.loopCallback = () => { scrollTo(1, false); this.updateCurrentIndex(0); this.loopCallback = null; }; scrollTo(this.dataLength + 1, true); return; } scrollTo(index + 1, true); this.updateCurrentIndex(index); return; } if (index < 0 || index >= this.dataLength) { return; } scrollTo(index, true); this.updateCurrentIndex(index); }; handleMomentumScrollEnd = (scrollIndex: number) => { if (!this.loopCallback) { return false; } if (scrollIndex === 0 || scrollIndex === this.dataLength + 1) { this.loopCallback(); return true; } this.loopCallback = null; return true; }; handleScrollBeginDrag = () => { this.loopCallback = null; }; goToPrevious = () => { this.goToIndex(this.currentIndex - 1); }; goToNext = () => { this.goToIndex(this.currentIndex + 1); }; cleanUp() { this.loopCallback = null; this.listeners.clear(); this.listRef = null; this.enableSwipeGesture = true; this.currentIndex = 0; this.dataLength = 0; this.maxZoomScale = 2; this.scale = null; this.translateX = null; this.translateY = null; this.rotation = null; this.eventListeners.clear(); } private updateCurrentIndex = (targetIndex: number) => { this.currentIndex = targetIndex; this.notifyListeners(); }; } export default GestureViewerManager;