I am using React-native-reanimated and react-native-gesture-handler,
I need to trigger a function after animation done, This is my my code:
let a = new Value(1);
let onStateChange = event([
{
nativeEvent: ({state}) =>
block([
cond(
eq(state, State.END),
set(a, runTiming(new Clock(), 1, 0)),
),
]),
},
]);
return <TapGestureHandler onHandlerStateChange={onStateChange}>
I need something like this :
onStateChange = event => {
if (event.nativeEvent.state === State.END) {
alert("I'm being pressed");
}
return block([
cond(
eq(event.nativeEvent.state, State.END),
set(a, runTiming(new Clock(), 1, 0)),
),
]);
},
But not works. :/
There is a call method to handle back from native section to JS.
You can use the call method once the animation is finished (state.END).
This example is for normal animation but the implementation for your case should be similar, notice that when you use call you can pass back parameters to the function you want to use.
function runTiming({ clock, value, dest, afterAnimationActions }) {
const state = {
finished: new Value(0),
position: value,
time: new Value(0),
frameTime: new Value(0),
};
const config = {
duration: ANIMATION_DURATION,
toValue: dest,
easing: Easing.inOut(Easing.ease),
};
return block([
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.time, 0),
set(state.position, value),
set(state.frameTime, 0),
set(config.toValue, dest),
startClock(clock),
]),
timing(clock, state, config),
cond(state.finished, [
stopClock(clock),
call([dest], (d) => afterAnimationActions(d)), // <-- Add this
]),
state.position,
]);
}
With that, you can call runTiming like this:
transY = runTiming({
clock: this.clock,
value: this.from,
dest: this.toValue,
afterAnimationActions: this.afterAnimationActions,
});
And the afterAnimationActions should look like this:
afterAnimationActions = ([dest]) => {
// `dest` is the param we passed using `call`
// The function logic here
};
Related
I am trying to hide and show the header based on the scroll event from reanimated 2 useAnimatedScrollHandler and I need to use the diffClamp so whenever the user scrolls up the header should be shown in less time than the whole scroll event contentOffset.y value but the problem is diffClamp is I think from reanimated v1 and I need to use useAnimatedStyle hook in order to animate styles in reanimated v2 and finally it gives an error.
Can someone please help with it?
const clamp = (value, lowerBound, upperBound) => {
"worklet";
return Math.min(Math.max(lowerBound, value), upperBound);
};
const scrollClamp = useSharedValue(0);
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event, ctx) => {
const diff = event.contentOffset.y - ctx.prevY;
scrollClamp.value = clamp(scrollClamp.value + diff, 0, 200);
},
onBeginDrag: (event, ctx) => {
ctx.prevY = event.contentOffset.y;
}
});
const RStyle = useAnimatedStyle(() => {
const interpolateY = interpolate(
scrollClamp.value,
[0, 200],
[0, -200],
Extrapolate.CLAMP
)
return {
transform: [
{ translateY: interpolateY }
]
}
})
I've created some arcs using deck.gl. When you click on different points/polygons, different arcs appear between countries. When doing this, I want the map to zoom to the bounds of those arcs.
For clarity, here is an example: When clicking on Glasgow, I'd want to zoom to the arc shown (as tightly as possible):
It appears that with WebMercatorViewport, you can call fitBounds
(see: https://deck.gl/docs/api-reference/core/web-mercator-viewport#webmercatorviewport)
It's not clear to me how this gets used, though. I've tried to find examples, but have come up short. How can I add this to what I have?
Here is the code for the arcs:
fetch('countries.json')
.then(res => res.json())
.then(data => {
console.log('data',data)
const inFlowColors = [
[0, 55, 255]
];
const outFlowColors = [
[255, 200, 0]
];
const countyLayer = new deck.GeoJsonLayer({
id: 'geojson',
data: data,
stroked: true,
filled: true,
autoHighlight: true,
lineWidthScale: 20,
lineWidthMinPixels: 1,
pointRadiusMinPixels: 10,
opacity:.5,
getFillColor: () => [0, 0, 0],
getLineColor: () => [0,0,0],
getLineWidth: 1,
onClick: info => updateLayers(info.object),
pickable: true
});
const deckgl = new deck.DeckGL({
mapboxApiAccessToken: 'pk.eyJ1IjoidWJlcmRhdGEiLCJhIjoiY2pudzRtaWloMDAzcTN2bzN1aXdxZHB5bSJ9.2bkj3IiRC8wj3jLThvDGdA',
mapStyle: 'mapbox://styles/mapbox/light-v9',
initialViewState: {
longitude: -19.903283,
latitude: 36.371449,
zoom: 1.5,
maxZoom: 15,
pitch: 0,
bearing: 0
},
controller: true,
layers: []
});
updateLayers(
data.features.find(f => f.properties.name == 'United States' )
);
function updateLayers(selectedFeature) {
const {exports, centroid, top_exports, export_value} = selectedFeature.properties;
const arcs = Object.keys(exports).map(toId => {
const f = data.features[toId];
return {
source: centroid,
target: f.properties.centroid,
value: exports[toId],
top_exports: top_exports[toId],
export_value: export_value[toId]
};
});
arcs.forEach(a => {
a.vol = a.value;
});
const arcLayer = new deck.ArcLayer({
id: 'arc',
data: arcs,
getSourcePosition: d => d.source,
getTargetPosition: d => d.target,
getSourceColor: d => [0, 55, 255],
getTargetColor: d => [255, 200, 0],
getHeight: 0,
getWidth: d => d.vol
});
deckgl.setProps({
layers: [countyLayer, arcLayer]
});
}
});
Here it is as a Plunker:
https://plnkr.co/edit/4L7HUYuQFM19m9rI
I try to make it simple, starting from a raw implementation with ReactJs then try to translate into vanilla.
In ReactJS I will do something like that.
Import LinearInterpolator and WebMercatorViewport from react-map-gl:
import {LinearInterpolator, WebMercatorViewport} from 'react-map-gl';
Then I define an useEffect for viewport:
const [viewport, setViewport] = useState({
latitude: 37.7577,
longitude: -122.4376,
zoom: 11,
bearing: 0,
pitch: 0
});
Then I will define a layer to show:
const layerGeoJson = new GeoJsonLayer({
id: 'geojson',
data: someData,
...
pickable: true,
onClick: onClickGeoJson,
});
Now we need to define onClickGeoJson:
const onClickGeoJson = useCallback((event) => {
const feature = event.features[0];
const [minLng, minLat, maxLng, maxLat] = bbox(feature); // Turf.js
const viewportWebMercator = new WebMercatorViewport(viewport);
const {longitude, latitude, zoom} = viewport.fitBounds([[minLng, minLat], [maxLng, maxLat]], {
padding: 20
});
viewportWebMercator = {
...viewport,
longitude,
latitude,
zoom,
transitionInterpolator: new LinearInterpolator({
around: [event.offsetCenter.x, event.offsetCenter.y]
}),
transitionDuration: 1500,
};
setViewport(viewportWebMercator);
}, []);
First issue: in this way we are fitting on point or polygon clicked, but what you want is fitting arcs. I think the only way to overcome this kind of issue is to add a reference inside your polygon about bounds of arcs. You can precompute bounds for each feature and storage them inside your geojson (the elements clicked), or you can storage just a reference in feature.properties to point another object where you have your bounds (you can also compute them on the fly).
const dataWithComputeBounds = {
'firstPoint': bounds_arc_computed,
'secondPoint': bounds_arc_computed,
....
}
bounds_arc_computed need to be an object
bounds_arc_computed = {
minLng, minLat, maxLng, maxLat,
}
then on onClick function just take the reference
const { minLng, minLat, maxLng, maxLat} = dataWithComputedBounds[event.features[0].properties.reference];
const viewportWebMercator = new WebMercatorViewport(viewport);
...
Now just define our main element:
return (
<DeckGL
layers={[layerGeoJson]}
initialViewState={INITIAL_VIEW_STATE}
controller
>
<StaticMap
reuseMaps
mapStyle={mapStyle}
preventStyleDiffing
mapboxApiAccessToken={YOUR_TOKEN}
/>
</DeckGL>
);
At this point we are pretty close to what you already linked (https://codepen.io/vis-gl/pen/pKvrGP), but you need to use deckgl.setProps() onClick function instead of setViewport to change your viewport.
Does it make sense to you?
I'm trying to make element return to its initial position once PanGestureHandler is released.
onGestureEvent = event(
[
{
nativeEvent: ({ translationY, state, velocityY }) =>
cond(
eq(state, State.ACTIVE),
// ignore this part, it's working fine
[
cond(
lessOrEq(this.scrollOffset, 0),
set(
this.gestureY,
divide(sub(translationY, this.ignoredGestureY), 2)
),
set(this.ignoredGestureY, translations)
)
],
[
// here I invoke bounce back animation
set(
this.gestureY,
runTiming(this.backClock, this.gestureY, new Value(0))
)
]
)
}
],
{ useNativeDriver: true }
)
Here is runTiming function:
function runTiming (clock, value, dest, startStopClock = true) {
const state = {
finished: new Value(0),
position: new Value(0),
frameTime: new Value(3000),
time: new Value(3000)
}
const config = {
toValue: new Value(0),
duration: 3000,
easing: Easing.ease
}
return [
cond(clockRunning(clock), 0, [
set(state.finished, 0),
set(state.frameTime, 3000),
set(state.time, 3000),
set(state.position, value),
set(config.toValue, dest),
startClock(clock)
]),
timing(clock, state, config),
cond(state.finished, stopClock(clock)),
state.position
]
}
The problem is that the element returns back instantly. Why is 3000 ms duration is not working?
animation = new Animated.Value(0);
animationSequnce = Animated.sequence(
[Animated.timing(this.animation, { toValue: 1 }),
Animated.timing(this.animation, { toValue: 0, delay: 1000 }),
],
);
startAnimation = () => {
animationSequnce.start();
}
stopAnimation = () => {
animationSequnce.stop();
}
I want to start an animation sequence several times.
I tested it by writing code that calls startAnimation when the button is pressed.
The animation runs on the first run.
When the second button is clicked after the first animation is finished
Cannot read property 'start' of undefined error occurs.
startAnimation = () => {
Animated.sequence(
[Animated.timing(this.animation, { toValue: 1 }),
Animated.timing(this.animation, { toValue: 0, delay: 1000 }),
],
).start();
}
This change to startAnimation will not cause an error, but you will not be able to call stopAnimation because it will call a different AnimationSequnce each time.
What is the best way to use an animation multiple times?
to stop animation
this.animation.stopAnimation()
optional get callBack when the animation stop
this.animation.stopAnimation(this.callback())
or
Animated.timing(
this.animation
).stop();
The first time you call Animated.sequence(), react native creates a variable current, refers to the index of the current executing animation, and stores it locally.
When Animated.sequence().start() finished, the variable current won't be cleaned or reset to 0, so the second time you call start() on the same sequenced animation, the array index out of bound and cause error.
It's a hidden bug of react native, below is the implementation of Animated.sequence
const sequence = function(
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function(callback?: ?EndCallback) {
const onComplete = function(result) {
if (!result.finished) {
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function() {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function() {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function() {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function(): boolean {
return false;
},
};
};
My solution is to define a custom sequence, and manually reset current to 0 when the sequenced animations finished:
const sequence = function(
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function(callback?: ?EndCallback) {
const onComplete = function(result) {
if (!result.finished) {
current = 0;
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
current = 0;
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
current = 0;
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function() {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function() {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function() {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function(): boolean {
return false;
},
};
};
I got an directive that i use for animating elements within the DOM, the directive looks like this:
Vue.directive('animate', {
bind(el, binding) {
let start = (binding.value)?binding.value.start:{y: 50, autoAlpha: 0};
TweenMax.set(el, start);
},
inserted: function(el, binding) {
let end = (binding.value)?binding.value.end:{y: 0, autoAlpha: 1};
end.ease = Quart.easeOut;
end.onComplete = () => {
el.removeAttribute("style");
el.classList.add("animation-done");
};
const f = function() {
console.log("scrolling");
if (InViewPort(el)) {
window.removeEventListener('scroll', f);
TweenMax.to(el, 1.5, end);
}
};
window.addEventListener('scroll', f);
f();
}
});
The directive is called within the component like this:
v-animate="{start: {y: 50, autoAlpha: 0}, end: {y: 0, autoAlpha: 1, delay: index * 0.5}}"
This gives me the flexibility i would like to have, only one problem. The event listener is within the inserted scope, and not available on the unbind hook.
Maybe directive isn't the way to go in Vue, does anybody have some advice how to handle this?