Closed rijk closed 4 years ago
Hi Rijk
We aren't going to implement this because of the performance implications - I'd rather keep working on the transform approach.
The good news is you can cover a lot of layoutTransition use-cases simply by animating height
via animate
. For instance in your use-case animating height: 20
and height: 'auto'
.
Also if you didn't know, you know you can use useInvertedScale
without a new component:
const scaleX = useMotionValue(1)
const scaleY = useMotionValue(1)
const inverted = useInvertedScale({ scaleX, scaleY })
return (
<motion.div layoutTransition style={{ scaleX, scaleY }}>
<motion.div style={inverted} />
</motion.div>
)
Likewise there is an experimental API in the works for animating surrounding elements into their new positions. You can already use the UnstableSyncLayout
component today:
<UnstableSyncLayout>
<motion.div layoutTransition style={{ height: isOpen ? 0 : 200 }} />
<motion.div positionTransition />
</UnstableSyncLayout>
We will be trying to land on a better name ASAP.
Hi Matt, I’ll give it a try, thanks. What I’m trying to do is not animate from 20
to auto
, but auto-animate from [previous height] to [new content height]. Like example 2 at https://codepen.io/nkbt/pen/MarzEg?editors=101. If I had to name this as a property I would call this heightTransition
. Is this something you would be interested to accept a PR for?
You should already be able achieve this by forcing an animation using imperative animation controls
const controls = useAnimation()
useEffect(() => {
contentHasChanged && controls.start('height', 'auto')
}, [])
return <motion.div animate={controls} />
There's also a PR in progress that would allow you to do this declaratively via an invalidateAnimate
prop:
<motion.div invalidateAnimate={isOpen} animate={{height: 'auto'}} />
Edit:
Ahh ignore the above, sorry I understand the auto measurements will be off because of the lifecycle of the above.
Sounds great, thanks for the pointer!
Sorry I thought about it a little longer and realised the auto measurements will be off as they'll all take place after the content is switched.
Ah. So is there a way to do this, leveraging the measurement code you’ve written for layoutTransition
? The thing that was especially appealing there is that it runs on (and only on) component renders. This prevents the (otherwise very hard to solve) problem of nested 'autoHeight' components having a slowed down animation because they are responding to each other’s animations. And also, I just really want to switch to framer-motion :)
Try this custom hook! https://codesandbox.io/s/example-growing-container-39sdm
Hey Matt, I think I might have confused things with the expand/collapse stuff. That’s actually not the main thing; the main thing is the auto growing of the container. I’ve added an "Add" button to your Sandbox: https://codesandbox.io/s/example-growing-container-7xx10
What is supposed to happen is, when clicking add, a reply is added and the container slowly grows to its new size to not confuse the user (e.g. in case he was reading the comment below it and it suddenly jumps to a new position).
What happens now is the container stays the same height (while the replies are added and overflow):
This hook will run whenever a dependency changes - so you can just pass through num
too:
useAutoHeightAnimation([expanded, num])
Aah, sorry I should have seen that. Works like a charm man! https://codesandbox.io/s/example-growing-container-7xx10
Sorry, spoke too soon. I noticed a glitch so I recorded my screen to slow it down. Here's a frame by frame:
So the parent is first scaled to the new height (by the browser?) and only then does the animation kick in, scaling it back and then animating up to the new height.
Same thing when collapsing:
I've slowed down the transition (and added overflow: hidden
to the container) so you can really clearly see the glitch: https://codesandbox.io/s/example-growing-container-7xx10
It happens on expanding, collapsing, and adding replies.
By the way, this is a very frequently used pattern when you have a timeline or list of items that can change (e.g. notifications, to do items, news feed posts, comments etc — you animate the changes in so it's not jarring to the user). Is there really not something built into the library for it (like AnimatePresence
)? I always get the feeling when something is so hard to do, I am doing something wrong :)
Like, an alternative solution to this would be to just wrap all <Comment>
s in a Motion component that will animate them from 0 to auto when they are added to the tree (and of course disabling this on the initial page load).
Fixed the glitch! https://codesandbox.io/s/example-growing-container-d5oe5 (added a wrapper div so the ref
is not on the animated component). There's still a glitch on the first expand, but we're getting there :)
You’ll probably get some luck removing visual glitches by changing the useEffect to useLayoutEffect.
It’s not that it’s difficult to do - the hook above proves that - it’s that once you put something in the API you can’t take it out (specifically in Framer where we have to support old projects), and it’s yet another concept to explain. Animating height and width is wrong - it’s bad for performance and because you can only animate round values it looks poor even at 60fps. I’d rather give people a snippet they can use on the side and work towards a proper solution by improving layoutTransition in the meantime.
Gotcha. I appreciate the added concerns you have as a library maintainer. Just thought this might be a common use case you overlooked.
Regarding animating width/height: I get the performance issues, but I think in this use case this is exactly what you want, as the point of the effect is to make the surrounding elements to gradually move to make space for the new item. Just scaling the new item doesn’t do the trick (even with the scale inverter), as the surrounding content will still snap into place without animation. Unless you can think of another way to achieve this?
Tried useLayoutEffect
but doesn’t remove the glitch on the first expand unfortunately: https://codesandbox.io/s/example-growing-container-d5oe5
Tried a different angle, using positionTransition
: https://codesandbox.io/s/example-growing-container-with-positiontransition-lv6cm
It looks very promising, I think this could work! Reading the docs it also seems like this was designed for what I’m trying to do here:
Add a positionTransition prop to the child components to allow them to smoothly animate into their new positions when an item is removed.
For completeness, here is the final implementation, using layoutTransition
after all: https://codesandbox.io/s/example-growing-container-with-layouttransition-9d5tl
I tried to use this:
<UnstableSyncLayout>
<motion.div layoutTransition style={{ height: isOpen ? 0 : 200 }} />
<motion.div positionTransition />
</UnstableSyncLayout>
But the transitions were not in sync; the positionTransition
was way more bouncy than the layoutTransition
. The <UnstableSyncLayout>
component didn’t really seem to have any effect; changing it to a fragment (<>
) gave the exact same result.
So I ended up giving everything a layoutTransition
(both the replies container and the surrounding comments), and that was basically the answer I think. In addition to moving the expanded state up of course, as of course the surrounding comments cannot be animated when the parent does not rerender.
Thanks a ton for the help and nudging me in the right direction. ✌️
Yeah the defaults are different for both transitions, the thinking being layoutTransition probably affects wider areas whereas positionTransition is more item reordering. You can give the props the same settings as transition
to customise the animation used. Awesome that it works! That's some performant stuff ;)
ie layoutTransition={{ duration: 2 }}
Sorry to keep bothering.. 👀
I've worked out a more elaborate sandbox, which is closer to the component in my app I am trying to implement: https://codesandbox.io/s/timeline-with-height-transition-for-versions-tj9j3
It is a timeline with versions, each version containing its own comments. I've done the version expanding/collapsing using height 0/auto and AnimatePresence
to animate out the collapsed version. This works great. However, it doesn't work well with the layoutTransition
used for comments. The versions don't animate in response to content changes (for example, when adding a comment in version 2, version 3 jumps down instantly). As a result, when collapsing the replies, the container (which has overflow:hidden
) instantly jumps to its new height, cutting of the animation:
You can see this by toggling the replies in version 2 (or adding/deleting a comment).
What would your suggested approach be for this type of UI?
In case you were going to say "use layoutTransition
".. Here's an example where I've used layoutTransition
for the version content as well: https://codesandbox.io/s/timeline-with-positiontransition-everywhere-6fjrw (I've pulled the state up into the parent component, so that rerenders are triggered).
As you can see here a weird effect happens when switching versions or expanding replies, not really the accordeon type effect I tried to achieve.
I've coded another Sandbox using just height transitions, to demonstrate the intended effect: https://codesandbox.io/s/timeline-with-height-transition-everywhere-c55hb
Note that this only works on expanding/collapsing versions and replies, not on adding comments or replies. And also the performance sucks probably :)
const scaleX = useMotionValue(1) const scaleY = useMotionValue(1) const inverted = useInvertedScale({ scaleX, scaleY }) return ( <motion.div layoutTransition style={{ scaleX, scaleY }}> <motion.div style={inverted} /> </motion.div> )
This solution was perfect to solve the visual distortion issue with
layoutTransition
Comment moved from #268.
For the new layoutTransition was opted for a transform +
useInvertedScale()
approach. There are several practical issues with this when trying to implement something like a auto growing container for items:useInvertedScale()
transform: none
. Could be 'fixed' by doing something liketransform: rotate( .0001deg )
, but is a nasty workaround and not easy to do as the transform is applied inline.Describe the solution you'd like An option to animate
height
(+overflow:hidden
) on the parent container instead of using transforms. Ideal would be if theoverflow:hidden
would be applied automatically as well, and only while the animation is running.Here is an example of the effect I like to achieve: https://codesandbox.io/s/example-growing-container-t3sq2 -> but of course, without setting fixed heights (it also animate when content changes).