moltres-react
Version:
React integration with moltres CMS
463 lines (438 loc) • 11.7 kB
JavaScript
import React from 'react'
import { View, Animated, PanResponder } from 'react-native'
import PropTypes from 'prop-types'
import {
compose,
defaultProps,
setPropTypes,
withHandlers,
withPropsOnChange,
withState
} from 'recompose'
import momentumCenter from './momentumCenter'
import velocityAtBounds from './velocityAtBounds'
const ModePropType = PropTypes.oneOf(['decay', 'snap', 'spring-origin'])
const OvershootPropType = PropTypes.oneOf(['spring', 'clamp'])
const AnimatedPropType = PropTypes.any
const handleResponderGrant = (anim, mode) => {
switch (mode) {
case 'spring-origin':
anim.setValue(0)
break
case 'snap':
case 'decay':
anim.setOffset(anim._value + anim._offset)
anim.setValue(0)
break
}
}
const handleResponderMove = (anim, delta, min, max, overshoot, overshootReductionFactor) => {
let val = anim._offset + delta
if (val > max) {
switch (overshoot) {
case 'spring':
val = max + (val - max) / overshootReductionFactor
break
case 'clamp':
val = max
break
}
}
if (val < min) {
switch (overshoot) {
case 'spring':
val = min - (min - val) / overshootReductionFactor
break
case 'clamp':
val = min
break
}
}
val = val - anim._offset
anim.setValue(val)
}
const handleSnappedScroll = (anim, min, max, velocity, spacing, deceleration) => {
let endX = momentumCenter(anim._value, velocity, spacing, deceleration)
endX = Math.max(endX, min)
endX = Math.min(endX, max)
const bounds = [endX - spacing / 2, endX + spacing / 2]
const endV = velocityAtBounds(anim._value, velocity, bounds, deceleration)
const listener = anim.addListener(({ value }) => {
if (value > bounds[0] && value < bounds[1]) {
Animated.spring(anim, {
toValue: endX,
velocity: endV
}).start()
}
})
Animated.decay(anim, {
deceleration,
velocity
}).start(() => {
anim.removeListener(listener)
})
}
const handleMomentumScroll = (
anim,
min,
max,
velocity,
overshoot,
deceleration,
onOvershoot,
overshootSpringConfig
) => {
const listener = anim.addListener(({ value }) => {
if (value < min) {
anim.removeListener(listener)
if (onOvershoot) {
onOvershoot() // TODO: what args should we pass to this
}
switch (overshoot) {
case 'spring':
Animated.spring(anim, {
...overshootSpringConfig,
toValue: min,
velocity
}).start()
break
case 'clamp':
anim.setValue(min)
break
}
} else if (value > max) {
anim.removeListener(listener)
if (onOvershoot) {
onOvershoot() // TODO: what args should we pass to this
}
switch (overshoot) {
case 'spring':
Animated.spring(anim, {
...overshootSpringConfig,
toValue: max,
velocity
}).start()
break
case 'clamp':
anim.setValue(min)
break
}
}
})
Animated.decay(anim, {
deceleration,
velocity
}).start(() => {
anim.removeListener(listener)
})
}
const handleResponderRelease = (
anim,
min,
max,
velocity,
overshoot,
mode,
snapSpacing,
deceleration,
onOvershoot,
overshootSpringConfig,
springOriginConfig
) => {
anim.flattenOffset()
if (anim._value < min) {
if (onOvershoot) {
onOvershoot() // TODO: what args should we pass to this
}
switch (overshoot) {
case 'spring':
Animated.spring(anim, {
...overshootSpringConfig,
toValue: min,
velocity
}).start()
break
case 'clamp':
anim.setValue(min)
break
}
} else if (anim._value > max) {
if (onOvershoot) {
onOvershoot() // TODO: what args should we pass to this
}
switch (overshoot) {
case 'spring':
Animated.spring(anim, {
...overshootSpringConfig,
toValue: max,
velocity
}).start()
break
case 'clamp':
anim.setValue(min)
break
}
} else {
switch (mode) {
case 'snap':
handleSnappedScroll(anim, min, max, velocity, snapSpacing, overshoot, deceleration)
break
case 'decay':
handleMomentumScroll(
anim,
min,
max,
velocity,
overshoot,
deceleration,
onOvershoot,
overshootSpringConfig
)
break
case 'spring-origin':
Animated.spring(anim, {
...springOriginConfig,
toValue: 0,
velocity
}).start()
break
}
}
}
const enhance = compose(
setPropTypes({
// Component Config
locked: PropTypes.bool,
lockDirection: PropTypes.bool,
horizontal: PropTypes.bool,
vertical: PropTypes.bool,
overshootX: OvershootPropType,
overshootY: OvershootPropType,
xBounds: PropTypes.arrayOf(PropTypes.number),
yBounds: PropTypes.arrayOf(PropTypes.number),
xMode: ModePropType,
yMode: ModePropType,
snapSpacingX: PropTypes.number, // TODO: also allow an array of values?
snapSpacingY: PropTypes.number,
// Animated Values
panX: AnimatedPropType,
panY: AnimatedPropType,
// Animation Config
overshootSpringConfig: PropTypes.any,
momentumDecayConfig: PropTypes.any,
springOriginConfig: PropTypes.any,
directionLockDistance: PropTypes.number,
overshootReductionFactor: PropTypes.number,
// Events
onOvershoot: PropTypes.func,
onDirectionChange: PropTypes.func,
onReleaseX: PropTypes.func,
onReleaseY: PropTypes.func,
onRelease: PropTypes.func
}),
defaultProps({
locked: false,
horizontal: false,
vertical: false,
lockDirection: true,
overshootX: 'spring',
overshootY: 'spring',
xBounds: [-Infinity, Infinity],
yBounds: [-Infinity, Infinity],
yMode: 'decay',
xMode: 'decay',
overshootSpringConfig: { friction: 7, tension: 40 },
momentumDecayConfig: { deceleration: 0.993 },
springOriginConfig: { friction: 7, tension: 40 },
overshootReductionFactor: 3,
directionLockDistance: 10
}),
withPropsOnChange(
() => false,
({ panX, panY }) => ({
panX: panX || new Animated.Value(0),
panY: panY || new Animated.Value(0)
})
),
withState('direction', 'setDirection', null),
withHandlers({
onStartShouldSetPanResponder: ({ locked }) => () => !locked,
onMoveShouldSetPanResponder: () => () => false,
onMoveShouldSetPanResponderCapture: () => () => false,
onMoveShouldSetResponderCapture: () => () => false,
onPanResponderTerminationRequest: () => () => true,
onPanResponderEnd: () => () => true,
onPanResponderGrant: ({
horizontal,
panX,
panY,
setDirection,
vertical,
xMode,
yMode
}) => (/* ...args */) => {
// if (this.props.onPanResponderGrant) {
// this.props.onPanResponderGrant(...args)
// }
handleResponderGrant(panX, xMode)
handleResponderGrant(panY, yMode)
setDirection(horizontal && !vertical ? 'x' : vertical && !horizontal ? 'y' : null)
},
onPanResponderMove: ({
direction,
// locked,
panX,
panY,
xBounds,
yBounds,
overshootX,
overshootY,
horizontal,
vertical,
lockDirection,
directionLockDistance,
overshootReductionFactor,
onDirectionChange,
setDirection
}) => (_, { dx, dy, x0, y0 }) => {
// if (locked) {
// return
// }
if (!direction) {
const dx2 = dx * dx
const dy2 = dy * dy
if (dx2 + dy2 > directionLockDistance) {
const _direction = dx2 > dy2 ? 'x' : 'y'
setDirection(_direction)
if (onDirectionChange) {
onDirectionChange(_direction, { dx, dy, x0, y0 })
}
}
}
// if (this.props.onPanResponderMove) {
// this.props.onPanResponderMove(_, { dx, dy, x0, y0 })
// }
if (horizontal && (!lockDirection || direction === 'x')) {
const [xMin, xMax] = xBounds
handleResponderMove(panX, dx, xMin, xMax, overshootX, overshootReductionFactor)
}
if (vertical && (!lockDirection || direction === 'y')) {
const [yMin, yMax] = yBounds
handleResponderMove(panY, dy, yMin, yMax, overshootY, overshootReductionFactor)
}
},
onPanResponderRelease: ({
direction,
panX,
panY,
xBounds,
yBounds,
overshootX,
overshootY,
horizontal,
vertical,
lockDirection,
xMode,
yMode,
snapSpacingX,
snapSpacingY,
onRelease,
onReleaseX,
onReleaseY,
onOvershoot,
overshootSpringConfig,
springOriginConfig,
momentumDecayConfig,
setDirection
}) => (_, { vx, vy, dx, dy }) => {
let cancel = false
if (onRelease) {
cancel = false === onRelease({ vx, vy, dx, dy })
}
if (!cancel && horizontal && (!lockDirection || direction === 'x')) {
const [xMin, xMax] = xBounds
if (onReleaseX) {
cancel = false === onReleaseX({ vx, vy, dx, dy })
}
!cancel &&
handleResponderRelease(
panX,
xMin,
xMax,
vx,
overshootX,
xMode,
snapSpacingX,
momentumDecayConfig.deceleration,
onOvershoot,
overshootSpringConfig,
springOriginConfig
)
}
if (!cancel && vertical && (!lockDirection || direction === 'y')) {
const [yMin, yMax] = yBounds
if (onReleaseY) {
cancel = false === onReleaseY({ vx, vy, dx, dy })
}
!cancel &&
handleResponderRelease(
panY,
yMin,
yMax,
vy,
overshootY,
yMode,
snapSpacingY,
momentumDecayConfig.deceleration,
onOvershoot,
overshootSpringConfig,
springOriginConfig
)
}
setDirection(horizontal && !vertical ? 'x' : vertical && !horizontal ? 'y' : null)
return cancel
}
}),
withPropsOnChange(
[
'onDirectionChange',
'onMoveShouldSetPanResponder',
'onMoveShouldSetResponderCapture',
'onMoveShouldSetPanResponderCapture',
'onPanResponderEnd',
'onPanResponderGrant',
'onPanResponderMove',
'onPanResponderRelease',
'onPanResponderTerminationRequest',
'onStartShouldSetPanResponder'
],
({
// onDirectionChange,
onMoveShouldSetPanResponder,
onMoveShouldSetPanResponderCapture,
onMoveShouldSetResponderCapture,
onPanResponderEnd,
onPanResponderGrant,
onPanResponderMove,
onPanResponderRelease,
onPanResponderTerminationRequest,
onStartShouldSetPanResponder
}) => ({
responder: PanResponder.create({
onMoveShouldSetPanResponder,
onMoveShouldSetPanResponderCapture,
onMoveShouldSetResponderCapture,
onPanResponderEnd,
onPanResponderGrant,
onPanResponderMove,
onPanResponderRelease,
onPanResponderTerminationRequest,
onStartShouldSetPanResponder
})
})
)
)
const PanController = enhance(({ children, style, responder }) => (
<View children={children} style={style} removeClippedSubviews={true} {...responder.panHandlers} />
))
export default PanController