react-native-interactable-with-android-link
Version:
Upgrade for react-native-interactable
552 lines (506 loc) • 15.1 kB
JavaScript
import React, { Component } from 'react';
import Animated from 'react-native-reanimated';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
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.7;
const DEFAULT_GRAVITY_STRENGTH = 400;
const DEFAULT_GRAVITY_FALLOF = 40;
function sq(x) {
return multiply(x, x);
}
function influenceAreaWithRadius(radius, anchor) {
return {
left: (anchor.x || 0) - radius,
right: (anchor.x || 0) + radius,
top: (anchor.y || 0) - radius,
bottom: (anchor.y || 0) + radius,
};
}
function snapTo(target, snapPoints, best, clb, dragClb) {
const dist = new Value(0);
const snap = (pt) => [
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) =>
add(sq(sub(target.x, pt.x || 0)), sq(sub(target.y, pt.y || 0)));
return [
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)]);
}),
(clb || dragClb) &&
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' },
});
}
});
}),
];
}
function springBehavior(dt, target, obj, anchor, tension = 300) {
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, target, obj, damping = 0.7) {
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, target, obj, anchor) {
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,
target,
obj,
anchor,
strength = DEFAULT_GRAVITY_STRENGTH,
falloff = DEFAULT_GRAVITY_FALLOF
) {
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, target, obj, area, bounce = 0) {
const xnodes = [];
const ynodes = [];
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,
};
}
function withInfluence(area, target, 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
);
const test = and(...testNodes);
return {
x: cond(test, behavior.x),
y: cond(test, behavior.y),
};
}
function withLimits(value, lowerBound, upperBound) {
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;
}
class Interactable extends Component {
static defaultProps = {
dragToss: 0.1,
dragEnabled: true,
initialPosition: { x: 0, y: 0 },
};
constructor(props) {
super(props);
this.gesture = { x: new Value(0), y: new Value(0) };
this.state = new Value(-1);
this._onGestureEvent = event([
{
nativeEvent: {
translationX: this.gesture.x,
translationY: this.gesture.y,
state: this.state,
},
},
]);
this.target = {
x: new Value(props.initialPosition.x || 0),
y: new Value(props.initialPosition.y || 0),
};
this.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),
};
this.initialize(props);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (!_.isEqual(nextProps.snapPoints, this.props.snapPoints)) {
this.initialize(nextProps);
}
}
initialize = (props) => {
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(
new Value(props.initialPosition.x || 0),
multiply(props.dragToss, obj.vx)
),
y: add(this.target.y, multiply(props.dragToss, obj.vy)),
};
const permBuckets = [[], [], []];
const addSpring = (anchor, tension, influence, buckets = permBuckets) => {
buckets[0].push(
withInfluence(
influence,
this.target,
springBehavior(dt, this.target, obj, anchor, tension)
)
);
};
const addFriction = (damping, influence, buckets = permBuckets) => {
buckets[1].push(
withInfluence(
influence,
this.target,
frictionBehavior(dt, this.target, obj, damping)
)
);
};
const addGravity = (
anchor,
strength,
falloff,
influence,
buckets = permBuckets
) => {
buckets[0].push(
withInfluence(
influence,
this.target,
gravityBehavior(dt, this.target, obj, anchor, strength, falloff)
)
);
};
const dragAnchor = { x: new Value(0), y: new Value(0) };
const dragBuckets = [[], [], []];
if (props.dragWithSpring) {
const { tension, damping } = props.dragWithSpring;
addSpring(dragAnchor, tension, null, dragBuckets);
addFriction(damping, null, dragBuckets);
} else {
dragBuckets[0].push(anchorBehavior(dt, this.target, obj, dragAnchor));
}
const handleStartDrag =
props.onDrag &&
call([this.target.x, this.target.y], ([x, y]) =>
props.onDrag({ nativeEvent: { x, y, state: 'start' } })
);
const snapBuckets = [[], [], []];
const updateSnapTo = snapTo(
tossedTarget,
props.snapPoints,
this.snapAnchor,
props.onSnap,
props.onDrag
);
addSpring(this.snapAnchor, this.snapAnchor.tension, null, snapBuckets);
addFriction(this.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,
this.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) => ({
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), []),
});
const dragBehaviors = sortBuckets(dragBuckets);
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)
),
[
props.onStop
? cond(
clockRunning(clock),
call([this.target.x, this.target.y], ([x, y]) =>
props.onStop({ nativeEvent: { x, y } })
)
)
: undefined,
stopClock(clock),
],
startClock(clock)
);
const trans = (axis, vaxis, lowerBound, upperBound) => {
const dragging = new Value(0);
const start = new Value(0);
const x = this.target[axis];
const vx = obj[vaxis];
const anchor = dragAnchor[axis];
const drag = this.gesture[axis];
let advance = cond(
lessThan(abs(vx), ANIMATOR_PAUSE_ZERO_VELOCITY),
x,
add(x, multiply(vx, dt))
);
if (props.boundaries) {
advance = withLimits(
advance,
props.boundaries[lowerBound],
props.boundaries[upperBound]
);
}
const last = new Value(Number.MAX_SAFE_INTEGER);
const noMoveFrameCount = noMovementFrames[axis];
const testMovementFrames = block([
onChange(this.snapAnchor.x, set(last, Number.MAX_SAFE_INTEGER)),
onChange(this.snapAnchor.y, set(last, Number.MAX_SAFE_INTEGER)),
cond(
eq(advance, last),
set(noMoveFrameCount, add(noMoveFrameCount, 1)),
[set(last, advance), set(noMoveFrameCount, 0)]
),
]);
const step = cond(
eq(this.state, State.ACTIVE),
[
cond(dragging, 0, [
handleStartDrag,
startClock(clock),
set(dragging, 1),
set(start, x),
]),
set(anchor, add(start, drag)),
cond(dt, dragBehaviors[axis]),
],
[
cond(dragging, [updateSnapTo, set(dragging, 0)]),
cond(dt, snapBehaviors[axis]),
testMovementFrames,
stopWhenNeeded,
]
);
const wrapStep = props.dragEnabled
? 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]);
};
// variables to be used to access reanimated values from imperative commands
this._dragging = {};
this._velocity = {};
this._position = this.target;
this._snapAnchor = this.snapAnchor;
this._transX = trans('x', 'vx', 'left', 'right');
this._transY = trans('y', 'vy', 'top', 'bottom');
};
render() {
const { children, style, horizontalOnly, verticalOnly } = this.props;
return (
<PanGestureHandler
maxPointers={1}
minDist={10}
enabled={this.props.dragEnabled}
failOffsetY={[-10, 10]}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onGestureEvent}
>
<Animated.View
style={[
style,
{
transform: [
{ translateX: verticalOnly ? 0 : this._transX },
{ translateY: horizontalOnly ? 0 : this._transY },
],
},
]}
>
{children}
</Animated.View>
</PanGestureHandler>
);
}
// imperative commands
setVelocity({ x, y }) {
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 }) {
const snapPoint = this.props.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);
this.props.onSnap &&
this.props.onSnap({ nativeEvent: { ...snapPoint, index } });
}
changePosition({ x, y }) {
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);
}
}
snapToPosition({ x, y }) {
this._snapAnchor.x.setValue(x || 0);
this._snapAnchor.y.setValue(y || 0);
}
}
export default {
View: Interactable,
};