我把Framer-motion的动画(动效)实现逻辑分为三种:

👉直接触发动效

👉条件触发动效

👉动态控制动效

直接触发动效👈就是我们已经讲过的,直接通过animate属性,传入一个状态名称或者变化信息对象这样的形式。这种形式的特点就是页面加载之后马上会触发,会启动。

#

直接触发动效


const [isClicked, setClicked] = useState(false)

<motion.div
	animate={ isClicked ?  {x:100}: {x:0}}
	onClick={()=>{
		setClicked(!isClicked)
	}}
>

</motion.div>

但是很多时候,我们需要动效是在特定情况下才触发。比如像下面这种按钮,我们需要点击的时候,才会有上面那个方块这种旋转切换的效果。

像这种需要某个行为,或者需要某个条件,比如某个元素只有到了窗口区域,才会执行出现的动画,我都把它们统一称为👉条件触发的动画

一般这种动画,很多时候都是在两个状态之间进行变化。有的是一种状态,变化到另一个状态,有的是两种状态之间可以来回切换。

那像这样的动画(动效),通过motion组件该如何实现呢?

最常规的实现方式,其实就是使用useState。设置一个数据,然后通过某个行为或者条件来改变这个数据。通过这个数据的改变来改变元素的状态。

这种方式也是一般React组件中很常见的让元素产生变化的方式,❗️下面代码是使用animate属性,但是如果是一般React元素需要使用style属性,同时配合设置transition属性才有过渡效果。

const [isClicked, setClicked] = useState(false)
const states = {
	begin:{x:0},
	end:{x:100}
}
<motion.div
	variants={states}
	animate={ isClicked ?  'begin': 'end'}
	onClick={()=>{
		setClicked(!isClicked)
	}}
>

</motion.div>

而在motion组件中,如果该组件variants属性接受了多状态对象,那么还可以在animate属性里使用状态的名称。

❗️这里要注意,如果你在使用motion组件时,还是想通过使用style属性传入的对象中的数据改变来实现动画效果,在你没有在style中设置transition属性的时候,是不会有使用animate那样的过渡效果的。

如果你要非要这样做,那么要在style中写上transition属性,这样才会有过渡变化效果。因为我们如果用style属性中的数据变化来实现动画效果,那是一种类似于原生CSSJS的实现,它需要transition属性的配合,这个也是一般React组件的方式,而motion组件上的animate属性,本身就是经过加工处理的,它内部已经帮我们设置了相应的transition过渡属性了。


const [isClicked, setClicked] = useState(false)

<motion.div
	style={{
         x: isClicked ?  100:0
        }}
	onClick={()=>{
		setClicked(!isClicked)
	}}
>

</motion.div>

1.基本方式

2.特有方式

我们来看看,另一种条件触发动效的方式。这个方式需要用到motion组件给我们提供的一些属性,这些属性分别是:

所有这些都是while开头的,这些属性都能让元素在这些条件时进行状态变化,当这些条件不满足了,就会变回原来的状态。

比如我们来看看最常用的whileHoverwhileTap

whileHover

鼠标悬浮时

whileTap

按住时

whileDrag

拖动时

whileFocus

获取焦点时

whileInView

出现在视窗时

这些属性,跟animate一样,既可以传动画信息对象,也可以传状态的名字。

这几个while开头的实现条件触发的属性,除了whileInView其他的使用逻辑都非常简单。whileInView还可以和其他属性进行配合设置,所以使用起来会稍微复杂一点,关于它我们会在以后在和滚动相关的交互动效中再进行详细介绍。

#

条件触发动效

接下来,就要来介绍第三种了,我称为动态控制动效。

要实现这个动态控制,我们需要使用Framer-motion提供的一个自定义Hook,叫做useAnimationControls

通过它,我们不仅能很灵活的控制动画什么时候触发,而且可以很方便地控制元素动画暂停、变化状态、以及实现分段动画等等复杂的效果。

那么,❓我们该如何去写代码呢?

首先我们要引入useAnimationControls,然后在组件中执行它,它会返回一个animationControls的实例,我们后面简称它为动画控制实例

