先来看下效果:
代码地址:ripple
canvas
绘制和css3
绘制我都实现过,最终还是css效果、性能更佳。
思路
这个其实十分简单,首先分两部分去理解:
css
动画:水波纹的效果是 (透明度1+缩放至0) -> (透明度0+缩放至1),可以结合用@keyframes
来实现。而波纹的大小交给js
动态设置,再把效果写成指定的calss
,最后js
动态输出该节点并设置对应class
即可;js
逻辑部分:首先生成一个节点,然后根据点击的目标节点大小来设置波纹的尺寸,也就是根据目标节点的宽高
取一个最大值作为波纹的大小,这里波纹永远都是正方形的。最后获取鼠标摁下
的坐标位置,设置对应波纹css-class
并输出该节点即可。至于什么时候销毁该节点,可以利用node.addEventListener("animationend", fn)
来监听处理。
css 部分
[ripple] {
position: relative;
overflow: hidden;
}
[ripple] .ripple {
position: absolute;
border-radius: 100%;
transform: scale(0);
pointer-events: none;
animation: ripple .4s ease-out;
}
@keyframes ripple {
to {
transform: scale(2);
opacity: 0;
}
}
js 部分
这里有个细节,就是水波纹效果结束之后被移除的节点我将他们放到一个数组里面,下次使用的时候直接从数组里面读取,之前做游戏的时候使用的一种节能模式,在网页上还没验证过,不知道有没有真正节省到性能的开销...没有的话直接删除该逻辑操作,可以尽可能的减少代码量。
/**
* 水波纹节点对象池
* @type {Array<HTMLElement>}
*/
const ripplePool = [];
/**
* 点击水波纹
* @param {TouchEvent | MouseEvent} event 点击事件
* @param {HTMLElement} target 点击目标
*/
function ripple(event, target) {
/**
* 水波纹动画节点
* @type {HTMLElement}
*/
let node;
// 从对象池里面拿取节点
if (ripplePool.length > 1) {
node = ripplePool.shift();
} else {
node = document.createElement("div");
node.className = "ripple";
}
/** 点击目标矩阵尺寸 */
const rect = target.getBoundingClientRect();
// 当前自定义颜色值,如果有绑定值则获取,没有默认就是白色透明
const color = target.getAttribute("ripple") || "rgba(255, 255, 255, .45)";
/** 波纹大小 */
let size = Math.max(rect.width, rect.height);
// 设置最大范围
if (size > 200) size = 200;
// 设置大小
node.style.height = node.style.width = size + "px";
// 设置波纹颜色值
node.style.backgroundColor = color;
// 这里必须输出节点后再设置位置,不然会有问题
target.appendChild(node);
const y = event.touches ? event.touches[0].clientY : event.clientY;
const x = event.touches ? event.touches[0].clientX : event.clientX;
const top = y - rect.top - (node.offsetHeight / 2);
const left = x - rect.left - (node.offsetWidth / 2);
// console.log(top, left);
node.style.top = top + "px";
node.style.left = left + "px";
function end() {
node.removeEventListener("animationend", end);
// console.log("动画结束", node);
target.removeChild(node);
ripplePool.push(node);
}
node.addEventListener("animationend", end);
}
使用方式
传统网页模式:
<button class="button" ripple>BUTTON-1</button>
<button class="button" ripple="rgba(0,0,0,0.1)">BUTTON-2 设置波纹颜色</button>
这里我使用事件代理去完成方法操作,因为节点有可能是动态生成的。
const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|OperaMini/i.test(navigator.userAgent);
/** 添加事件类型 */
const eventType = mobile ? "touchstart" : "mousedown";
document.body.addEventListener(eventType, function (e) {
/** 事件类型 */
const event = e || window.event || arguments.callee.caller.arguments[0];
/** 循环的次数 */
let loopCount = 3; // 这里的 3 次是布局的子节点层数,可根据布局层数增加减少
/**
* 定义目标变量
* @type {HTMLElement}
*/
let target = event.target;
// 循环 3 次由里向外查找目标节点
while (loopCount > 0 && target && target != document.body) {
loopCount--;
if (target.hasAttribute("ripple")) {
ripple(event, target);
break;
}
target = target.parentNode;
}
});
注意:用事件代理有个瑕疵,就是点击事件没有直接绑定在节点上灵敏,也就偶尔会出现点击没有触发对应事件的情况,页面节点较多,层级嵌套过深的时候尤为明显,所以根据实际情况来使用。
在传统模式下,也可以这样绑定事件,前提ripple
方法必须为全局模式。
<button class="button" ripple onmousedown="ripple(event, this)">BUTTON-1</button>
vue 项目使用自定义指令模式使用
<button class="button" v-ripple>BUTTON-1</button>
Vue.directive("ripple", {
inserted(el, binding) {
el.setAttribute("ripple", binding.value || "");
el.addEventListener(eventType, function (e) {
ripple(e, el);
});
}
})
完工!