blogs's People
Forkers
renovate-botblogs's Issues
使用 JavaScript 实现按钮的涟漪效果
使用 JavaScript 实现按钮的涟漪效果
不知道你们有没有使用过 Material UI。这是一个 React UI 组件库,它实现了 Google 的 Material Design。
Material Design 设计规范中包含了很多关于点击的涟漪效果,类似于一块石头跌落水中所产生的波浪效果。
以下是效果图
速度放慢之后的效果:
本文就以 Material Design 中的涟漪效果作为目标,来使用原生的 JavaScript、CSS、HTML 来实现此效果。
分析
通过观察,我们可以发现点击的涟漪效果是在鼠标点击的点开始以一个正圆往外扩散。当圆形扩散到正好能将 Button 全部包围住的时候停止,在扩散的过程中颜色逐渐变浅直到消失,并且此效果可以叠加。
长按效果也是一个圆往外扩散,只不过是在长按结束之后,圆才会消失。
除了鼠标点击效果外,还有键盘焦点事件的效果。当使用键盘的 Tab 键切换到按钮上的时候,会有一个抖动的效果,类似于呼吸效果。
我们提炼出几个比较关键的点:
- 从鼠标点击的位置开始扩散;
- 是一个正圆形;
- 圆形扩散到正好能将 Button 全部包围住的时候停止;
- 长按效果;
- 效果叠加;
第一点,我们可以通过 JavaScript 计算当前鼠标的坐标信息;第三点,获取 Button 四个顶点的坐标信息,再选一个距离鼠标最远的点,以它们的距离作为半径来画一个圆;第五点,每一个效果都是一个 dom 元素,每点击一次就追加一个 dom 元素,在动画结束的时候,移除此 dom;
实现
创建一个 index.html 文件,包含以下内容;
<!-- index.html -->
<style>
@import 'button.css';
@import 'ripple.css';
</style>
<button class="button-root" id="ripple-example-button" type="button">
Button
<!-- 用来装涟漪效果的 DOM 的容器 -->
<span class="ripple-root"></span>
</button>
创建 button.css 和 ripple.css,分别是 Button 的基础样式和涟漪效果的样式。
/* button.css */
.button-root {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 16px;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.75;
min-width: 64px;
margin: 0;
border-radius: 4px;
border: 1px solid rgba(25, 118, 210, 0.5);
cursor: pointer;
box-sizing: border-box;
outline: none;
appearance: none;
user-select: none;
color: #1976d2;
background-color: transparent;
transition-property: background-color, color, box-shadow, border-color;
transition-duration: 0.25s;
}
.button-root:hover {
background-color: rgba(25, 118, 210, 0.04);
border: 1px solid #1976d2;
}
/* ripple.css */
@keyframes enterKeyframe {
0% {
transform: scale(0);
opacity: 0.1;
}
100% {
transform: scale(1);
opacity: 0.3;
}
}
@keyframes exitKeyframe {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes pulsateKeyframe {
0% {
transform: scale(0.9);
}
50% {
transform: scale(0.8);
}
100% {
transform: scale(0.9);
}
}
.ripple-root {
display: block;
position: absolute;
overflow: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
background-color: transparent;
z-index: 0;
border-radius: inherit;
}
.ripple-root > .ripple-child {
position: absolute;
display: block;
opacity: 0;
}
.ripple-root > .ripple-child.enter {
opacity: 0.3;
transform: scale(1);
animation: enterKeyframe 550ms ease-in-out;
}
.ripple-root > .ripple-child > .ripple-child-child {
opacity: 1;
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: currentColor;
}
.ripple-root > .ripple-child.exit > .ripple-child-child {
opacity: 0;
animation: exitKeyframe 550ms ease-in-out;
}
.ripple-root > .ripple-child.pulsate > .ripple-child-child {
position: absolute;
left: 0;
top: 0;
animation: pulsateKeyframe 2500ms ease-in-out 200ms infinite;
}
开始写 JavaScript。创建一个 ripple.apis.js 文件,编写 startRipple
函数。该函数首先要获取 Button 的位置信息和宽高。
// ripple.apis.js
export function startRipple(event) {
const { currentTarget: container } = event
const { left, top, width, height } = container.getBoundingClientRect()
}
接着计算开始扩散的位置。
// ripple.apis.js
export function startRipple(event) {
// ...
// 效果开始的坐标(相对于 Button)
let rippleX, rippleY
// 鼠标当前的坐标
let clientX = 0, clientY = 0
/**
* 涟漪效果是否从节点的中心扩散,否则从鼠标点击的位置开始扩散
* 使用 Tab 键移动焦点的时候,从节点的中心扩散
*/
let center = false
let isFocusVisible = false
if (container.matches(':focus-visible')) {
center = isFocusVisible = true
} else {
clientX = event.clientX
clientY = event.clientY
}
rippleX = center ? width / 2 : clientX - left
rippleY = center ? height / 2 : clientY - top
}
通过勾股定理,构造一个能正好包围当前元素的圆。
// ripple.apis.js
export function startRipple(event) {
// ...
// 从鼠标点击的中心位置,构造一个能正好包围当前元素的圆
const sizeX = Math.max(width - rippleX, rippleX) * 2
const sizeY = Math.max(height - rippleY, rippleY) * 2
const diagonal = Math.sqrt(sizeX ** 2 + sizeY ** 2)
}
再创建一个 createRippleChild
函数,用来创建涟漪效果的 DOM,并且使用一个全局变量来保存已经创建的 DOM。
// ripple.apis.js
const rippleChildren = []
/**
* 创建以下结构并返回:
* <span class="ripple-child enter">
* <span class="ripple-child-child"></span>
* </span>
*/
function createRippleChild(rect) {
const rippleChild = document.createElement('span')
rippleChild.classList.add('ripple-child', 'enter')
const rippleChildChild = document.createElement('span')
rippleChildChild.classList.add('ripple-child-child')
rippleChild.appendChild(rippleChildChild)
const { height, left, top, width } = rect
rippleChild.style.height = height
rippleChild.style.width = width
rippleChild.style.top = top
rippleChild.style.left = left
rippleChildren.push(rippleChild)
return rippleChild
}
回到 startRipple
函数,使用刚才创建的 createRippleChild
函数。
// ripple.apis.js
export function startRipple(event) {
// ...
const rippleChild = createRippleChild({
width: `${diagonal}px`,
height: `${diagonal}px`,
left: `${-diagonal / 2 + rippleX}px`,
top: `${-diagonal / 2 + rippleY}px`,
})
if (isFocusVisible) {
rippleChild.classList.add('pulsate')
}
const rippleRoot = container.querySelector(':scope > .ripple-root')
rippleRoot.appendChild(rippleChild)
}
完成了 startRipple
函数之后,我们再创建一个 stopRipple
函数。该函数中,从 rippleChildren
取出最早创建的 DOM,添加一个动画结束的监听事件,在动画结束的时候,删除该 DOM。
// ripple.apis.js
export function stopRipple() {
const rippleChild = rippleChildren.shift()
if (!rippleChild) return
rippleChild.addEventListener('animationend', (event) => {
if (event.animationName === 'exitKeyframe') {
rippleChild.remove()
}
})
rippleChild.classList.add('exit')
}
此时,我们已经完成了大部分的代码,接下来就是给 Button 绑定事件的时候了。在 index.html 文件中添加以下代码:
<!-- index.html -->
<style>
@import 'button.css';
@import 'ripple.css';
</style>
<script type="module">
import { startRipple, stopRipple } from 'ripple.apis.js'
const button = document.querySelector('#ripple-example-button')
button.addEventListener('mousedown', startRipple)
button.addEventListener('focus', startRipple)
button.addEventListener('mouseup', stopRipple)
button.addEventListener('mouseleave', stopRipple)
button.addEventListener('blur', stopRipple)
</script>
<button class="button-root" id="ripple-example-button" type="button">
Button
<!-- 用来装涟漪效果的 DOM 的容器 -->
<span class="ripple-root"></span>
</button>
我们完成了所有的功能!完整的代码在此仓库中。
也可以直接在 CodeSandbox 中编辑
Web Components 介绍
将 Props 的更新同步到 State
将 Props 的更新同步到 State
前言
在您的项目中,是否有很多将 Props 或 Context 的更新同步到 State 的情况?
我们有多种方法处理这种情况。
- 使用 useEffect 监听变化;
- 使用 useRef 跟踪上一个值的变化;
- 封装自定义 Hooks;
使用 useEffect( ❌ 最好不要)
您是不是经常在项目中看到这样的用法?
import { useState, useRef, useEffect } from 'react';
const Users = ({ users }) => {
const [internalUsers, setInternalUsers] = useState(users);
useEffect(() => {
setInternalUsers(users);
}, [users]);
return (
<ul>
{internalUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
这几行代码看起来非常的优雅:使用 useEffect 的第二个参数监听 users 的变化,然后重新设置 state,完美!✌️
可是您有没有想过,上面的代码会额外的触发了一次重渲染。🤔️
每当 users 变化时,直到整个应用加载完 DOM 后,useEffect 里的函数才会执行。由于我们调用了 setInternalUsers
,这又会导致我们的程序额外的重渲染一次。
为了给用户提供一个加载更快、体验更好的应用程序,请不要这样做。
使用 useRef( ✅ )
import { useState, useRef, useEffect } from 'react';
const Users = ({ users }) => {
const [internalUsers, setInternalUsers] = useState(users);
const previousUsersRef = useRef();
if (users !== previousUsersRef.current && users !== internalUsers) {
setInternalUsers(users);
}
useEffect(() => {
previousUsersRef.current = users
}, [users])
return (
<ul>
{internalUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
此写法用到了 useRef
来保存上一次更新的值,然后在渲染阶段判断更新前和更新后的值是否相同,如果不相同则设置 state。
从而在渲染前的阶段就完成了 state 的设置,避免了额外的渲染。
虽然比上一个写法多出了几行代码,但是在性能、用户体验(屏幕闪烁)上都优于第一种写法。
自定义 Hooks ( ✅ ✅ )
第二种写法虽然解决了我们的问题,但是其可维护性较低、耦合度较高。
这些逻辑全部写在了组件内部,后来的维护者们有可能因为修改其他问题,而使你写的功能失效或出现其他小问题;
或者您的组件不止监听一个 Props;
又或者其他组件也需要此功能;
对此,您应该将此逻辑抽离成自定义 Hooks。
// File: useStateListeningProp.ts
import { useState, useRef, useEffect } from "react";
type StateListeningPropResult<T> = [T, React.Dispatch<React.SetStateAction<T>>];
const useStateListeningProp = <T>(prop: T): StateListeningPropResult<T> => {
const [state, setState] = useState<T>(prop);
const previousPropRef = useRef<T>();
if (prop !== previousPropRef.current && prop !== state) {
setState(prop);
}
useEffect(() => {
previousPropRef.current = prop;
}, [prop]);
return [state, setState];
};
export default useStateListeningProp;
像这样,就能在您的组件中使用了。
// File: Users.tsx
import React from "react";
import useStateListeningProp from "./useStateListeningProp";
type User = { id: string; name: string };
const Users: React.FC<{ users: User[] }> = (props) => {
const { users: usersProp } = props;
const [users, setUsers] = useStateListeningProp(usersProp);
const addUser = (userName: User['name']) => {
setUsers((oldUsers) =>
oldUsers.concat({
id: Date.now().toString(),
name: userName
})
)
}
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code === "Enter") {
addUser(event.currentTarget.value)
event.currentTarget.value = "";
}
};
return (
<div>
<input
type="text"
placeholder="Enter Your Name"
onKeyPress={handleKeyPress}
/>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
本人开源了此自定义 hooks 的 npm 包 use-state-listening-prop,可以在您的项目中使用 npm install use-state-listening-prop
来安装并使用。
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.