使用framer-motion构建收起展开过渡动画的React通用组件

1,757 阅读2分钟

前言

在前端开发过程中,经常会遇到将一块区域收起展开的需求。如果只是简单的显示隐藏会很突兀,用户体验很差。所以需要一个过渡动画使之平滑。framer-motion就是一个不错的选择。链接:www.framer.com/motion/

思路:

使用motion.div做一个动画壳子并定义过渡效果和触发时机。

关联到需要收起展开的内容并使之通用

安装并导入framer-motion,使用<motion.div>作为动画壳子

npm i framer-motion

import { motion } from 'framer-motion'

<motion.div>
  <div>需要收起展开的内容</div>
</motion.div>

过渡效果

展开时,开始不显示,结束时完全显示,持续时间约0.3秒,过程中高度不断增加。

收起时,开始完全显示,结束不显示,持续时间约0.3秒,过程中高度不断减少。

1.初始状态: initial={{ height: 0 }}

2.动画:当height属性变化时执行动画,slotHeight是需要收起展开的内容的高度。

animate={{ height: slotHeight }}

3.过渡:(动画持续0.3秒) transition={{ duration: 0.3 }}

4.退出(收起) exit={{ height: 0 }}

5.壳子的效果虽然定义好了,但里面的内容始终处于显示的状态。所以增加一条样式。

style={{ overflow: 'hidden' }}

触发

触发收起展开,点击收起展开时,给isShow取反。

const [isShow,setIsShow]=useState(false)

获得收起展开的内容高度并基于isShow赋值

const slotRef = useRef(null)
const [slotHeight, setSlotHeight] = useState(0)

useEffect(() => {
  slotRef?.current &&  setSlotHeight(slotRef?.current?.offsetHeight || 0)
}, [isShow])
  
<div ref="slotRef">需要收起展开的内容</div>

退出动画需要特殊处理,AnimatePresence可以使组件销毁时,仍有动画效果。

import { AnimatePresence } from 'framer-motion'
<AnimatePresence>
  isShow && (<motion.div> <div>需要收起展开的内容</div></motion.div>)}
</AnimatePresence>

通用

把slotRef和slotHeight写进custom hook

import { useEffect, useRef, useState } from 'react'
export const useCollapseExpand = (isShow) => {
  const slotRef = useRef(null)
  const [slotHeight, setSlotHeight] = useState(0)
  useEffect(() => {
    slotRef?.current &&  setSlotHeight(slotRef?.current?.offsetHeight || 0)
  }, [isShow])
  return { slotRef, slotHeight }
}

把需要收起展开的内容抽象成children

import { AnimatePresence, motion } from 'framer-motion'
import { cloneElement } from 'react'
import { useCollapseExpand } from './use-collapse-expand'

function CollapseExpand({ isShow, children }) {
  const { slotRef, slotHeight } = useCollapseExpand(isShow)
  return (
    <AnimatePresence>
      {isShow && (
        <motion.div
          style={{overflow:'hidden'}}
          key="modal"
          initial={{height:0}}
          animate={{height:slotHeight}}
          exit={{height:0}}
          transition={{duration:0.3}}
        >
          {cloneElement(children, { ref: slotRef })}
        </motion.div>
      )}
    </AnimatePresence>
  )
}
export default CollapseExpand

传入ref的组件需要使用forwordRef

import { forwardRef } from 'react'
const Item = forwardRef(({ item }, ref) => {
  return (
    <section ref={ref}>
      {...item}
    </section>
  )
})

export default Item

使用时只需要把需要收起展开的内容包起来,关心的变量也只有一个收起展开isShow

<CollapseExpand isShow={isRealShow}>
   <Item item={item} />
</CollapseExpand>