Stories

Detail Return Return

從一個動畫需求,來學習js中animation動畫事件的具體應用 - Stories Detail

本文通過一個具體的動畫需求,來講解一下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...
user avatar imhaoli Avatar kaidiwen Avatar zhuifengdekukafei Avatar momeak9 Avatar pulsgarney Avatar wonfody Avatar jerryc Avatar zz_641473ad470bc Avatar baozouai Avatar myskies Avatar onlythinking Avatar aiyaotoudedianfengshan Avatar
Favorites 20 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.