接着,我们要把这个实例赋值给我们想要控制动画效果的元素的animate属性上,对的,你没看错,就是那个用来传动画信息对象或者动画状态名称的animate属性,一旦这样设置,那么这个动画控制实例就和这个元素绑定了。

接着,你就可以通过调用这个实例上的方法,来控制这个元素产生相应的变化了。不管你通过什么方式、哪个元素、什么行为来使用该实例上的方法,都能让这个绑定的元素发生动态变化。

❗️注意,这种绑定是可以一对多的,如果多个元素的animate属性都传入同一个动画控制实例,那么这个实例就可以控制所有这些元素,让他们一起被这个实例操控了。


import {useAnimationControls,motion} from 'framer-motion'

function Component() {
  const controls = useAnimationControls()
  
  return <motion.div 
		animate={controls} 
		onClick={()=>{
			controls.start({ scale: 2 })
		}}
	>
	<motion.div/>
}

上面这个案例就是利用useAnimationControls的实例来实现不同按钮控制同一个元素发生不同的变化,利用的就是不同的按钮都控制同一个动画控制实例来让该元素发生变化。

通过useAnimationControls执行产生的动画控制实例上有三个方法:

.start()

启动

.stop()

停止

.set()

直接切换

start方法可以接受三种类型的数据,动画信息对象或动画状态名称或者是函数。

set方法可以接受动画信息对象或者动画状态名称。

stop方法不需要传递任何参数。

这三个方法使用起来都非常简单,只是要注意几个细节:

  1. 当元素通过实例的start方法开始变化后,在该动画效果没有结束前,如果再使用start方法是会产生问题的,你可以在上面案例中不断快速点击start按钮看看会有什么效果。所以,如果是有可能会产生重复触发的情况,要做好一定的保护措施。

  2. 当元素通过实例的start方法启动变化后,在该动画效果没有结束前,如果使用实例的stop方法让元素停止,那么元素会在当下的状态下停止,如果这个时候再通过start方法启动,那么并不是继续未完成的部分,而是开始一个新的动画过程,因此就算和stop之前的动画目标是一样的,但是整个动画时长会从0开始重新计算,所以不要简单的理解为是恢复或者继续之前未完成的变化过程,而是开始一个新的动画过程。

  3. 当元素在动画过程中,调用set方法是无效的,只有动画结束,调用set才会有用。

motion组件定义了两个动画相关的事件,一个是onAnimationStart,另一个是onAnimationComplete,一个是启动时,一个是结束时,这两个事件只要是通过animate属性来实现的变化效果,那么在相应的时机就会触发设置的函数。

实现顺序动画

我们可以通过使用async await关键字,搭配动画控制实例的start方法来实现更复杂一些的顺序动画,因为start方法本质上执行返回的是一个Promise对象,通过async await我们可以让动画执行依次进行,我们可以看看下面这个案例:

动态数据获取

const controls = useAnimationControls()

useEffect(() => {
  controls.start(i => ({
    opacity: 0,
    x: 100,
    transition: { delay: i * 0.3 },
  }))
}, [])

return (
  <ul>
    <motion.li custom={0} animate={controls} />
    <motion.li custom={1} animate={controls} />
    <motion.li custom={2} animate={controls} />
  </ul>
)

如果在start方法中传入函数,是可以设置一个参数的,这个方式和使用variants属性时给传入的多状态对象属性设置函数的方式是基本一致的,函数参数也是只有一个,这个参数也是可以用来获取元素上的custom属性上设置的值。

#

动态控制动效

// 基本的写法  一般情况下可以不需要返回 因为基本都是通过事件回调来触发
const sequence = async () => {
  await menuControls.start({ x: 0 })
  return await itemControls.start({ opacity: 1 })
}

这应该算是一个bug,当你要进行元素的颜色相关的变化,不管是背景色也好还是其他的颜色,在设置变化的信息的,颜色值不要设置成'white''pink'这样的颜色名称的值,一定要设置成其他的格式,比如HEX,不管是初始值还是目标值,都不能设置成颜色名称的值,不然的话,颜色变化不会有过渡效果,只会硬切。

  1. 颜色变化设置的色值不能是颜色名称

#

特别提醒

上一篇