✊不积跬步,无以至千里;不积小流,无以成江海
需求描述
要实现如图需求的动画,点击左上角图标召唤菜单栏,菜单栏滑动进入,主页面用黑色遮罩层遮挡。
使用react spring库实现遮罩层和主页层的遮罩动画。基础代码长这样。
const maskStyles = useSpring({opacity: visible ? 1 : 0})
const menuStyles = useSpring({
opacity: visible ? 1 : 0,
transform: visible ? 'translateX(0%)' : 'translateX(-100%)',
})
......
<animated.div fixed top-0 left-0 w="100%" h="100%" className="bg-black:75"
style={maskStyles} z="[calc(var(--z-menu)-1)]" onClick={onClickMask}/>
......
<animated.div fixed top-0 left-0 w="70vw" max-w-20em h-screen flex flex-col
style={menuStyles}/>
......
但是这样运行的时候会发现有个问题,鼠标根本没法点击左上角图触发发菜单栏。检查了一下发现是有一个fix的div把整个页面盖住了,相当于有一个透明的浮在所有的上面。于是尝试开始解决。
首先删掉遮罩层的div,发现页面能够正常点击,但由于删掉了遮盖层,导致召唤菜单栏之后背景为白色。如下图:
可以得到结论,说明由于遮罩层并没有位移,看不见的时候在页面正上方,那么通过给遮罩层加上位移可以解决这个问题吗?即代码中加入:
const maskStyles = useSpring({opacity: visible ? 1 : 0,
//加入位移代码
transform: visible ? 'translateX(0%)' : 'translateX(-100%)'})
const menuStyles = useSpring({
opacity: visible ? 1 : 0,
transform: visible ? 'translateX(0%)' : 'translateX(-100%)',
})
...
发现页面长这样:
虽然能动起来了,但是动画非常丑,是从左到右滑动的。黑色遮罩需要时从当前位置慢慢浮现,而不是滑动的。
解决思路
已知,我们期待黑色遮罩从背景中慢慢浮现,即不透明度从0到1,但当不透明度为0的时候,又会挡住点击图标导致无法触发操作。
解决思路:能否一开始将遮罩藏起来,等点击时再将遮罩召唤出来。
//添加状态
const [maskVisible, setMaskVisible] = useState(false)
const maskStyles = useSpring({opacity: visible ? 1 : 0})
const menuStyles = useSpring({
opacity: visible ? 1 : 0,
transform: visible ? 'translateX(0%)' : 'translateX(-100%)',
})
......
<animated.div fixed top-0 left-0 w="100%" h="100%" className="bg-black:75"
//给style同时添加状态
style={...maskStyles, visibility: (maskVisible ? 'visible' : 'hidden') as 'visible' | 'hidden'}
z="[calc(var(--z-menu)-1)]" onClick={onClickMask}/>
......
<animated.div fixed top-0 left-0 w="70vw" max-w-20em h-screen flex flex-col
style={menuStyles}/>
......
但用这样的方法会发现,遮罩还是不见了,如下图所示。
出现这个问题的原因是因为遮罩层一直被hidden。
(opacity是0时,对象看不见可以点击;visiblity是hidden时,对象看不见也无法点击;display是none时,对象看不见也无法点击;display: none 会将元素从文档流中删除,元素不会占用任何空间,也不会响应鼠标事件。visibility: hidden 会将元素隐藏,元素仍然会占用空间,并会响应鼠标事件。)
不能让遮罩层一直被hidden,它需要将遮罩在某个时刻被再被召唤回visible的状态,并让它们在两种状态间自由切换。
因此,
可以选择一些回调函数来表示状态的切换。
const [maskVisible, setMaskVisible] = useState(false)
const maskStyles = useSpring({opacity: visible ? 1 : 0,
//添加回调函数表示状态
//因为动画其实有两个:
//1.打开动画 0->1
//2.关闭动画 1->0
//所以我们需要依据当前的状态做判断,传入一个状态值命名为value
onStart: ({ value }) => {
//如果当前的opacity < 0.1,就让遮罩被看见
if (value.opacity < 0.1) { setMaskVisible(true) }
},
onRest: ({ value }) => {
//如果当前的opacity < 0.1,就让遮罩被关闭
if (value.opacity < 0.1) { setMaskVisible(false) }
}
})
const menuStyle中的 = useSpring({
opacity: visible ? 1 : 0,
......
加入状态切换后如图所示:
bug解决,得到了想要的动画~
更新一个bug
后面添加跳转路由之后,发现点击菜单中选项再后退时,遮罩又变回到白色,如图所示。(鼠标没录进去,鼠标步骤是1点击右上角 2点击自定义标签 3后退到菜单页面)
分析一下原因:在渲染这个路由页面时,useMenuStore中的visible初始值为false,即菜单栏第一次刷新页面的时候值为false,召唤菜单栏后visible值变为true,而后选择标签进入新路由再回退后,visible在内存中并没有被更新(无论被删除还是被更新),因此此时visible还是true。此时的visible = true,则const maskStyles = useSpring({opacity: visible ? 1 : 0})中的opacity 为 1;menuStyle中的opacity 为 1,所以他们两个同时存在在页面中。
但由于我们状态管理用的是zustand const [maskVisible, setMaskVisible] = useState(false)。只要页面刷新,zustand就会刷新。可以想象,初始值visible为false,点击后,visible变为true(此时当前的组件要全部重新渲染,因为页面刷新了),伴随着visible的更新,maskStyle/menuStyles都要跟着一起变化,同时进行动画,遮罩由白变黑。当跳转到全新的路由之后,管理maskStyle的组件已经不在内存中了,所以maskStyle的内部状态也不在内存中了,可visible还在,且没有更新。这时候冲突出现了,即visible还在内存中,表示状态的maskVisible却不在了,再回退时,这两个变量依旧保持这个状态,这会导致visible没有更新,因此一直是true放到了mask/menu的状态判断中。由上面可知opacity 都为 1,因此所有的组件都会保留在页面中。但由于maskVisible不存在,连锁反应导致 visibility: (maskVisible ? 'visible' : 'hidden') 管理visibleity会表示为hidden,因此黑色遮罩的style被hidden,所以最终呈现出现白色。
解决方式其实很简单,将状态管理的初始值设置的visible一致即可,即const [maskVisible, setMaskVisible] = useState(visible)。那么就会随着更新而改变表示状态的maskVisible。更改后的状态如图。虽然改动很小,但是背后的原理很难一下子就想明白。