Closed mattgperry closed 4 years ago
The initial and subsequent releases sound great 👍. I think being able to solve something like a collapse animation with scale/translate would be amazing!
I'm curious about your thoughts on handling scaling two pieces of text that have changed in size? I think it would be used alongside/instead of invertScale
. I was messing around with react-flip-toolkit a couple of weeks ago and it was the one thing I really wanted but the library didn't seem to support. I ended up with my own approach to solve this. The idea was if the fontSize
has changed when performing a FLIP then use normal scaling rather than trying to scale the box it's contained in.
Sounds great!
Great writeup! I like the initial idea of the sizeTransition
prop, but think things will get hairy when we try implementing them with scale. Using a invertScale
prop on motion.div
for that feels wrong to me, so I'd prefer a separate component.
What I don't like about it is that there's now way to couple it to the sizeTransition that is happening, what if there multiple levels of nested sizeTransition props and InvertScale
components? Come to think of it, would a dedicated SizeTransition
component not be a better fit for this than adding it to motion.div
? That would also solve the problems with Framer X compatibility as long as that is not using full DOM-layout yet.
I like the proposal as well, mainly the first one (width/height). Should make it easy to implement something similar react-collapse. This is kind of like an accordion, but I mainly use it to make containers that grow animated when something is added to the bottom (say, a comment is added).
Here's the component I use today:
import React, { memo } from 'react'
import { useSpring, animated } from 'react-spring'
import { useMeasure } from './useMeasure'
const Collapse = ({ children, className, isOpen = true }) => {
const [ bind, measured ] = useMeasure()
const { height } = useSpring({ height: ( isOpen ? measured.height : 0 ) })
return (
<animated.div className={className} style={{ height, overflow: 'hidden' }}>
<div {...bind}>
{children}
</div>
</animated.div>
)
}
export default memo( Collapse )
The trickiest thing with this component is when you have another nested Collapse
. For instance, when a comment has a "3 replies" link that can be expanded, and you want those replies to animate as well. In that case the parent Collapse
will animate too slowly, as it is reacting to the changes in height triggered by the child Collapse
(instead of an instant change in height, like normally would be the case).
This is a very tricky challenge, for which there is no (real) solution as far as I know. There are workarounds for it, but always based on the childs being fixed height, or knowing from the parent whether it contains active child Collapse
s.
Do you have thoughts on this? Would motion
be able to solve these issues?
@nvh We discussed this offline but for the good of the thread - I do see the value of InvertScale
being a separate component as we actively don't want to user to control any facet of it. Maybe even including its styling? We would need to prototype.
But I do think sizeTransition
should be a prop as its quite composable with other functionality ie positionTransition
, motion values, hover animations etc. It's also the element for layout so we'd retain semantics, motion.li
etc.
@rijk We can already do accordions as we can animate height
between any two value types (in this case 0
and "auto"
) - see here https://codesandbox.io/s/framer-motion-accordion-qx958 - but yeah sizeTransition
would be able to automatically animate to accomodate new content.
If this was to work the same way as positionTransition
, (measure on render, then during useLayoutEffect
) as long as all the nested components were being changed as a result of the same render cycle then I don't see this being a problem.
A more difficult situation is if we have a component that should update in size as a result of a child being removed from the tree asyncronously, via an exit animation triggered by AnimatePresence
. As in this case AnimatePresence
will only remove the outgoing component, therefore changing its size, once its exit animation has completed. So sizeTransition
will detect no change in size when the render happens, and the eventual change in size will happen locally within AnimatePresence
.
<motion.div sizeTransition>
<AnimatePresence>
<motion.div exit={{ opacity: 0 }} positionTransition />
<motion.div exit={{ opacity: 0 }} positionTransition />
<motion.div exit={{ opacity: 0 }} positionTransition />
</AnimatePresence>
</motion.div>
However I don't think this is the fault of the API proposal, more of a difficult consideration it should be developed with in mind. The way the motion
component is wired up should make this kind of communication quite possible.
@souporserious I'd have to see an example but I do think that kind of things would be better solved with a Magic Move-style animation which I think is a medium-term thing.
@rijk what does useMeasure do ? Would you mind sharing this hook. Trying to implement your Collapse component for a parent component that changes height based on how many children is added or removed.
@eric-personal it's a simple hook that measures the element. Something you probably won't need when using framer-motion
, as it supports animating to "auto" values (example).
Here's the code:
import React, { useState, useRef, useEffect } from 'react'
import ResizeObserver from 'resize-observer-polyfill'
export function useMeasure() {
const ref = useRef()
const [bounds, set] = useState({ left: 0, top: 0, width: 0, height: 0 })
const [ro] = useState(() => new ResizeObserver(([entry]) => set(entry.contentRect)))
useEffect(() => {
if (ref.current) ro.observe(ref.current)
return () => ro.disconnect()
}, [])
return [{ ref }, bounds]
}
@InventingWithMonster apologies if this is a dumb question, I'm not super well versed in the motion codebase yet. It's still not clear to me how sizeTransition
will handle nested animations? For example, with my Collapse
component I have the following tree:
<Collapse key="comments">
<Comment>
<Collapse key="replies">
<Reply />
<Reply />
</Collapse>
</Comment>
</Collapse>
Now, when a reply is added, the <Collapse key="replies">
will animate as intended. But the outer <Collapse key="comments">
will animate more slowly, as it responds to the change in size triggered by the inner animation.
The only ways to work around this are to programatically disable animations on one or the other, but you can only do this when you know which should animate. And I cannot know that, because at any time a new Reply or Comment could come in (from someone else).
Would this sizeTransition
proposal support this kind of nested use?
In that instance the parent animation wouldn’t be triggered because sizeTransition only measures changes in height as the result of a re-render. Which would be the one state change of the expanded comments.
Even so, this isn’t going to be an issue in any event as I’ve been prototyping and I’m going skip the first step of animating width and height. I saw the accordion in action after making it and it’s ok but the results from scale will be much smoother.
Sweet! Makes a lot of sense.
@InventingWithMonster
I'd have to see an example but I do think that kind of things would be better solved with a Magic Move-style animation which I think is a medium-term thing.
WOOOOOOOOOOOO 🔥
Some prior art you might like: https://github.com/aholachek/react-flip-toolkit
Has this been added? :)
Edit: was checking the code, seems halfway done.. If the scaling proves difficult, a simple width/height transition (combined with overflow: hidden
) would already be very valuable for auto animating containers.
Ah yeah I should have added a note. The WIP branch is now feature/layout-transition
Is this feature already added?
Maybe now?
This is all superseded by the layout
prop.
Overview
A
motion
prop for HTML elements,sizeTransition
, that enables content to animate between layout sizes.Mirroring
positionTransition
,sizeTransition
would animate components when they changewidth
/height
as the result of a re-render.Use-case(s)
Proposal
sizeTransition
can be defined either a boolean:or a
Transition
:Release 1:
width
/height
In the first iteration, it'll animate
width
andheight
from their previous to their new measured dimension.Release 2:
scaleX
/scaleY
By default, animating
width
andheight
will trigger layout. This can be slow and is better avoided for consistent 60fps animations. A more performant way would be animatescale
.This will necessitate either an inversion prop or component that can "undo" the scaling effect (so only the container visually scales).
Prop:
Component:
At parent
scale
s of < ~0.05 - ~0.1 we can fade this element out to prevent scaling toInfinity
.Is the presence of an inversion prop/component a pre-requisite to enable the
scale
performance animation? And/or the signal to usescale
overwidth
/height
?Release 3: Interactions with
positionTransition
(prospective)width
andheight
will naturally reflow-components every frame, but it isn't performant. Butscale
doesn't effect layout, so its surrounding components would "jump" into their new layout.It'd be good if at least sibling components could respond to each other's
scale
resizing with a co-ordinated animation. So for instance this code:As one component animate in size via
scale
, its surrounding components would animate in position usingx
/y
.Framer X considerations
As sizing isn't abstracted the way positional layout is to x/y, this should drop-in to
Frame
. Animating the position of surrounding components would require them to be in a DOM layout, ieStack
.