Note: The key name x
in the style prop is an arbitrary choice. I could’ve written:
{(style) =>
}
Use the key name that indicates most clearly what you’re animating.
StaggeredMotion
works slightly differently than
in that everything is now multiple instead of singular. Instead of providing a defaultStyle we provide defaultStyles and instead of providing style we provide styles.
Considering we want to animate multiple things this makes sense. One other caveat: the styles
prop is now a function of previous styles. Sounds confusing, but once we apply it, it all makes sense.
Since we are responding to user touches here we must bring in our old friend the PanResponder
:
panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderMove: (event) => {
this.setState({x: event.nativeEvent.pageX, y: event.nativeEvent.pageY})
},
})
This says that once the user moves their finger we want to update the component state with the new coordinates of their finger.
Now, we are faced with two questions: How should the animation work?, and what should it be animating?.
How?
We answer this with just six lines:
styles={(prevStyles) => prevStyles.map((a, i) => {
return i === 0 ? this.state : {
x: spring(prevStyles[i - 1].x, presets.gentle),
y: spring(prevStyles[i - 1].y, presets.gentle),
}
})}
I mentioned previously that our styles prop will be a function of previous styles. This is what I meant. So we have an array of values that describes the ‘animation state’ of each chat bubble. This styles
prop gives us the chance to customize their behavior. To make sense of all this we observe there are two rules to this animation:
- The first bubble (or the bubble on top) directly follows the user’s finger. Thus its position should be where the user’s finger is. Recall that with the
PanResponder
we created before our component’s state is now keeping track of exactly where the user’s finger is, so we check this. Ifi === 0
, we are describing the style for the first bubble — so we simply return the current location of the user’s finger. - Bubble
i
is playing ‘catch-up’ with bubblei-1
. The second bubble wants to be where the first bubble just was, the third bubble wants to be where the second just was, and so on. This is why we have prevStyles!prevStyles
tells us where each bubble just was! So now all we have to do is tell each bubble to spring to where the bubble above it was. Thus, we have:{ x: spring(prevStyles[i — 1].x, presets.gentle), y: spring(prevStyles[i — 1].y, presets.gentle) }
. Thepresets.gentle
is merely a prepackaged configuration object (behind the scenes,presets.gentle={stiffness: 120, damping: 14}
).
What?
So now, we have described how the animation should work, along with the ‘staggering’ effect. But we haven’t actually put the animated styles to use yet. We do this with the below:
{styles =>
{styles.slice().reverse().map(({x, y}, i) => {
const index = styles.length - i - 1
return key={index}
style={{
width: 70,
borderRadius: 35,
height: 70,
position: 'absolute',
left: x + 3 * index,
top: y + 3 * index,
backgroundColor: colors[index],
}}/>
}
)}
}
Most of this code is self-explanatory but there are a few oddities you may be confused by.
Why are we calling .reverse()
? This has to do with ReactNative’s problems dealing with zIndex
. It seems that with an absolutely positioned View
, ReactNative completely ignores zIndex
. However the last child is always rendered first. For example:
You will see C
on top, then B
, then A
. If you want A
to be on top, you will have to reverse the order. So we simply reverse the order of the styles to get our first element to render last, putting it on top.
If this sounds counterintuitive, try it out for yourself! However, we still want to know the ‘actual’ index of each style object, thus we find it by taking styles.length — i — 1
. I hope this clears up the somewhat odd code structure. In an ideal world, this is what the code would actually look like:
{styles.map(({x, y}, i) => {
return key={i}
style={{
width: 70,
borderRadius: 35,
height: 70,
position: 'absolute',
left: x + 3 * i,
top: y + 3 * i,
backgroundColor: colors[i],
zIndex: styles.length - i
}}/>
}
)}
Unfortunately, React Native seems to have an unavoidable bug with zIndex that makes this impossible.
The second question: why are is left
not simply equal to x
and top
simply equal to y
? Well, if we left it like this, the bubbles would be stacked directly on top of each other, giving no indication that there are actually multiple bubbles. To offset this, we add an offset to every bubble. Each bubble is 3 pixels to the right and 3 pixels down from the bubble preceding it. This creates a more natural stacking effect.