theme: juejin highlight: atom-one-dark
点击反馈
不知道小伙伴们有没有注意过这样一个细节,有的应用按钮,链接,可交互的卡片点击起来十分有感觉,而有的却像是点在白纸上了一样,是什么造成了他们使用户有如此明显的感受区分呢?..
鼠标移入时的小手、鼠标点击时按钮下压弹起的动画、触屏应用点击时的屏幕震动,这些效果都给予用户一种是我的行为产生了这样的效果
的直觉,这些效果也被统称为点击反馈,虽然看似是应用中的细枝末节,但是只要稍微投入一点点心思,带来的用户体验提升是十分明显的
水波效果
这里作者为小伙伴们推荐一种作者最喜欢的点击反馈效果。当用户点击时,会以点击中心为圆心产生一个水波扩散的涟漪效果,适用各个场景,美观又不浮夸,关键是可以给用户带来很直观的反馈。
来看实现
首先这里基于Vue3自定义指令进行封装,Vue3的自定义指令跟Vue2相比变动不是很大,详细说明请看Vue3自定义指令。我们的目标是完成一个水波指令的基本原型,这里循序渐进展开。
定制一个水波纹默认样式
水波纹实际上就是通过用户点击的位置
生成一个小圆圈,并且尺寸逐渐扩大到整个被点击元素的一个过程,所以这里先制定一个水波基本的样式,并设置好过度动画,过度动画应该是一个先慢后快的一个过程,这里使用贝塞尔曲线定制,不清楚如何调试动画曲线的可以看这一篇文章
.my-ripple {
position: absolute;
top: 0;
left: 0;
z-index: 100;
border-radius: 50%;
background-color: currentColor;
opacity: 0;
transition: transform 0.2s cubic-bezier(0.68, 0.01, 0.62, 0.6), opacity 0.08s linear;
will-change: transform, opacity;
pointer-events: none;
}
计算水波纹的位置和直径
如果确定了水波的直径
、创建时的(x,y)
、过度动画结束时的(x,y)
,我们就可以通过transition
去渲染水波动画了,创建时的(x,y)
就是用户点击的位置,但是水波的直径
和过度动画结束时的(x,y)
怎么计算呢?我们的元素都是矩形,不论用户从元素的任意坐标进行点击,以矩形斜边作为直径的圆都可以完美的覆盖整个元素,斜边的计算我们利用小学数学知识
求两边平方和进行开方得到,下面是过度动画结束时的水波推演图
。
第一个箭头
: 期望得到的水波
第二个箭头
: 元素(0,0)点创建的水波
第三个箭头
: 元素(0,0)点创建的水波, 不带圆角效果
我们可以发现通过元素(0,0)点创建的水波进行一定偏移就可以得到我们想要的水波,由此我们可以推断出
动画结束时的水波的尺寸
= 圆的斜边
创建时的(x,y)
= 用户点击的位置
过度动画结束时的(x,y)
= 元素(0,0)点创建的水波进行x和y的偏移得到
function computeRippleStyles(element, event) {
const { top, left } = element.getBoundingClientRect()
const { clientWidth, clientHeight } = element
const radius = Math.sqrt(clientWidth ** 2 + clientHeight ** 2) / 2
const size = radius * 2
const localX = event.clientX - left
const localY = event.clientY - top
const centerX = (clientWidth - radius * 2) / 2
const centerY = (clientHeight - radius * 2) / 2
const x = localX - radius
const y = localY - radius
return { x, y, centerX, centerY, size }
}
鼠标按下时创建水波
然后我们需要在鼠标按下时创建水波,监听鼠标按下的事件,这里以pc端为例子,刚创建水波时使用transform
缩小到0.3
,这是作者尝试过相对合适的创建大小, 然后修改transform触发过度水波扩散动画,这里还加入了透明度的过度,可以使水波涟漪更有质感。
function createRipple(event) {
const container = this
const { x, y, centerX, centerY, size } = computeRippleStyles(container, event)
const ripple = document.createElement('div')
ripple.classList.add('my-ripple')
ripple.style.opacity = `0`
ripple.style.transform = `translate(${x}px, ${y}px) scale3d(.3, .3, .3)`
ripple.style.width = `${size}px`
ripple.style.height = `${size}px`
// 记录水波的创建时间
ripple.dataset.createdAt = String(performance.now())
const { position } = window.getComputedStyle(container)
container.style.overflow = 'hidden'
position === 'static' && (this.style.position = 'relative')
container.appendChild(ripple)
window.setTimeout(() => {
ripple.style.transform = `translate(${centerX}px, ${centerY}px) scale3d(1, 1, 1)`
ripple.style.opacity = `.25`
})
}
const VRipple = {
mounted(el) {
el.addEventListener('mousedown', createRipple)
}
}
鼠标抬起时销毁水波
当鼠标抬起时,只需要找到这个生成的水波节点修改透明度,再等到透明度修改动画结束之后将水波纹节点移除即可
function removeRipple() {
const container = this
const ripples = container.querySelectorAll('.my-ripple')
if (!ripples.length) {
return
}
const lastRipple = ripples[ripples.length - 1]
// 通过水波的创建时间计算出扩散动画还需要执行多久,确保每一个水波都完整的执行了扩散动画
const delay = 300 - performance.now() + Number(lastRipple.dataset.createdAt)
setTimeout(() => {
lastRipple.style.opacity = `0`
setTimeout(() => lastRipple.parentNode?.removeChild(lastRipple), 300)
}, delay)
}
const VRipple = {
mounted(el) {
el.addEventListener('mousedown', createRipple)
document.addEventListener('mouseup', removeRipple)
},
unmounted(el) {
el.removeEventListener('mousedown', createRipple)
document.removeEventListener('mouseup', removeRipple)
}
}
通过指令binding去扩展你的水波选项
你还可以通过binding去扩展你的指令,比如可以提供修改颜色,禁用状态等等选项,这里就不详细展开了。我们来看一下成果。
写在最后
到此为止我们就实现了一个简单的ripple指令,在我们的组件库中也有这样的指令,所以更完善的版本可以去看我们的源码。 先要感谢一下掘金社区,已经有一部分小伙伴开始pr一些代码到我们的仓库中来,我们也很高兴能和社区的小伙伴们去一起做这样一件事情,另外我们的组件库团队一直在募集爱好者来参与贡献,有兴趣的小伙伴欢迎加入讨论,加入方式就是直接去仓库提issue留邮箱,我们会第一时间处理,有没有兴趣都希望为我们点点star,关注一下我们,社区小伙伴的支持和兴趣是我们最大的动力。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 耗子君QAQ 原文链接:https://juejin.im/post/6968343900742221861