本文通過一個具體的動畫需求,來講解一下js中animationend事件使用
前言
- 平常,我們代碼中,會做一些事件的監聽
- 比如點擊事件、右鍵事件、滾動事件等
- 實際上,js中還提供了動畫事件的相關api
- 以便於我們能夠應對產品奇怪的需求
需求描述
- 假設我們有一個輸入框
- 當用户輸入錯誤的數據的時候
- 進行校驗不通過的動畫播放提示(讓輸入框左右抖動三次)
- 在動畫播放期間,不允許用户再次點擊提交按鈕
- 必須要動畫播放完畢後,才允許用户點擊按鈕
- 且在動畫播放完畢以後,清除掉用户輸入的錯誤的數據
- 如下效果圖:
效果圖
- 所以,我們需要通過動畫事件進行相關的邏輯控制
動畫事件知識點
如下:三個動畫相關事件——用於鏈式動畫控制、狀態更新操作等交互邏輯
animationstart:動畫開始時觸發animationend:動畫完成時觸發animationiteration:動畫每完成一次循環時觸發
const element = document.getElementById('animatedElement');
element.addEventListener('animationstart', () => {
console.log('動畫開始了');
});
element.addEventListener('animationend', () => {
console.log('動畫結束了');
});
element.addEventListener('animationiteration', () => {
console.log('動畫完成一次迭代');
});
注意: 動畫事件可以獲取事件對象,包含動畫名稱、屬性等信息
此外,還有動畫過度事件,比如transitionend 過渡效果完成時觸發,這裏不贅述了
const element = document.getElementById('transitionElement');
element.addEventListener('transitionend', (event) => {
console.log(`過渡完成: ${event.propertyName}`);
});
代碼邏輯
在頁面加載完成後的useEffect中,去綁定對應動畫事件
- 在頁面加載完成後的useEffect中,去綁定對應動畫事件
- 當動畫開始的時候,去禁用按鈕
setIsDisabledBtn(true); - 當動畫結束的時候,
setVal('') // 清空輸入框,setErr('') // 清空錯誤提示,setIsDisabledBtn(false); // 取消按鈕禁用
useEffect(() => {
const handleAnimationStart = () => {
console.log('動畫開始');
setIsDisabledBtn(true);
}
const handleAnimationIteration = () => {
console.log('動畫迭代');
}
const handleAnimationEnd = () => {
console.log('動畫結束');
setVal('') // 清空輸入框
setErr('') // 清空錯誤提示
setIsDisabledBtn(false); // 取消按鈕禁用
}
inputContainerRef.current?.addEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.addEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.addEventListener('animationend', handleAnimationEnd)
return () => {
inputContainerRef.current?.removeEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.removeEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.removeEventListener('animationend', handleAnimationEnd)
}
}, [])
html結構
使用classNames庫,去更好的控制類名的動態切換
<div>
<h2>動畫事件的應用場景舉例</h2>
<h3>請輸入文字,至少五個字符</h3>
<div className={classNames('inputContainer', { 'errorInput': err })} ref={inputContainerRef}>
<Input status={err} value={val} ref={inputRef} onChange={onChange} placeholder="請輸入" style={{ width: 240 }} />
{err && <div className='errorText'>請輸入至少五個字符</div>}
</div>
<br />
<Button onClick={clickBtn} style={{ marginTop: '12px' }} type="primary" disabled={isDisabledBtn}>提交</Button>
</div>
css動畫控制
通過transform左右控制,再搭配cubic-bezier曲線,讓動畫更加有節奏
.inputContainer {
padding: 0 12px;
}
.errorText {
color: red;
font-size: 12px;
margin-top: 4px;
}
.errorInput {
/* 動畫執行時間為1.5s,執行3次,動畫函數為cubic-bezier(0.36, 0.07, 0.19, 0.97) */
animation: shakeLeftRight 1.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) 3;
}
@keyframes shakeLeftRight {
0% {
transform: translateX(0);
}
10% {
transform: translateX(-20px);
}
20% {
transform: translateX(20px);
}
30% {
transform: translateX(-15px);
}
40% {
transform: translateX(15px);
}
50% {
transform: translateX(-10px);
}
60% {
transform: translateX(10px);
}
70% {
transform: translateX(-4px);
}
80% {
transform: translateX(4px);
}
90% {
transform: translateX(-2px);
}
100% {
transform: translateX(0);
}
}
完整React代碼
import React, { useRef, useEffect, useState } from 'react'
import { Input, Button, InputRef } from 'antd';
import './AnimationEvent.css'
import classNames from 'classnames'
const AnimationEvent: React.FC = () => {
const [val, setVal] = useState<string>('初始值');
const [err, setErr] = useState<'' | 'warning' | 'error' | undefined>('');
const [isDisabledBtn, setIsDisabledBtn] = useState<boolean>(false);
const inputRef = useRef<InputRef>(null);
const inputContainerRef = useRef<HTMLDivElement>(null);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
setVal(target.value);
}
useEffect(() => {
const handleAnimationStart = () => {
console.log('動畫開始');
setIsDisabledBtn(true);
}
const handleAnimationIteration = () => {
console.log('動畫迭代');
}
const handleAnimationEnd = () => {
console.log('動畫結束');
setVal('') // 清空輸入框
setErr('') // 清空錯誤提示
setIsDisabledBtn(false); // 取消按鈕禁用
}
inputContainerRef.current?.addEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.addEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.addEventListener('animationend', handleAnimationEnd)
return () => {
inputContainerRef.current?.removeEventListener('animationstart', handleAnimationStart)
inputContainerRef.current?.removeEventListener('animationiteration', handleAnimationIteration)
inputContainerRef.current?.removeEventListener('animationend', handleAnimationEnd)
}
}, [])
const clickBtn = () => {
if (val.trim().length < 5) {
setErr('error')
return
}
alert('提交成功')
}
return (
<div>
<h2>動畫事件的應用場景舉例</h2>
<h3>請輸入文字,至少五個字符</h3>
<div className={classNames('inputContainer', { 'errorInput': err })} ref={inputContainerRef}>
<Input status={err} value={val} ref={inputRef} onChange={onChange} placeholder="請輸入" style={{ width: 240 }} />
{err && <div className='errorText'>請輸入至少五個字符</div>}
</div>
<br />
<Button onClick={clickBtn} style={{ marginTop: '12px' }} type="primary" disabled={isDisabledBtn}>提交</Button>
</div>
)
}
export default AnimationEvent
動畫事件的第三個參數
- 實際上,動畫事件還有第三個參數
- 方便我們進行配置,從而實現相應的效果
- 上述案例中,我們得先綁定動畫事件,最後再去解綁動畫事件
- 無形中,增加了代碼量
- 動畫事件的第三個參數,方便我們直接使用
一次性的動畫事件 - 如下示例
element.addEventListener('transitionend', handleTransition, {
once: true, // 只觸發一次
capture: false, // 冒泡階段觸發
passive: true // 不調用 preventDefault
});
once: true最常用,一次性使用,不用再去手動解綁事件,另外兩個用的不多
如下效果圖,就以once: true,為案例
效果圖
- 這裏的需求就是點擊一個item,讓這個item播放動畫
- 同時清除掉其他的item身上的動畫
- 搭配
once: true代碼更加簡約了
完整Vue代碼
<template>
<div>
<div
:class="['item', 'item' + index, activeClass[index]]"
@click="clc(item, index)"
v-for="(item, index) in arr"
:key="index"
>
{{ item.name }} - {{ item.age }}
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const arr = ref([
{ name: "Tom", age: 18 },
{ name: "Jerry", age: 20 },
{ name: "Jack", age: 22 },
]);
// 用來存儲每個 item 的動畫狀態
const activeClass = ref([]);
setTimeout(() => {
console.log("activeClass", activeClass.value);
}, 1000);
const clc = (item, index) => {
document.querySelector(".item" + index).addEventListener(
"animationend",
() => {
activeClass.value = activeClass.value.map(() => "");
},
{ once: true }
);
// 清空所有元素的動畫狀態
activeClass.value = activeClass.value.map(() => "");
// 給點擊的元素添加動畫狀態
activeClass.value[index] = "animate";
};
</script>
<style lang="less" scoped>
.item {
border: 1px solid #f00;
width: 100px;
margin: 36px;
cursor: pointer;
}
/* 動畫類,觸發動畫 */
.animate {
animation: donghua 2s 1;
}
@keyframes donghua {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 #589fd7;
}
100% {
transform: scale(1.2);
box-shadow: 0 0 0 12px rgba(204, 73, 152, 0%);
}
}
</style>
A good memory is better than a bad pen. Record it down...