UNPKG

react-native-image-gallery

Version:

Pure JavaScript image gallery component for iOS and Android

422 lines (373 loc) 14 kB
import React from 'react'; import ReactNative, { View, Animated, Easing, NativeModules } from 'react-native'; import Scroller from '../Scroller'; import PropTypes from 'prop-types'; import { createResponder } from '../GestureResponder'; import { Rect, Transform, transformedRect, availableTranslateSpace, fitCenterRect, alignedRect, getTransform } from './TransformUtils'; export default class ViewTransformer extends React.Component { static Rect = Rect; static getTransform = getTransform; static propTypes = { enableTransform: PropTypes.bool, enableScale: PropTypes.bool, enableTranslate: PropTypes.bool, maxOverScrollDistance: PropTypes.number, maxScale: PropTypes.number, contentAspectRatio: PropTypes.number, enableResistance: PropTypes.bool, onViewTransformed: PropTypes.func, onTransformGestureReleased: PropTypes.func, onSingleTapConfirmed: PropTypes.func, onLayout: PropTypes.func, onTransformStart: PropTypes.func, children: PropTypes.node }; static defaultProps = { maxOverScrollDistance: 20, enableScale: true, enableTranslate: true, enableTransform: true, maxScale: 1, enableResistance: false }; constructor (props) { super(props); this.state = { // transform state scale: 1, translateX: 0, translateY: 0, // animation state animator: new Animated.Value(0), // layout width: 0, height: 0, pageX: 0, pageY: 0 }; this._viewPortRect = new Rect(); // A holder to avoid new too much this.onLayout = this.onLayout.bind(this); this.cancelAnimation = this.cancelAnimation.bind(this); this.contentRect = this.contentRect.bind(this); this.transformedContentRect = this.transformedContentRect.bind(this); this.animate = this.animate.bind(this); this.scroller = new Scroller(true, (dx, dy, scroller) => { if (dx === 0 && dy === 0 && scroller.isFinished()) { this.animateBounce(); return; } this.updateTransform({ translateX: this.state.translateX + dx / this.state.scale, translateY: this.state.translateY + dy / this.state.scale }); }); } viewPortRect () { this._viewPortRect.set(0, 0, this.state.width, this.state.height); return this._viewPortRect; } contentRect () { let rect = this.viewPortRect().copy(); if (this.props.contentAspectRatio && this.props.contentAspectRatio > 0) { rect = fitCenterRect(this.props.contentAspectRatio, rect); } return rect; } transformedContentRect () { let rect = transformedRect(this.viewPortRect(), this.currentTransform()); if (this.props.contentAspectRatio && this.props.contentAspectRatio > 0) { rect = fitCenterRect(this.props.contentAspectRatio, rect); } return rect; } currentTransform () { return new Transform(this.state.scale, this.state.translateX, this.state.translateY); } componentWillMount () { this.gestureResponder = createResponder({ onStartShouldSetResponder: (evt, gestureState) => true, onMoveShouldSetResponderCapture: (evt, gestureState) => true, // onMoveShouldSetResponder: this.handleMove, onResponderMove: this.onResponderMove, onResponderGrant: this.onResponderGrant, onResponderRelease: this.onResponderRelease, onResponderTerminate: this.onResponderRelease, onResponderTerminationRequest: (evt, gestureState) => false, // Do not allow parent view to intercept gesture onResponderSingleTapConfirmed: (evt, gestureState) => { this.props.onSingleTapConfirmed && this.props.onSingleTapConfirmed(); } }); } componentDidUpdate (prevProps, prevState) { this.props.onViewTransformed && this.props.onViewTransformed({ scale: this.state.scale, translateX: this.state.translateX, translateY: this.state.translateY }); } componentWillUnmount () { this.cancelAnimation(); } render () { let gestureResponder = this.gestureResponder; if (!this.props.enableTransform) { gestureResponder = {}; } return ( <View {...this.props} {...gestureResponder} ref={'innerViewRef'} onLayout={this.onLayout}> <View style={{ flex: 1, transform: [ { scale: this.state.scale }, { translateX: this.state.translateX }, { translateY: this.state.translateY } ] }}> { this.props.children } </View> </View> ); } onLayout (e) { const {width, height} = e.nativeEvent.layout; if (width !== this.state.width || height !== this.state.height) { this.setState({width, height}); } this.measureLayout(); this.props.onLayout && this.props.onLayout(e); } measureLayout () { let handle = ReactNative.findNodeHandle(this.refs['innerViewRef']); NativeModules.UIManager.measure(handle, (x, y, width, height, pageX, pageY) => { if (typeof pageX === 'number' && typeof pageY === 'number') { // avoid undefined values on Android devices if (this.state.pageX !== pageX || this.state.pageY !== pageY) { this.setState({ pageX: pageX, pageY: pageY }); } } }); } onResponderGrant (evt, gestureState) { this.props.onTransformStart && this.props.onTransformStart(); this.setState({responderGranted: true}); this.measureLayout(); } onResponderMove (evt, gestureState) { this.cancelAnimation(); let dx = gestureState.moveX - gestureState.previousMoveX; let dy = gestureState.moveY - gestureState.previousMoveY; if (this.props.enableResistance) { let d = this.applyResistance(dx, dy); dx = d.dx; dy = d.dy; } if (!this.props.enableTranslate) { dx = dy = 0; } let transform = {}; if (gestureState.previousPinch && gestureState.pinch && this.props.enableScale) { let scaleBy = gestureState.pinch / gestureState.previousPinch; let pivotX = gestureState.moveX - this.state.pageX; let pivotY = gestureState.moveY - this.state.pageY; let rect = transformedRect( transformedRect( this.contentRect(), this.currentTransform() ), new Transform(scaleBy, dx, dy, { x: pivotX, y: pivotY }) ); transform = getTransform(this.contentRect(), rect); } else { if (Math.abs(dx) > 2 * Math.abs(dy)) { dy = 0; } else if (Math.abs(dy) > 2 * Math.abs(dx)) { dx = 0; } transform.translateX = this.state.translateX + dx / this.state.scale; transform.translateY = this.state.translateY + dy / this.state.scale; } this.updateTransform(transform); return true; } onResponderRelease (evt, gestureState) { let handled = this.props.onTransformGestureReleased && this.props.onTransformGestureReleased({ scale: this.state.scale, translateX: this.state.translateX, translateY: this.state.translateY }); if (handled) { return; } if (gestureState.doubleTapUp) { if (!this.props.enableScale) { this.animateBounce(); return; } let pivotX = 0; let pivotY = 0; if (gestureState.dx || gestureState.dy) { pivotX = gestureState.moveX - this.state.pageX; pivotY = gestureState.moveY - this.state.pageY; } else { pivotX = gestureState.x0 - this.state.pageX; pivotY = gestureState.y0 - this.state.pageY; } this.performDoubleTapUp(pivotX, pivotY); } else { if (this.props.enableTranslate) { this.performFling(gestureState.vx, gestureState.vy); } else { this.animateBounce(); } } } performFling (vx, vy) { let startX = 0; let startY = 0; let maxX, minX, maxY, minY; let availablePanDistance = availableTranslateSpace(this.transformedContentRect(), this.viewPortRect()); if (vx > 0) { minX = 0; if (availablePanDistance.left > 0) { maxX = availablePanDistance.left + this.props.maxOverScrollDistance; } else { maxX = 0; } } else { maxX = 0; if (availablePanDistance.right > 0) { minX = -availablePanDistance.right - this.props.maxOverScrollDistance; } else { minX = 0; } } if (vy > 0) { minY = 0; if (availablePanDistance.top > 0) { maxY = availablePanDistance.top + this.props.maxOverScrollDistance; } else { maxY = 0; } } else { maxY = 0; if (availablePanDistance.bottom > 0) { minY = -availablePanDistance.bottom - this.props.maxOverScrollDistance; } else { minY = 0; } } vx *= 1000; // per second vy *= 1000; if (Math.abs(vx) > 2 * Math.abs(vy)) { vy = 0; } else if (Math.abs(vy) > 2 * Math.abs(vx)) { vx = 0; } this.scroller.fling(startX, startY, vx, vy, minX, maxX, minY, maxY); } performDoubleTapUp (pivotX, pivotY) { let curScale = this.state.scale; let scaleBy; if (curScale > (1 + this.props.maxScale) / 2) { scaleBy = 1 / curScale; } else { scaleBy = this.props.maxScale / curScale; } let rect = transformedRect( this.transformedContentRect(), new Transform(scaleBy, 0, 0, { x: pivotX, y: pivotY }) ); rect = transformedRect( rect, new Transform( 1, this.viewPortRect().centerX() - pivotX, this.viewPortRect().centerY() - pivotY ) ); rect = alignedRect(rect, this.viewPortRect()); this.animate(rect); } applyResistance (dx, dy) { let availablePanDistance = availableTranslateSpace(this.transformedContentRect(), this.viewPortRect()); if ((dx > 0 && availablePanDistance.left < 0) || (dx < 0 && availablePanDistance.right < 0)) { dx /= 3; } if ((dy > 0 && availablePanDistance.top < 0) || (dy < 0 && availablePanDistance.bottom < 0)) { dy /= 3; } return { dx, dy }; } cancelAnimation () { this.state.animator.stopAnimation(); } animate (targetRect, durationInMillis) { let duration = 200; if (durationInMillis) { duration = durationInMillis; } let fromRect = this.transformedContentRect(); if (fromRect.equals(targetRect, 0.01)) { return; } this.state.animator.removeAllListeners(); this.state.animator.setValue(0); this.state.animator.addListener((state) => { let progress = state.value; let left = fromRect.left + (targetRect.left - fromRect.left) * progress; let right = fromRect.right + (targetRect.right - fromRect.right) * progress; let top = fromRect.top + (targetRect.top - fromRect.top) * progress; let bottom = fromRect.bottom + (targetRect.bottom - fromRect.bottom) * progress; let transform = getTransform(this.contentRect(), new Rect(left, top, right, bottom)); this.updateTransform(transform); }); Animated.timing( this.state.animator, { toValue: 1, duration: duration, easing: Easing.inOut(Easing.ease) } ).start(); } animateBounce () { let curScale = this.state.scale; let minScale = 1; let maxScale = this.props.maxScale; let scaleBy = 1; if (curScale > maxScale) { scaleBy = maxScale / curScale; } else if (curScale < minScale) { scaleBy = minScale / curScale; } let rect = transformedRect( this.transformedContentRect(), new Transform( scaleBy, 0, 0, { x: this.viewPortRect().centerX(), y: this.viewPortRect().centerY() } ) ); rect = alignedRect(rect, this.viewPortRect()); this.animate(rect); } updateTransform (transform) { this.setState(transform); } forceUpdateTransform (transform) { this.setState(transform); } getAvailableTranslateSpace () { return availableTranslateSpace(this.transformedContentRect(), this.viewPortRect()); } }