Closed nandorojo closed 3 years ago
There has been discussion about native support from Framer Motion: https://github.com/framer/motion/issues/180
I'm not aiming to recreate every feature they have. Only the simple parts. This needs to solve 80% of the problems, not all of them.
This would honestly be awesome.
Reanimated v2 in of itself is a leaps and bounds improvement API over the API in v1 imo.
What do you would be the most important features to knock out right out of the gate?
Reanimated v2 in of itself is a leaps and bounds improvement API over the API in v1 imo.
Agreed, I found v1 almost unusable.
What do you would be the most important features to knock out right out of the gate?
I'm torn between animations and transitions, but I think simple style transitions would be the most important at first.
Expo + Next.js is an incredible stack. And yet, achieving this is a hassle:
const color = loading ? 'blue' : 'green'
<Text transitionProperties={['color']} sx={{ color }}/>
That doesn't make sense to me.
Doing this for responsive style arrays, and for function style props would be more difficult. I think that could be step 2.
Tracking if this variable changed to drive an animation is more of a challenge on native:
const color = loading ? 'blue' : 'green'
<Text transitionProperties={['color']} sx={{ color: [loading && 'blue', loading && 'green'] }}/>
On web, doing the above already works for me if I use the transitionProperty
CSS key in sx
.
I'm not sure if transitions or animations are more important, but I think transitions happen more often, and are currently a bit more difficult to drive on native. You can use Reanimated v1's Transition
, which is actually pretty nice, but it requires you to imperatively call ref.current.animateNextTransition()
which is annoying.
Another idea could be to use Reanimated's Transitioning.View
component under the hood, and when a certain style prop changes, we could run a transition (on native) by calling ref.current.animateNextTransition()
. The downside with this is you get less control over the style change, and it won't look consistent with web. Could still be worth playing with, though. If we went in this direction, we could rely on the useImperativeHandle
hook to make sure we forward the refs down properly.
It’s possible that running the animations is an easier start than the transitions, I’ll look into both. I wonder if this should live separately from the core Dripsy package. It would maybe be nice if it did, but I’m not sure if that’s possible, since it might have to read in the internal styles after the sx
prop has been parsed.
I wonder if this should live separately from the core Dripsy package. It would maybe be nice if it did, but I’m not sure if that’s possible, since it might have to read in the internal styles after the sx prop has been parsed.
I was wondering the same thing. Maybe it's possible to somehow merge the base style property and sx when the animations are being applied. But that I'm not sure of. It does however make sense for the library to be separate I can definitely see people choosing to use this even if they don't use dripsy.
Maybe it makes the most sense for it to be its own package that dripsy uses under the hood. If you use dripsy, you get it out of the box, but otherwise you don’t have to.
This way, the animation lib is receiving the final parsed style objects from dripsy itself.
I’d have to think through how creating a themed component would work in that case. It could be messy if every lib just re-exports React Native in its entirety.
One argument in favor of including it in Dripsy (which I’m not stuck to) is that you could use it for only animated styles and not even know about anything else. If you don’t use responsive styles or a theme, dripsy works just like a normal RN element (or at least it should...)
Another thought here, the animated library would basically only need to export View and Text now that I think about it. And an HOC for making your own.
I just checked out the Framer Motion docs. Pretty crazy how simple this is: https://www.framer.com/api/motion/animate-shared-layout/
That's pretty crazy. Some of those were relatively complex animations with just a few lines. I've never used framer personally but have definitely kept an eye on it.
If we could replicate something even close to that it would be super useful in RN.
Yeah...would be awesome. For now, even looping through object keys isn’t really supported in Reanimated 2, so I’m trying to put together a demo that can loop though each style change.
I put together an idea here: https://github.com/nandorojo/redrip
<View animate={{ width }} />
It takes an animate
prop which acts essentially as a CSS transition for values that change. I haven't taken a stab at animations yet, such as from
-> to
keyframes.
This is pretty sweet. I have some basic animations that might be able to use this on the app I'm working on I'll try it out.
If this new library did work closely with Dripsy one cool feature might be to have this library key off theme keys to create animations.
For example:
const theme = {
durations: {
short: 200
}
}
<View animate={{width }} duration='short' />
Agreed. That would be pretty doable.
I'm not sure if the transitions should live in the normal style
/sx
prop, or in something like animate
. The benefit of animate
is that there might be special things we want to do with those styles, such as turn them into transition-property
CSS values for web.
React spring has discussed integrating reanimated. This seems really promising.
https://github.com/pmndrs/react-spring/issues/764
However, it hasn’t been updated on npm in 6 months, so not sure if there’s much movement.
Haven't seen any movement there. React Spring looks pretty solid, here's an example (https://snack.expo.io/@nandorojo/biased-popsicle). I do prefer a component-only API personally.
I might try it for now, but I definitely have my eye on Reanimated 2 as a universal solution. I'll be trying both out as Reanimated approaches stability.
I haven't actually upgraded to Reanimated 2 in my app yet. I might have to wait for Expo SDK40 (since I assume it'll be more stable then.) That said, I may upgrade sooner if I can justify the refactor.
Something I haven't been able to figure out here.
I like this api from framer-motion:
<View initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
The idea is, initial
(if set) is the first state, which runs an animation to animate
. Also, any subsequent updates in animate
also animate.
Right now, the repo I shared that I'm working on does this:
<View animate={{ opacity: visible ? 1 : 0 }} />
That works well. I'm not sure how to get the initial
to work, though. I tried making it into another style with useAnimatedStyle
, and putting it first, but that doesn't seem to work. Maybe I should set these in useEffect
or something?
Turns out, I got all of this to work pretty gracefully. It's really nice. The only problem is that performance is (or at least seems to be) quite laggy on web. Not sure if Reanimated has really optimized for web yet or not. I'm going to see if using keyframes (or react-spring
/framer-motion
) makes a big difference. I'll push my changes soon, it's pretty cool.
Latest changes are pushed here: https://github.com/nandorojo/redrip
The example has a side-by-side comparison of @react-spring/native
and react-native-reanimated
.
Here is a video showing the two on iOS.
It's hard to say definitively which is more performant from such a minor example. Feel free to try them on ios/web to see. I could also try the same with framer-motion
on web, but I'd prefer not to, since this wouldn't use RNW.
50
ms makes it feel smoother. My guess is it's getting slowed down by the website first loading. This is, however, a reality of most JS-heavy sites, so I'd like to see what it's like when it's stress tested.react-spring
had trouble with this, while reanimated
was quite smooth.@react-spring/native
performed on iOS animating height. As you'll see in the video, it stays at 60FPS consistently. My guess is, this wouldn't be the case in a more complex app(?)If it turns out that Reanimated isn't mature enough on web, I think @react-spring/native
could be a good substitute in the meantime. I'd have to make sure there is parity between the APIs, but I'm feeling confident in being able to do so. I'd also make sure the default configs (such as spring configs, etc.) are the same.
While I've been hesitant to upgrade my app to Reanimated 2 (due to its alpha stage), I think I'm going to upgrade and try to bring this idea into it. It's possible I could release a beta for the universal animation lib very soon.
I got a lot of help from @terrysahaidak on Twitter for how I should structure this. I'll be pushing some updates soon, and once https://github.com/software-mansion/react-native-reanimated/issues/1511 is fixed I'll be comfortable merging this into my app.
I think I've nailed the ideal API. You can animate your components with either of the following options:
import { View } from 'redrip'
const Loader = ({ isLoading }) => {
return <View initial={{ opacity: 0 }} animate={{ opacity: isLoading ? 1 : 0 }} />
}
This API is ideal if you rely on state changes to drive your animations, such as loading states, etc. It's also very clean: any values in animate
will smoothly transition as you change them. If you set an initial
prop, then they will start there, but this isn't required.
I might rename initial
to from
. Not sure tbh.
This API also lets you run a simple enter animation, just like CSS keyframes:
import { View } from 'redrip'
const FadeIn = () => {
return <View initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ type: 'spring' }} />
}
If you never update the values in animate
, then this should be just as performant as option 2.
Super easy 🔥
This API looks more like react-spring
. It's more performant, since you never trigger re-renders and keep everything on the native thread.
Predefine static states, and transition to them as you please.
const animator = useAnimator({
initial: { opacity: 0 },
open: { opacity: 1 },
pressed: { opacity: 0.7 }
})
const onOpen = () => {
if (animator.current === 'initial') {
animator.transitionTo('open')
}
}
<View animator={animator} />
The benefits of this API become much more apparent as your styles have more than just one value. The pseudocode doesn't really do it justice.
Something I always thought has been a pain in React Native: animating dynamic height, such as auto
.
Even though it's based on a state change, the DX for this is really clean. It's nice for those times when you don't want to set up animated values and just want layout shifts to be smoother.
The idea use-case I see: you're showing a skeleton before some content loads, and then you want it to smoothly transition to its next state.
function Measure() {
const [{ height }, onLayout] = useLayout()
const [open, toggle] = useReducer((s) => !s, false)
return (
<>
<Drip.View animate={{ height }} style={{ overflow: 'hidden' }}>
<View
onLayout={onLayout}
style={{ height: open ? 100 : 300, backgroundColor: 'green' }}
/>
</Drip.View>
<Button title="toggle" onPress={toggle} />
</>
)
}
There's no magic: you just wrap a variable-height element with our library's View
, measure the child, and pass the height to the animate
prop.
You'll notice in the video that the shrinking of content is a bit less smooth than the expansion. I think for most situations, this is fine, since placeholder content is usually smaller than real content.
If you need more fine-grained control, you can use fixed heights. But I can probably count on one hand the number of times I've used fixed heights in real-world apps, besides images.
This is almost ready to push online. I just managed to get mount/unmount animations to work using framer-motions
's AnimatePresence
.
Video: https://twitter.com/FernandoTheRojo/status/1349884929765765123
Dripsy v3 has docs for usage with Moti: https://github.com/nandorojo/dripsy/blob/typez/README.md#%EF%B8%8F-animated-values
We've come full circle.
@nandorojo I'm starting a component lib using dripsy
, I also want to include Moti for animations. Do you know if there are any performance implications if I just used the MotiViews/MotiText
as the base for all the components animations work out of the box?
My guess is there may be some. As far as I know, creating Animated shared values has some cost to it. For example, if every component has useAnimatedStyle
under the hood, this may cause some issues.
That said, I don't know this to be true with certainty. It's just from stuff I've read on Reanimated issues. So I recommend asking on Reanimated's repo to get a better answer. It would certainly be cool to have both Moti and Dripsy working with every component.
When asking on Reanimated, I would want to know if 1) there is cost to useAnimatedStyle
with no styles, 2) if there is cost to creating a shared value with useSharedValue
since Moti uses this under the hood, and if Animated.View
in general is expensive to use vs a normal View.
That said, I don't know this to be true with certainty. It's just from stuff I've read on Reanimated issues. So I recommend asking on Reanimated's repo to get a better answer. It would certainly be cool to have both Moti and Dripsy working with every component.
Gotcha that makes a lot of sense; thanks for the insight. I'll play it safe in the meantime until I know a little bit more.
Sweet. I recommend using 2.3.x, I heard it has much better performance for many animated nodes.
This issue is more of a RFC.
What
There is an opportunity to make something like
react-native-animatable
that is powered by Reanimated 2.x. I think it would follow an API similar to CSS transitions. I'm looking toframer/motion
for inspiration on that, since it's super clean and easy to use.How
My top priority would be to achieve transitions at the component level, without any hooks, and with the least amount of code. It should be as simple as possible – no config. This seems like a great DX:
Under the hood, it would only use reanimated on native. Components would intelligently transition properties you tell it to. I don't have experience with Reanimated 2 yet, but it seems like they provide hooks that would make this remarkably doable. We could even default components to have
transitionProperty: all
, such that all you have to write is this:...and you get smooth transitions. Not sure if that's desired, but just spit balling.
We could use CSS transitions and keyframes on web, since RNW supports that. I'm not married to a specific method of solving this problem, though, so maybe Reanimated will work on web too.
In addition to property transitions, animations would be great:
Today
With react native web, CSS animations are really straightforward:
Same goes with transitions:
The problem is that this code is not universal. It only works on web. In my opinion, using
Platform.select
orPlatform.OS === 'web'
is an anti-pattern when it comes to styles. That logic should all be handled by a low-level design system. Dripsy has made strides for responsive styles in this regard. Smooth transitions with a similar DX would be a big addition.Final point: good defaults
In the spirit of zero configuration, this library should have great defaults. It should know the right amount of time for the ideal transition in milliseconds. I would look to well-designed websites to see what the best kind of easing function is, and this would be the default.
There is a plethora of "unopinionated" animation projects for React Native. There are few that are dead-simple, highly-performant, and have professional presets from the get go. I don't want to create an animation library. Only a small component library that animates what you want, smoothly, without any setup.