从一个动画需求,来学习js中animation动画事件的具体应用

本文通过一个具体的动画需求,来讲解一下js中animationend事件使用

前言

  • 平常,我们代码中,会做一些事件的监听
  • 比如点击事件、右键事件、滚动事件等
  • 实际上,js中还提供了动画事件的相关api
  • 以便于我们能够应对产品奇怪的需求

需求描述

  • 假设我们有一个输入框
  • 当用户输入错误的数据的时候
  • 进行校验不通过的动画播放提示(让输入框左右抖动三次)
  • 在动画播放期间,不允许用户再次点击提交按钮
  • 必须要动画播放完毕后,才允许用户点击按钮
  • 且在动画播放完毕以后,清除掉用户输入的错误的数据
  • 如下效果图:

效果图

  • 所以,我们需要通过动画事件进行相关的逻辑控制

动画事件知识点

如下:三个动画相关事件------用于链式动画控制、状态更新操作等交互逻辑

  • animationstart:动画开始时触发
  • animationend:动画完成时触发
  • animationiteration:动画每完成一次循环时触发
js 复制代码
const element = document.getElementById('animatedElement');

element.addEventListener('animationstart', () => {
  console.log('动画开始了');
});

element.addEventListener('animationend', () => {
  console.log('动画结束了');
});

element.addEventListener('animationiteration', () => {
  console.log('动画完成一次迭代');
});

注意: 动画事件可以获取事件对象,包含动画名称、属性等信息

此外,还有动画过度事件,比如transitionend 过渡效果完成时触发,这里不赘述了

js 复制代码
const element = document.getElementById('transitionElement');

element.addEventListener('transitionend', (event) => {
  console.log(`过渡完成: ${event.propertyName}`);
});

代码逻辑

在页面加载完成后的useEffect中,去绑定对应动画事件

  • 在页面加载完成后的useEffect中,去绑定对应动画事件
  • 当动画开始的时候,去禁用按钮setIsDisabledBtn(true);
  • 当动画结束的时候,setVal('') // 清空输入框setErr('') // 清空错误提示setIsDisabledBtn(false); // 取消按钮禁用
js 复制代码
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库,去更好的控制类名的动态切换

html 复制代码
<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曲线,让动画更加有节奏

css 复制代码
.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代码

js 复制代码
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

动画事件的第三个参数

  • 实际上,动画事件还有第三个参数
  • 方便我们进行配置,从而实现相应的效果
  • 上述案例中,我们得先绑定动画事件,最后再去解绑动画事件
  • 无形中,增加了代码量
  • 动画事件的第三个参数,方便我们直接使用一次性的动画事件
  • 如下示例
js 复制代码
element.addEventListener('transitionend', handleTransition, {
  once: true,    // 只触发一次
  capture: false, // 冒泡阶段触发
  passive: true   // 不调用 preventDefault
});

once: true最常用,一次性使用,不用再去手动解绑事件,另外两个用的不多

如下效果图,就以once: true,为案例

效果图

  • 这里的需求就是点击一个item,让这个item播放动画
  • 同时清除掉其他的item身上的动画
  • 搭配once: true代码更加简约了

完整Vue代码

html 复制代码
<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...

相关推荐
汪子熙2 小时前
浏览器环境中 window.eval(vOnInit); // csp-ignore-legacy-api 的技术解析与实践意义
前端·javascript
轻语呢喃2 小时前
Tailwind CSS:原子类名驱动的现代CSS框架
css·html
BUG收容所所长2 小时前
🤖 零基础构建本地AI对话机器人:Ollama+React实战指南
前端·javascript·llm
小高0072 小时前
🚀前端异步编程:Promise vs Async/Await,实战对比与应用
前端·javascript·面试
Spider_Man2 小时前
"压"你没商量:性能优化的隐藏彩蛋
javascript·性能优化·node.js
用户87612829073742 小时前
对于通用组件如何获取表单输入,区分表单类型的试验
前端·javascript
每天吃饭的羊2 小时前
面试题:Sass
前端·css·sass
Bdygsl3 小时前
前端开发:JavaScript(6)—— 对象
开发语言·javascript·ecmascript
Mintopia4 小时前
AIGC Claude(Anthropic)接入与应用实战:从字节流到智能交互的奇妙旅程
前端·javascript·aigc
Mintopia4 小时前
Next.js 样式魔法指南:CSS Modules 与 Tailwind CSS 实战
前端·javascript·next.js