Created
February 14, 2020 11:07
-
-
Save makirby/af261c45c4f98f860bd802095fd443e6 to your computer and use it in GitHub Desktop.
Interactable view in typescript with modifications
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react' | |
import Animated from 'react-native-reanimated' | |
import { PanGestureHandler, State as GestureState } from 'react-native-gesture-handler' | |
import { StyleProp, ViewStyle, ViewProps, InteractionManager } from 'react-native' | |
import _ from 'lodash' | |
const { | |
add, | |
cond, | |
diff, | |
divide, | |
eq, | |
event, | |
exp, | |
lessThan, | |
and, | |
call, | |
block, | |
multiply, | |
pow, | |
set, | |
abs, | |
clockRunning, | |
greaterOrEq, | |
lessOrEq, | |
sqrt, | |
startClock, | |
stopClock, | |
sub, | |
Clock, | |
Value, | |
onChange, | |
} = Animated | |
const ANIMATOR_PAUSE_CONSECUTIVE_FRAMES = 10 | |
const ANIMATOR_PAUSE_ZERO_VELOCITY = 1 | |
const DEFAULT_SNAP_TENSION = 300 | |
const DEFAULT_SNAP_DAMPING = 0.65 | |
const DEFAULT_GRAVITY_STRENGTH = 400 | |
const DEFAULT_GRAVITY_FALLOF = 40 | |
function sq(x: Animated.Adaptable<number>) { | |
return multiply(x, x) | |
} | |
function influenceAreaWithRadius(radius: number, anchor: GravityPoint) { | |
return { | |
left: (anchor.x || 0) - radius, | |
right: (anchor.x || 0) + radius, | |
top: (anchor.y || 0) - radius, | |
bottom: (anchor.y || 0) + radius, | |
} | |
} | |
function snapTo( | |
target: TossedTarget, | |
snapPoints: SnapPoint[], | |
best: SnapAnchor, | |
clb?: OnSnap, | |
dragClb?: OnDrag | |
): Animated.Node<number>[] { | |
const dist = new Value(0) | |
const snap = (pt: SnapPoint) => [ | |
set(best.tension, pt.tension || DEFAULT_SNAP_TENSION), | |
set(best.damping, pt.damping || DEFAULT_SNAP_DAMPING), | |
set(best.x, pt.x || 0), | |
set(best.y, pt.y || 0), | |
] | |
const snapDist = (pt: SnapPoint) => | |
add(sq(sub(target.x, pt.x || 0)), sq(sub(target.y, pt.y || 0))) | |
const arr = [ | |
set(dist, snapDist(snapPoints[0])), | |
...snap(snapPoints[0]), | |
...snapPoints.map(pt => { | |
const newDist = snapDist(pt) | |
return cond(lessThan(newDist, dist), [set(dist, newDist), ...snap(pt)]) | |
}), | |
] | |
if (clb || dragClb) { | |
arr.push(call([best.x, best.y, target.x, target.y], ([bx, by, x, y]) => { | |
snapPoints.forEach((pt, index) => { | |
if ( | |
(pt.x === undefined || pt.x === bx) && | |
(pt.y === undefined || pt.y === by) | |
) { | |
clb && clb({ nativeEvent: { ...pt, index } }) | |
dragClb && | |
dragClb({ | |
nativeEvent: { x, y, targetSnapPointId: pt.id, state: 'end' }, | |
}) | |
} | |
}) | |
})) | |
} | |
return arr | |
} | |
function springBehavior( | |
dt: Animated.Node<number>, | |
target: AdaptableTarget, | |
obj: BounceObj, | |
anchor: AdaptableTarget, | |
tension: Animated.Adaptable<number> = 300, | |
): Behavior { | |
const dx = sub(target.x, anchor.x); | |
const ax = divide(multiply(-1, tension, dx), obj.mass); | |
const dy = sub(target.y, anchor.y); | |
const ay = divide(multiply(-1, tension, dy), obj.mass); | |
return { | |
x: set(obj.vx, add(obj.vx, multiply(dt, ax))), | |
y: set(obj.vy, add(obj.vy, multiply(dt, ay))), | |
}; | |
} | |
function frictionBehavior( | |
dt: Animated.Node<number>, | |
target: AdaptableTarget, | |
obj: BounceObj, | |
damping: Animated.Adaptable<number> = 0.7, | |
): Behavior { | |
const friction = pow(damping, multiply(60, dt)); | |
return { | |
x: set(obj.vx, multiply(obj.vx, friction)), | |
y: set(obj.vy, multiply(obj.vy, friction)), | |
}; | |
} | |
function anchorBehavior( | |
dt: Animated.Node<number>, | |
target: AdaptableTarget, | |
obj: BounceObj, | |
anchor: AdaptableTarget, | |
): Behavior { | |
const dx = sub(anchor.x, target.x); | |
const dy = sub(anchor.y, target.y); | |
return { | |
x: set(obj.vx, divide(dx, dt)), | |
y: set(obj.vy, divide(dy, dt)), | |
}; | |
} | |
function gravityBehavior( | |
dt: Animated.Node<number>, | |
target: AdaptableTarget, | |
obj: BounceObj, | |
anchor: AdaptableTarget, | |
strength = DEFAULT_GRAVITY_STRENGTH, | |
falloff = DEFAULT_GRAVITY_FALLOF | |
): Behavior { | |
const dx = sub(target.x, anchor.x); | |
const dy = sub(target.y, anchor.y); | |
const drsq = add(sq(dx), sq(dy)); | |
const dr = sqrt(drsq); | |
const a = divide( | |
multiply(-1, strength, dr, exp(divide(multiply(-0.5, drsq), sq(falloff)))), | |
obj.mass | |
); | |
const div = divide(a, dr); | |
return { | |
x: cond(dr, set(obj.vx, add(obj.vx, multiply(dt, dx, div)))), | |
y: cond(dr, set(obj.vy, add(obj.vy, multiply(dt, dy, div)))), | |
}; | |
} | |
function bounceBehavior( | |
dt: Animated.Node<number>, | |
target: AdaptableTarget, | |
obj: BounceObj, | |
area: Boundaries, | |
bounce = 0, | |
): BounceBehavior { | |
const xnodes: Animated.Node<number>[] = []; | |
const ynodes: Animated.Node<number>[] = []; | |
const flipx = set(obj.vx, multiply(-1, obj.vx, bounce)); | |
const flipy = set(obj.vy, multiply(-1, obj.vy, bounce)); | |
if (area.left !== undefined) { | |
xnodes.push(cond(and(eq(target.x, area.left), lessThan(obj.vx, 0)), flipx)); | |
} | |
if (area.right !== undefined) { | |
xnodes.push( | |
cond(and(eq(target.x, area.right), lessThan(0, obj.vx)), flipx) | |
); | |
} | |
if (area.top !== undefined) { | |
xnodes.push(cond(and(eq(target.y, area.top), lessThan(obj.vy, 0)), flipy)); | |
} | |
if (area.bottom !== undefined) { | |
xnodes.push( | |
cond(and(eq(target.y, area.bottom), lessThan(0, obj.vy)), flipy) | |
); | |
} | |
return { | |
x: xnodes, | |
y: ynodes, | |
}; | |
} | |
export type Area = { | |
left?: Animated.Adaptable<number>; | |
right?: Animated.Adaptable<number>; | |
top?: Animated.Adaptable<number>; | |
bottom?: Animated.Adaptable<number>; | |
} | |
export type AdaptableTarget = { | |
x: Animated.Adaptable<number>; | |
y: Animated.Adaptable<number>; | |
}; | |
export type AnimatedTarget = { | |
x: Animated.Value<number>; | |
y: Animated.Value<number>; | |
}; | |
function withInfluence( | |
area: Area | undefined | null, | |
target: AdaptableTarget, | |
behavior: Behavior, | |
) { | |
if (!area) { | |
return behavior | |
} | |
const testLeft = area.left === undefined || lessOrEq(area.left, target.x) | |
const testRight = area.right === undefined || lessOrEq(target.x, area.right) | |
const testTop = area.top === undefined || lessOrEq(area.top, target.y); | |
const testBottom = area.bottom === undefined || lessOrEq(target.y, area.bottom) | |
const testNodes = [testLeft, testRight, testTop, testBottom].filter( | |
(t) => t !== true | |
) | |
// @ts-ignore there is always more than one node | |
const test = and(...testNodes) | |
return { | |
x: cond(test, behavior.x), | |
y: cond(test, behavior.y), | |
} | |
} | |
function withLimits( | |
value: Animated.Node<number>, | |
lowerBound: number | undefined, | |
upperBound: number | undefined | |
) { | |
let result = value | |
if (lowerBound !== undefined) { | |
result = cond(lessThan(value, lowerBound), lowerBound, result) | |
} | |
if (upperBound !== undefined) { | |
result = cond(lessThan(upperBound, value), upperBound, result) | |
} | |
return result | |
} | |
export type SnapAnchor = { | |
x: Animated.Value<number>; | |
y: Animated.Value<number>; | |
tension: Animated.Value<number>; | |
damping: Animated.Value<number>; | |
} | |
export type InfluenceArea = { | |
x?: Animated.Adaptable<number>; | |
y?: Animated.Adaptable<number>; | |
}; | |
export type DragEvent = { | |
nativeEvent: { | |
x: number; | |
y: number; | |
targetSnapPointId?: string; | |
state: string; | |
}; | |
}; | |
type BounceBehavior = { | |
x: Animated.Node<number>[]; | |
y: Animated.Node<number>[]; | |
} | |
type Behavior = { | |
x: Animated.Node<number>; | |
y: Animated.Node<number>; | |
} | |
export type SnapEvent = { nativeEvent: SnapPoint & { index: number; id: string } }; | |
export type Position = { x?: number; y?: number }; | |
export type SnapPoint = { | |
id: string; | |
x?: number; | |
y?: number; | |
tension?: number; | |
damping?: number; | |
/** Used only by the first snap point. Ignored if set on others. */ | |
overdragMaxDistance?: number; | |
}; | |
export type Boundaries = { | |
left?: number; | |
right?: number; | |
top?: number; | |
bottom?: number; | |
bounce?: number; | |
}; | |
export type BounceObj = { | |
vx: Animated.Value<number>; | |
vy: Animated.Value<number>; | |
mass: number; | |
}; | |
export type TossedTarget = { x: Animated.Node<number>; y: Animated.Node<number> }; | |
export type OnDrag = (event: DragEvent) => void; | |
export type OnSnap = (event: SnapEvent) => void; | |
export type StopEvent = { nativeEvent: { x: number | undefined; y: number | undefined } } | |
export type OnStop = (event: StopEvent) => void; | |
export type GravityPoint = { | |
x: number; | |
y: number; | |
strength: number; | |
tension: number; | |
falloff: number; | |
influenceArea: Area; | |
damping: number; | |
} | |
export type FrictionArea = { | |
damping?: number; | |
influenceArea?: Area; | |
} | |
export type SpringPoint = { | |
x: number; | |
y: number; | |
tension: number; | |
influenceArea: Area; | |
damping: number; | |
} | |
export type Buckets = [(Behavior | BounceBehavior)[], Behavior[], Behavior[]] | |
type Props = { | |
dragToss: number; | |
dragEnabled?: boolean; | |
initialPosition: { x: number; y: number }; | |
animatedValueX?: Animated.Value<number>; | |
animatedValueY?: Animated.Value<number>; | |
horizontalOnly?: boolean; | |
verticalOnly?: boolean; | |
snapPoints: SnapPoint[]; | |
dragWithSpring?: { tension: number; damping: number }; | |
onSnap?: OnSnap; | |
onDrag?: OnDrag; | |
// Remove for perf/not used currently | |
// onStop?: OnStop; | |
springPoints?: SpringPoint[]; | |
gravityPoints?: GravityPoint[]; | |
frictionAreas?: FrictionArea[]; | |
boundaries?: Boundaries; | |
style?: StyleProp<ViewStyle>; | |
onSnapPointsChange?: (ref: Interactable) => void; | |
} & ViewProps | |
type State = { | |
snapPoints: SnapPoint[]; | |
_transY: Animated.Node<number>; | |
} | |
class Interactable extends Component<Props, State> { | |
static defaultProps = { | |
dragToss: 0, | |
dragEnabled: true, | |
initialPosition: { x: 0, y: 0 }, | |
}; | |
_snapAnchor: SnapAnchor; | |
_dragging: AnimatedTarget; | |
_position: AnimatedTarget; | |
_velocity: AnimatedTarget; | |
_transX: Animated.Node<number>; | |
_transFunc: ( | |
axis: string, | |
vaxis: string, | |
lowerBound: keyof Boundaries, | |
upperBound: keyof Boundaries, | |
updatedSnapTo: Animated.Node<number>[], | |
snapPoints: SnapPoint[], | |
boundaries: Boundaries | undefined, | |
) => Animated.Node<number>; | |
_onGestureEvent: () => void; | |
_updateSnapToFunc: (snapPoints: SnapPoint[]) => Animated.Node<number>[]; | |
constructor (props: Props) { | |
super(props) | |
const gesture = { x: new Value(0), y: new Value(0) }; | |
const state = new Value(-1); | |
this._onGestureEvent = event([ | |
{ | |
nativeEvent: { | |
translationX: gesture.x, | |
translationY: gesture.y, | |
state: state, | |
}, | |
}, | |
]) | |
const target = { | |
x: new Value(props.initialPosition.x || 0), | |
y: new Value(props.initialPosition.y || 0), | |
} | |
const update = { | |
x: props.animatedValueX, | |
y: props.animatedValueY, | |
} | |
const clock = new Clock() | |
const dt = divide(diff(clock), 1000) | |
const obj = { | |
vx: new Value(0), | |
vy: new Value(0), | |
mass: 1, | |
} | |
const tossedTarget = { | |
x: add(target.x, multiply(props.dragToss, obj.vx)), | |
y: add(target.y, multiply(props.dragToss, obj.vy)), | |
} | |
const permBuckets: Buckets = [[], [], []] | |
const addSpring = ( | |
anchor: AdaptableTarget, | |
tension: Animated.Adaptable<number>, | |
influence: Area | undefined | null, | |
buckets = permBuckets | |
) => { | |
buckets[0].push( | |
withInfluence( | |
influence, | |
target, | |
springBehavior(dt, target, obj, anchor, tension) | |
) | |
) | |
} | |
const addFriction = ( | |
damping: Animated.Adaptable<number> | undefined, | |
influence: Area | undefined | null, | |
buckets = permBuckets, | |
) => { | |
buckets[1].push( | |
withInfluence( | |
influence, | |
target, | |
frictionBehavior(dt, target, obj, damping) | |
) | |
) | |
} | |
const addGravity = ( | |
anchor: AdaptableTarget, | |
strength: number, | |
falloff: number, | |
influence: Area, | |
buckets = permBuckets | |
) => { | |
buckets[0].push( | |
withInfluence( | |
influence, | |
target, | |
gravityBehavior(dt, target, obj, anchor, strength, falloff) | |
) | |
) | |
} | |
const addOverdragResistance = ( | |
snapPoints: SnapPoint[], | |
_target: AdaptableTarget, | |
) => { | |
const firstSnapPoint = snapPoints[0] | |
if (firstSnapPoint.overdragMaxDistance) { | |
const firstSnapPointY = firstSnapPoint.y as number | |
const firstSnapPointDamping = firstSnapPoint.damping || DEFAULT_SNAP_DAMPING | |
const overdragDistance = abs(sub(firstSnapPointY, _target.y)) | |
const overdragMaxDistance = abs(firstSnapPoint.overdragMaxDistance) | |
const overdragInterpolation = divide(sub(overdragMaxDistance, overdragDistance), overdragMaxDistance) | |
const overdragDecay = cond( | |
greaterOrEq(overdragInterpolation, 0), | |
pow(overdragInterpolation, 3), | |
0, | |
) | |
const resistance = cond( | |
lessThan(obj.vy, 0), // if is dragging up | |
multiply(firstSnapPointDamping, overdragDecay), // apply overdrag decay | |
firstSnapPointDamping // else, is dragging down, so don't apply overdrag decay | |
) | |
dragBuckets[1][0] = ( | |
withInfluence( | |
{ bottom: firstSnapPointY }, | |
_target, | |
frictionBehavior(dt, _target, obj, resistance) | |
) | |
) | |
} else { | |
// make sure the behavior is removed | |
dragBuckets[1] = [] | |
} | |
} | |
const dragAnchor = { x: new Value(0), y: new Value(0) } | |
const dragBuckets: Buckets = [[], [], []] | |
if (props.dragWithSpring) { | |
const { tension, damping } = props.dragWithSpring | |
addSpring(dragAnchor, tension, null, dragBuckets) | |
addFriction(damping, null, dragBuckets) | |
} else { | |
dragBuckets[0].push(anchorBehavior(dt, target, obj, dragAnchor)) | |
} | |
const handleStartDrag = | |
props.onDrag && | |
call([target.x, target.y], ([x, y]) => { | |
// @ts-ignore this is already checked one line above | |
props.onDrag({ nativeEvent: { x, y, state: 'start' } }) | |
}) | |
const snapBuckets: Buckets = [[], [], []]; | |
const snapAnchor = { | |
x: new Value(props.initialPosition.x || 0), | |
y: new Value(props.initialPosition.y || 0), | |
tension: new Value(DEFAULT_SNAP_TENSION), | |
damping: new Value(DEFAULT_SNAP_DAMPING), | |
} | |
const updateSnapToFunc = (newSnapPoints: SnapPoint[]) => snapTo( | |
tossedTarget, | |
newSnapPoints, | |
snapAnchor, | |
props.onSnap, | |
props.onDrag | |
) | |
this._updateSnapToFunc = updateSnapToFunc | |
const updateSnapTo = this._updateSnapToFunc(props.snapPoints) | |
addSpring(snapAnchor, snapAnchor.tension, null, snapBuckets); | |
addFriction(snapAnchor.damping, null, snapBuckets); | |
if (props.springPoints) { | |
props.springPoints.forEach(pt => { | |
addSpring(pt, pt.tension, pt.influenceArea); | |
if (pt.damping) { | |
addFriction(pt.damping, pt.influenceArea); | |
} | |
}) | |
} | |
if (props.gravityPoints) { | |
props.gravityPoints.forEach(pt => { | |
const falloff = pt.falloff || DEFAULT_GRAVITY_FALLOF; | |
addGravity(pt, pt.strength, falloff, pt.influenceArea); | |
if (pt.damping) { | |
const influenceArea = | |
pt.influenceArea || influenceAreaWithRadius(1.4 * falloff, pt); | |
addFriction(pt.damping, influenceArea); | |
} | |
}) | |
} | |
if (props.frictionAreas) { | |
props.frictionAreas.forEach(pt => { | |
addFriction(pt.damping, pt.influenceArea); | |
}) | |
} | |
if (props.boundaries) { | |
snapBuckets[0].push( | |
bounceBehavior( | |
dt, | |
target, | |
obj, | |
props.boundaries, | |
props.boundaries.bounce | |
) | |
); | |
} | |
// behaviors can go under one of three buckets depending on their priority | |
// we append to each bucket but in Interactable behaviors get added to the | |
// front, so we join in reverse order and then reverse the array. | |
const sortBuckets = (specialBuckets: Buckets) => ({ | |
x: specialBuckets | |
.map((b, idx) => [...permBuckets[idx], ...b].reverse().map(b => b.x)) | |
.reduce((acc, b) => acc.concat(b), []), | |
y: specialBuckets | |
.map((b, idx) => [...permBuckets[idx], ...b].reverse().map(b => b.y)) | |
.reduce((acc, b) => acc.concat(b), []), | |
}) | |
// dragBehaviors is sorted inside transFunc because it needs to be | |
// called after addOverdragResistance to use updated snap points | |
const snapBehaviors = sortBuckets(snapBuckets) | |
const noMovementFrames = { | |
x: new Value( | |
props.verticalOnly ? ANIMATOR_PAUSE_CONSECUTIVE_FRAMES + 1 : 0 | |
), | |
y: new Value( | |
props.horizontalOnly ? ANIMATOR_PAUSE_CONSECUTIVE_FRAMES + 1 : 0 | |
), | |
}; | |
const stopWhenNeeded = cond( | |
and( | |
greaterOrEq(noMovementFrames.x, ANIMATOR_PAUSE_CONSECUTIVE_FRAMES), | |
greaterOrEq(noMovementFrames.y, ANIMATOR_PAUSE_CONSECUTIVE_FRAMES) | |
), | |
[ | |
// TODO: onStop implementation | |
// props.onStop && cond( | |
// clockRunning(clock), | |
// call([target.x, target.y], ([x, y]) => { | |
// props.onStop && props.onStop({ nativeEvent: { x, y } }) | |
// }) | |
// ), | |
stopClock(clock), | |
], | |
startClock(clock) | |
) | |
const transFunc = ( | |
axis: string, | |
vaxis: string, | |
lowerBound: keyof Boundaries, | |
upperBound: keyof Boundaries, | |
updatedSnapTo: Animated.Node<number>[], | |
snapPoints: SnapPoint[], | |
boundaries: Boundaries | undefined, | |
) => { | |
const dragging = new Value(0) | |
const start = new Value(0) | |
const x = target[axis] | |
const vx = obj[vaxis] | |
const anchor = dragAnchor[axis] | |
const drag = gesture[axis] | |
let advance = cond( | |
lessThan(abs(vx), ANIMATOR_PAUSE_ZERO_VELOCITY), | |
x, | |
add(x, multiply(vx, dt)) | |
); | |
if (boundaries) { | |
advance = withLimits( | |
advance, | |
boundaries[lowerBound], | |
boundaries[upperBound] | |
) | |
} | |
const last = new Value(Number.MAX_SAFE_INTEGER) | |
const noMoveFrameCount = noMovementFrames[axis] | |
// Fix for snapTo not firing https://github.com/kmagiera/react-native-reanimated/issues/182#issuecomment-519632308 | |
const testMovementFrames = block([ | |
onChange(snapAnchor.x, set(last, Number.MAX_SAFE_INTEGER)), | |
onChange(snapAnchor.y, set(last, Number.MAX_SAFE_INTEGER)), | |
cond( | |
eq(advance, last), | |
set(noMoveFrameCount, add(noMoveFrameCount, 1)), | |
[set(last, advance), set(noMoveFrameCount, 0)] | |
), | |
]) | |
addOverdragResistance(snapPoints, target) | |
const dragBehaviors = sortBuckets(dragBuckets) | |
const step = cond( | |
eq(state, GestureState.ACTIVE), | |
[ | |
cond(dragging, | |
0, | |
// @ts-ignore Undefined is handled ok but not typed | |
[ | |
handleStartDrag, | |
startClock(clock), | |
set(dragging, 1), | |
set(start, x), | |
] | |
), | |
set(anchor, add(start, drag)), | |
cond(dt, dragBehaviors[axis]), | |
], | |
[ | |
cond(clockRunning(clock), 0, startClock(clock)), | |
cond(dragging, [updatedSnapTo, set(dragging, 0)]), | |
cond(dt, snapBehaviors[axis]), | |
testMovementFrames, | |
stopWhenNeeded, | |
] | |
) | |
const wrapStep = props.dragEnabled | |
// @ts-ignore cond type says it cant operate on bool although it can | |
? cond(props.dragEnabled, step, [set(dragging, 1), stopClock(clock)]) | |
: step | |
// export some values to be available for imperative commands | |
this._dragging[axis] = dragging | |
this._velocity[axis] = vx | |
// update animatedValueX/animatedValueY | |
const doUpdateAnReturn = update[axis] ? set(update[axis], x) : x | |
return block([wrapStep, set(x, advance), doUpdateAnReturn]) | |
} | |
// save ouput of func | |
this._transFunc = transFunc | |
// variables to be used to access reanimated values from imperative commands | |
this._dragging = {} as AnimatedTarget; | |
this._velocity = {} as AnimatedTarget; | |
this._position = target; | |
this._snapAnchor = snapAnchor | |
this._transX = this._transFunc('x', 'vx', 'left', 'right', updateSnapTo, props.snapPoints, props.boundaries) | |
this.state = { | |
_transY: this._transFunc('y', 'vy', 'top', 'bottom', updateSnapTo, props.snapPoints, props.boundaries), | |
snapPoints: props.snapPoints, | |
} | |
} | |
componentDidUpdate() { | |
if (!_.isEqual(this.props.snapPoints, this.state.snapPoints)) { | |
const updateSnapTo = this._updateSnapToFunc(this.props.snapPoints) | |
// Update vertical translation | |
this.setState({ | |
_transY: this._transFunc('y', 'vy', 'top', 'bottom', updateSnapTo, this.props.snapPoints, this.props.boundaries), | |
snapPoints: this.props.snapPoints | |
}, () => { | |
this.props.onSnapPointsChange && this.props.onSnapPointsChange(this) | |
}) | |
} | |
} | |
render() { | |
const { children, style, horizontalOnly, verticalOnly } = this.props | |
return ( | |
<PanGestureHandler | |
minDist={10} | |
maxPointers={1} | |
enabled={this.props.dragEnabled} | |
onGestureEvent={this._onGestureEvent} | |
onHandlerStateChange={this._onGestureEvent}> | |
<Animated.View | |
style={[ | |
style, | |
{ | |
transform: [ | |
{ | |
translateX: verticalOnly ? 0 : this._transX, | |
translateY: horizontalOnly ? 0 : this.state._transY, | |
}, | |
], | |
}, | |
]}> | |
{children} | |
</Animated.View> | |
</PanGestureHandler> | |
); | |
} | |
// imperative commands | |
setVelocity({ x, y }: Position) { | |
if (x !== undefined) { | |
this._dragging.x.setValue(1); | |
this._velocity.x.setValue(x); | |
} | |
if (y !== undefined) { | |
this._dragging.y.setValue(1); | |
this._velocity.y.setValue(y); | |
} | |
} | |
snapTo({ index }: { index: number }) { | |
const snapPoint = this.state.snapPoints[index]; | |
this._snapAnchor.tension.setValue( | |
snapPoint.tension || DEFAULT_SNAP_TENSION | |
); | |
this._snapAnchor.damping.setValue( | |
snapPoint.damping || DEFAULT_SNAP_DAMPING | |
); | |
this._snapAnchor.x.setValue(snapPoint.x || 0); | |
this._snapAnchor.y.setValue(snapPoint.y || 0); | |
InteractionManager.runAfterInteractions(() => { | |
this.props.onSnap && | |
this.props.onSnap({ nativeEvent: { ...snapPoint, index } }); | |
}) | |
} | |
changePosition({ x, y }: Position) { | |
if (x !== undefined) { | |
this._dragging.x.setValue(1); | |
this._position.x.setValue(x); | |
} | |
if (y !== undefined) { | |
this._dragging.x.setValue(1); | |
this._position.y.setValue(y); | |
} | |
} | |
} | |
export default Interactable; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment