当前位置: 首页 > news >正文

动感按钮:如何打造交互感十足的点击动画效果

你是否曾在点击按钮时,期待它不仅仅是一个简单的事件触发?在这篇文章中将深入探讨如何通过细节设计和前端技巧打造出那些“触手可及”的交互效果,让每一次点击都成为一种愉悦的体验提升网站或应用的整体互动感

目录

按钮波纹效果

按钮涟漪效果

封装通用组件

按钮波纹效果

思路引进:对于react开发者来说,常用的组件库就是antd,细心的朋友就会发现antd组件库是有给按钮添加相应的反馈效果的,如下图所示点击按钮有一个类似波纹的效果,今天我们就探讨一下该功能具体是如何实现的:

从f12控制台查看可以看出,当我们点击按钮之后,出现的波纹效果就是一个个的div元素,并且该元素是随着波纹的显示与隐藏同步发生的,当波纹消失的时候对于的div元素也会被销毁掉:

代码实现:按照上面的这种方式,接下来我们通过代码来实现上面的效果,这里我们借助react框架来实现吧,为了让点击的反馈效果更具有通用性,这里我们肯定是要将反馈功能封装成通用的组件的,如下所示:

import Wave from "./components/button/wave"const style: React.CSSProperties = {width: 100,height: 30,borderRadius: '10px 0 20px 0',margin: 100,borderColor: 'red',
}const App = () => {return (<><Wave><button style={style}>点击</button></Wave></>)
}export default App

接下来我们就需要开始正式的波纹组件的核心逻辑的编写了,在代码开始之前我们肯定是需要先梳理一下我们编写波纹组件有哪些注意点,这里我梳理要注意的地方有以下几点:

1)传递波纹组件的children是否是单一节点?因为是不能同时支持不同的组件有波纹效果

2)什么情况下不能触发波纹效果?是否是元素、常规元素、是否被禁用等需要考虑

3)如何插入子节点元素?如何确保组件能够通过ref获取实例并操作children元素的DOM? 

抱着这些问题,接下来我们通过代码来一一实现和解决:

const Wave: React.FC<{ children: React.ReactNode }> = (props) => {const { children } = props;const containerRef = useRef<HTMLDivElement>(null);if (Array.isArray(children)) {throw new Error('Wave component only accepts a single child element');}if (!React.isValidElement(children)) {return <>{children}</>}useEffect(() => {const node = containerRef.current; // 获取当前节点的引用if (!node || node.nodeType !== 1 || node.getAttribute('disabled')) return; // 确保节点存在且未禁用const handleClick = () => {const warpper = document.createElement('div') // 创建波纹元素warpper.style.position = 'absolute'warpper.style.top = '0'warpper.style.left = '0'node.insertBefore(warpper, node.firstChild) // 将波纹元素插入到子节点之前const root = ReactDOM.createRoot(warpper) // 创建React根节点root.render(<WaveEffect target={node}/>) // 渲染波纹元素}node.addEventListener('click', handleClick, true); // 捕获阶段监听点击事件return () => {node.removeEventListener('click', handleClick, true); // 移除事件监听器}}, [])// @ts-ignorereturn React.cloneElement(children, { ref: containerRef })
}

通过上面的的代码我们实现了点击按钮之后就会不断的生成对应的子元素的dom节点,接下来我们就需要给这些子节点添加样式等其他逻辑功能了,老样子还是思考一下代码开始前的准备工作:

1)波纹颜色验证?例如按钮颜色为白色就无需波纹等效果

2)样式信息同步?例如按钮有无定位的效果,来判断当前波纹位置等样式信息

3)按钮尺寸变化?例如按钮尺寸发生变化的时候波纹效果能否同步发生更新

4)波纹元素移除?波纹效果已经结束之后,插入的子dom元素能否被移除

抱着这些问题,接下来我们通过代码来一一实现和解决:

const WaveEffect: React.FC<{ target: HTMLElement }> = ({ target }) => {const divRef = useRef<HTMLDivElement>(null);const [style, setStyle] = useState<React.CSSProperties>({});// 颜色验证和获取合并const getWaveColor = (node: HTMLElement, defaultColor='blue') => {const { borderColor, backgroundColor } = getComputedStyle(node);const isValid = (color: string) => color && !['transparent', '#fff', '#ffffff', 'rgba(255, 255, 255, 1)', 'rgb(255, 255, 255)'].includes(color);return isValid(borderColor) ? borderColor : isValid(backgroundColor) ? backgroundColor : defaultColor;};// 同步样式信息const syncStyle = () => {const { position, borderTopWidth, borderLeftWidth,borderTopLeftRadius, borderTopRightRadius,borderBottomRightRadius, borderBottomLeftRadius} = getComputedStyle(target);// 判断是否为静态定位,如果是则使用偏移量作为top和left值const isStatic = !position || position === 'static';setStyle({width: target.offsetWidth,height: target.offsetHeight,top: isStatic ? target.offsetTop : -parseFloat(borderTopWidth),left: isStatic ? target.offsetLeft : -parseFloat(borderLeftWidth),color: getWaveColor(target),borderRadius: [borderTopLeftRadius, borderTopRightRadius,borderBottomRightRadius, borderBottomLeftRadius].join(' ')});};useEffect(() => {// 监听尺寸变化,同步样式信息const ob = new ResizeObserver(syncStyle);ob.observe(target);// 延迟执行,确保样式已经同步const id = requestAnimationFrame(() => {divRef.current?.classList.add('wave-hide');syncStyle();});// 清理函数return () => {ob.disconnect();cancelAnimationFrame(id);};}, [target]);useEffect(() => {// 监听过渡结束,移除波纹元素const callback = () => divRef.current?.parentElement?.remove();const currentRef = divRef.current;currentRef?.addEventListener('transitionend', callback);return () => currentRef?.removeEventListener('transitionend', callback);}, []);return <div style={style} ref={divRef} className='wave' />;
};

接下来就是对类名wave和wave-hide进行相应的样式编写了,如下所示:

.wave {box-shadow: 0 0 0 5px currentColor;position: absolute;background-color: transparent;pointer-events: none;box-sizing: border-box;opacity: 0.2;transition: box-shadow 2s ease, opacity 2s ease;
}.wave-hide {opacity: 0;box-shadow: 0 0 0 2px currentColor;
}

最终实现的效果如下所示,效果还不错:

按钮涟漪效果

思路引进:除了react开发来常用的组件库就是,一些其他的组件库如varlet也是有比较有特点的按钮点击的反馈效果的,如下图所示点击按钮有一个类似涟漪的效果,今天我们就探讨一下该功能具体是如何实现的:

从f12控制台查看可以看出,当我们点击按钮之后,出现的涟漪效果就是一个div元素,并且该元素是随着涟漪的显示与隐藏同步发生的,当涟漪消失的时候对于的div元素也会被销毁掉:

代码实现:按照上面的这种方式,接下来我们通过代码来实现上面的效果,这里我们借助react框架来实现吧,实现思路就是点击按钮的时候会在点击的地方生成一个小圆圆,然后这个圆圆圈的动画是逐渐放大,不透明度是越来越低然后就模拟成了波纹,最后再把这个按钮剩余的区域隐藏掉即可,实现的代码如下所示:

// 确保容器样式正确
node.style.position = node.style.position || 'relative';
node.style.overflow = 'hidden';// 定义小波样式
const color = 'rgba(255, 255, 255, 0.3)';
const duration = 1000;
const rect = node.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;const ripple = document.createElement('div');// 应用内联样式
Object.assign(ripple.style, {position: 'absolute',width: '2px',height: '2px',left: `${x}px`,top: `${y}px`,borderRadius: '50%',backgroundColor: color,transform: 'scale(0)',opacity: '1',pointerEvents: 'none',transition: `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`
});node.appendChild(ripple);// 触发动画
requestAnimationFrame(() => {const maxDim = Math.max(node.offsetWidth, node.offsetHeight);ripple.style.transform = `scale(${maxDim * 2})`;ripple.style.opacity = '0';
});// 动画结束后移除元素
setTimeout(() => ripple.remove(), duration);

实现的效果如下所示:

封装通用组件 

因为这里我们封装的是一个通用组件,所以这里我们可以将上面的按钮效果合并在一起,并通过传递props来确保用户当前要选择哪种按钮反馈效果,如果后期还需加其他按钮效果的话也可以继续在代码上添加,这里定义的props内容如下:

import Wave from "./components/button/wave"const style: React.CSSProperties = {width: 200,height: 100,borderRadius: '10px 0 20px 0',margin: 100,backgroundColor: '#1677ff',outline: 'none',border: 'none',
}const App = () => {return (<><Wave feedback="ripple"><button style={style}>ripple点击</button></Wave><Wave feedback="wavelet"><button style={{ ...style, backgroundColor: 'red' }}>wavelet点击</button></Wave><Wave feedback="none"><button style={style}>none点击</button></Wave><Wave><button style={style}>正常点击</button></Wave></>)
}export default App

这里给出四种情况的点击效果:

为了简化代码,这里我将不再设置class类名而是直接给添加的子元素设置样式style,也就是说代码样式全用js来实现,不再使用单独的css文件来处理样式,也就是说封装的组件就是一个单独的tsx文件,完整代码如下所示:

import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom/client';interface WaveProps {children: React.ReactNode;feedback?: 'ripple' | 'wavelet' | 'none' | undefined;
}// 定义波纹基础样式
const waveBaseStyles: React.CSSProperties = {boxShadow: '0 0 0 5px currentColor',position: 'absolute',backgroundColor: 'transparent',pointerEvents: 'none',boxSizing: 'border-box',opacity: 0.2,transition: 'box-shadow 2s ease, opacity 2s ease'
};// 定义波纹隐藏状态样式
const waveHideStyles: React.CSSProperties = {...waveBaseStyles,opacity: 0,boxShadow: '0 0 0 2px currentColor'
};const WaveEffect: React.FC<{ target: HTMLElement }> = ({ target }) => {const divRef = useRef<HTMLDivElement>(null);const [style, setStyle] = useState<React.CSSProperties>({});const [isHidden, setIsHidden] = useState(false);// 颜色验证和获取合并const getWaveColor = (node: HTMLElement, defaultColor = 'blue') => {const { borderColor, backgroundColor } = getComputedStyle(node);const isValid = (color: string) => color && !['transparent', '#fff', '#ffffff', 'rgba(255, 255, 255, 1)', 'rgb(255, 255, 255)', // 白色系列'#000', '#000000', 'rgba(0, 0, 0, 1)', 'rgb(0, 0, 0)' // 黑色系列].includes(color); return isValid(backgroundColor) ? backgroundColor : isValid(borderColor) ? borderColor : defaultColor;};// 同步样式信息const syncStyle = () => {const {position, borderTopWidth, borderLeftWidth,borderTopLeftRadius, borderTopRightRadius,borderBottomRightRadius, borderBottomLeftRadius} = getComputedStyle(target);// 判断是否为静态定位,如果是则使用偏移量作为top和left值const isStatic = !position || position === 'static';setStyle({width: target.offsetWidth,height: target.offsetHeight,top: isStatic ? target.offsetTop : -parseFloat(borderTopWidth),left: isStatic ? target.offsetLeft : -parseFloat(borderLeftWidth),color: getWaveColor(target),borderRadius: [borderTopLeftRadius, borderTopRightRadius,borderBottomRightRadius, borderBottomLeftRadius].join(' ')});};useEffect(() => {// 监听尺寸变化,同步样式信息const ob = new ResizeObserver(syncStyle);ob.observe(target);// 延迟执行,确保样式已经同步const id = requestAnimationFrame(() => {setIsHidden(true);syncStyle();});// 清理函数return () => {ob.disconnect();cancelAnimationFrame(id);};}, [target]);useEffect(() => {// 监听过渡结束,移除波纹元素const callback = () => divRef.current?.parentElement?.remove();const currentRef = divRef.current;currentRef?.addEventListener('transitionend', callback);return () => currentRef?.removeEventListener('transitionend', callback);}, []);// 合并基础样式、动态样式和隐藏状态样式const combinedStyles = {...waveBaseStyles,...style,...(isHidden ? waveHideStyles : {})};return <div style={combinedStyles} ref={divRef} />;
};const Wave: React.FC<WaveProps> = (props) => {const { children, feedback } = props;const containerRef = useRef<HTMLDivElement>(null);if (Array.isArray(children)) {throw new Error('Wave component only accepts a single child element');}if (!React.isValidElement(children)) {return <>{children}</>;}useEffect(() => {const node = containerRef.current;if (!node || node.nodeType !== 1 || node.getAttribute('disabled')) return;if (feedback === 'none') return;const handleClick = (e: MouseEvent) => {if (feedback === 'wavelet') {// 确保容器样式正确node.style.position = node.style.position || 'relative';node.style.overflow = 'hidden';// 定义小波样式const color = 'rgba(255, 255, 255, 0.3)';const duration = 1000;const rect = node.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;const ripple = document.createElement('div');// 应用内联样式Object.assign(ripple.style, {position: 'absolute',width: '2px',height: '2px',left: `${x}px`,top: `${y}px`,borderRadius: '50%',backgroundColor: color,transform: 'scale(0)',opacity: '1',pointerEvents: 'none',transition: `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`});node.appendChild(ripple);// 触发动画requestAnimationFrame(() => {const maxDim = Math.max(node.offsetWidth, node.offsetHeight);ripple.style.transform = `scale(${maxDim * 2})`;ripple.style.opacity = '0';});// 动画结束后移除元素setTimeout(() => ripple.remove(), duration);}if (feedback === 'ripple' || feedback === undefined) {const warpper = document.createElement('div');Object.assign(warpper.style, {position: 'absolute',top: '0',left: '0'});node.insertBefore(warpper, node.firstChild);const root = ReactDOM.createRoot(warpper);root.render(<WaveEffect target={node} />);}};node.addEventListener('click', handleClick, true);return () => {node.removeEventListener('click', handleClick, true);};}, [feedback]);// 克隆子元素并添加refreturn React.cloneElement(children as React.ReactElement, { ref: containerRef as React.RefObject<HTMLDivElement> });
};export default Wave;

最终实现的效果如下所示:

最后总结

从效果上看也是实现了antd的一个按钮反馈效果,而且我设置的通用组件也是沿用了antd的效果并且组件默认也是波纹效果,如果想选择其他反馈效果或者取消反馈效果的话,也是通过我设置的feedback属性来进行控制,后期如果想添加新的反馈效果也可以继续添加,ok,今天的分享就到这里,感兴趣的朋友可以点个关注,我们下期再见!

http://www.dtcms.com/a/312669.html

相关文章:

  • 【1】WPF界面开发入门—— 图书馆程序:登录界面设计
  • 基于图像识别与分类的中国蛇类识别系统
  • [硬件电路-151]:数字电路 - 模拟电路与数字电路的本质
  • 【数据结构】二叉树的顺序结构实现
  • SQL注入SQLi-LABS 靶场less31-38详细通关攻略
  • 托福阅读38-3
  • 使用AssemblyAI将音频数据转换成文本
  • AI生成图片工具分享!
  • Linux的权限概念
  • 关于Web前端安全之XSS攻击防御增强方法
  • 【视频内容创作】PR的关键帧动画
  • 机器学习第三课之逻辑回归(三)LogisticRegression
  • Python-初学openCV——图像预处理(五)
  • 团队独立思考的力量
  • 论文阅读:DMD蒸馏 | One-step Diffusion with Distribution Matching Distillation
  • Python 动态属性和特性(定义一个特性工厂函数)
  • 「源力觉醒 创作者计划」_文心大模型4.5系列开源模型, 从一行代码到一个生态:聊聊开源战略那些事儿,顺便扯扯文心大模型 4.5 的使用心得
  • zookeeper分布式锁 -- 读锁和写锁实现方式
  • gpu instancer crowd 使用自定义材质并且只修改单个物体的材质参数
  • 【领域热点】【Vue】Vue 与 WebAssembly:前端性能优化的黄金搭档
  • 渗透高级-----应急响应
  • 机器翻译的分类:规则式、统计式、神经式MT的核心区别
  • 新电脑上GitHub推送失败?全面排查与解决指南
  • 第三章-提示词-高级:开启智能交互新境界(13/36)
  • Flutter Dart类的使用
  • WebMvc自动配置流程讲解
  • 【MySQL】MySQL的安全风险与安装安全风险
  • GraphRAG:基于知识图谱的检索增强生成技术解析
  • OSPF HCIP
  • RAG 中常见的文本分块(chunk)方法及实战代码示例