mirror of
https://github.com/zhigang1992/react-native-reanimated.git
synced 2026-01-13 09:29:58 +08:00
## Motivation Issue has been described here https://github.com/kmagiera/react-native-reanimated/issues/182 Using imperative snapTo in `Interactable.View` sometimes didn't lead to any effect. ## Changes It was caused by a fact that clock was not running and therefore changes haven't been evaluated properly. If GH responsible for dragging hadn't beed activated, clock was not running and then snapping without dragging was expected to run with clock stopped. Added extra condition for it.
530 lines
14 KiB
JavaScript
530 lines
14 KiB
JavaScript
import React, { Component } from 'react';
|
|
import Animated from 'react-native-reanimated';
|
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
|
|
|
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,
|
|
} = 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);
|
|
|
|
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 = [[], [], []];
|
|
|
|
const addSpring = (anchor, tension, influence, buckets = permBuckets) => {
|
|
buckets[0].push(
|
|
withInfluence(
|
|
influence,
|
|
target,
|
|
springBehavior(dt, target, obj, anchor, tension)
|
|
)
|
|
);
|
|
};
|
|
|
|
const addFriction = (damping, influence, buckets = permBuckets) => {
|
|
buckets[1].push(
|
|
withInfluence(
|
|
influence,
|
|
target,
|
|
frictionBehavior(dt, target, obj, damping)
|
|
)
|
|
);
|
|
};
|
|
|
|
const addGravity = (
|
|
anchor,
|
|
strength,
|
|
falloff,
|
|
influence,
|
|
buckets = permBuckets
|
|
) => {
|
|
buckets[0].push(
|
|
withInfluence(
|
|
influence,
|
|
target,
|
|
gravityBehavior(dt, 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, target, obj, dragAnchor));
|
|
}
|
|
|
|
const handleStartDrag =
|
|
props.onDrag &&
|
|
call([target.x, target.y], ([x, y]) =>
|
|
props.onDrag({ nativeEvent: { x, y, state: 'start' } })
|
|
);
|
|
|
|
const snapBuckets = [[], [], []];
|
|
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 updateSnapTo = snapTo(
|
|
tossedTarget,
|
|
props.snapPoints,
|
|
snapAnchor,
|
|
props.onSnap,
|
|
props.onDrag
|
|
);
|
|
|
|
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 => ({
|
|
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([target.x, 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 = 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 (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 = cond(
|
|
eq(advance, last),
|
|
set(noMoveFrameCount, add(noMoveFrameCount, 1)),
|
|
[set(last, advance), set(noMoveFrameCount, 0)]
|
|
);
|
|
const step = cond(
|
|
eq(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(clockRunning(clock), 0, startClock(clock)),
|
|
cond(dragging, [updateSnapTo, set(dragging, 0)]),
|
|
cond(dt, snapBehaviors[axis]),
|
|
testMovementFrames,
|
|
stopWhenNeeded,
|
|
]
|
|
);
|
|
const wrapStep = props.enabled
|
|
? cond(props.enabled, 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 = target;
|
|
this._snapAnchor = 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}
|
|
enabled={this.props.dragEnabled}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
View: Interactable,
|
|
};
|