Angular进阶之八: Angular Animation在项目中的实践经验

本文主要是介绍Angular进阶之八: Angular Animation在项目中的实践经验,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

使用 Angular 进行项目开发的程序员应该都很熟悉 Angular Animation。这是一个 Angular 原生的动画库,它可以替代或者辅助完成原本需要使用 css 的动画功能。

Angular 在国内的运用是很有限的,可借鉴的文档并不很丰富。尤其对于 Angular 动画模块的应用类文档更是少见。我认为原因可能是大家普遍认为动画应当是由 css 去实现的,毕竟它有非常完善并且兼容性极强的动画功能。而且作为前端工程师,精通 css 是基本功,使用 css 完成页面的动画功能成本低、效率高。

既然如此那又为什么需要 Angular Animation 呢?我在实际的项目中体会到,相比 css 动画,Angular Animation 最大的优点是能够提供一系列很准确的关键帧回调函数(callback)。

下面是我模仿项目中的功能写的一个例子。我会罗列出所遇到的问题,并且逐一阐述我的解决方案。

代码环境:

javascript 

{"name": "blog-angular-animation","version": "0.0.0","scripts": {"ng": "ng","start": "ng serve","build": "ng build","watch": "ng build --watch --configuration development","test": "ng test"},"private": true,"dependencies": {"@angular/animations": "^17.1.0","@angular/common": "^17.1.0","@angular/compiler": "^17.1.0","@angular/core": "^17.1.0","@angular/forms": "^17.1.0","@angular/platform-browser": "^17.1.0","@angular/platform-browser-dynamic": "^17.1.0","@angular/router": "^17.1.0","lodash": "^4.17.21","rxjs": "~7.8.0","tslib": "^2.3.0","zone.js": "~0.14.3"},"devDependencies": {"@angular-devkit/build-angular": "^17.1.0","@angular/cli": "^17.1.0","@angular/compiler-cli": "^17.1.0","@types/jasmine": "~5.1.0","@types/lodash": "^4.14.202","jasmine-core": "~5.1.0","karma": "~6.4.0","karma-chrome-launcher": "~3.2.0","karma-coverage": "~2.2.0","karma-jasmine": "~5.1.0","karma-jasmine-html-reporter": "~2.1.0","typescript": "~5.3.2"}
}

Demo 效果如下所示,这是一个简单的列表元素添加删除功能:

  • 点击 Add 会在列表末尾添加一个元素
  • 点击 Delete 会从列表中删除当前元素,并且调整后续元素的序号

Animation Demo

现在我们为这两个行为添加一些动画。下面是对动画过程的详细描述:

1. add

添加节点动画分为3个步骤

1.高度从0变为标准节点高度

2.宽度从1%增加到100%

3.渐入式显示内部元素

这三个步骤可以使用一个transition来完成,请看下面的代码:

typescript

trigger('addNode', [transition(':enter', [style({ height: '0px', width: '1%' }),query('.list-item-index', [style({ opacity: 0 })], { optional: true }),query('.list-item-value', [style({ opacity: 0 })], { optional: true }),query('.list-item-btn', [style({ opacity: 0 })], { optional: true }),group([style({ height: '0px', width: '1%' }),animate('0.2s ease-in-out', style({ height: '50px' }))]),group([style({ width: '1%' }),animate('0.2s 0.1s ease-in-out', style({ width: '100%' }))]),group([query('.list-item-index', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true }),query('.list-item-value', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true }),query('.list-item-btn', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true })])])
])

这里有两个问题需要注意:

初始的状态需要被设定

可以看到最上面的四行代码

typescript

style({ height: '0px', width: '1%' }),
query('.list-item-index', [style({ opacity: 0 })], { optional: true }),
query('.list-item-value', [style({ opacity: 0 })], { optional: true }),
query('.list-item-btn', [style({ opacity: 0 })], { optional: true }),

它们的作用就是设定动画开始时的各个元素的初始状态。目的是防止动画开始时的抖动。

因为触发动画的状态是:enter,所以当第一次渲染整个列表时,所有的节点都会触发动画。这可能不是我们需要看到的。此时我们需要其它的参数来标识出不需要动画的节点

如下所示,当我刷新了页面后,所有节点的 add animation 都执行了

我们可以利用 Angular animation 的 disabled 属性来禁用非必要动画

html

<div class="my-animation-container"><div class="list-container" [@fadeIndexMarke]="fadeIndexMarkeStatus" (@fadeIndexMarke.done)="fadeIndexMarkeDone()"><div class="list-item-container"*ngFor="let item of list"[@.disabled]="animationNodeIndex !== item.index"[@addNode][@deleteNode](@addNode.done)="addAnimationDone()"(@deleteNode.done)="deleteAnimationDone()"><div class="list-item-index" [ngStyle]="{ opacity: animationRunning ? 0 : 1 }">{{item.index}}</div><div class="list-item-value">{{item.value}}</div><div class="list-item-btn" (click)="handleDelete(item.index)">Delete</div></div></div><div class="list-active" (click)="handleAdd()">Add</div>
</div>

typescript

handleAdd() {this.animationNodeIndex = this.list?.length || 0;this.addNode(); // Push a node in list
}addAnimationDone() {if (this.animationNodeIndex >= 0) {this.animationNodeIndex = -1;}
}

这样就可以在我们需要动画的时候再执行它

2. delete

这个动画看似与 add animation 相似,但是过程却比它要复杂一些。下面是完整的动画:

这个动画分为3个步骤:

  • 隐藏所有的序号
  • 删除指定节点
  • 显示节点前的序号

这一组动画有很强的顺序性,必须是上一个动画执行完后才能执行下一个动画。特别要注意的是删除节点的操作需要在第二步完成,所以我们需要监听第一个步骤完成时的回调。

这在 css 中很难实现,可能需要借助 setTimeout。在 Angular 中,定时器并非是解决问题的一个好的选择。

Angular Animation 为我们提供了一个更好的方案。我们可以将动画拆分成两部分绑定在不同的元素上

typescript

animations: [trigger('fadeIndexMarke', [transition('fadeIn => fadeOut', [query('.list-item-index', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true })]),transition('fadeOut => fadeIn', [query('.list-item-index', [style({ opacity: 0 }),animate('0.2s ease-in-out', style({ opacity: 1 }))], { optional: true })])]),trigger('deleteNode', [transition(':leave', [style({ width: '100%', height: '50px', overflow: 'hidden' }),query('.list-item-index', style({ opacity: 0 }), { optional: true }),group([query('.list-item-value', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true }),query('.list-item-btn', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true })]),group([animate('0.2s 0.2s ease-in-out', style({ width: '0%' })),animate('0.2s 0.3s ease-in-out', style({ height: '0px' }))])])])]

上面这两个动画分别绑定在最外围的列表元素和每一个节点元素上

html

<div class="list-container" [@fadeIndexMarke]="fadeIndexMarkeStatus"(@fadeIndexMarke.done)="fadeIndexMarkeDone()"><div class="list-item-container"*ngFor="let item of list"[@deleteNode](@deleteNode.done)="deleteAnimationDone()">
...

我们可以先执行隐藏索引的动画,然后监听 animation.done,此时再删除指定节点。

typescript

fadeIndexMarkeDone() {if (this.fadeIndexMarkeStatus === 'fadeOut') {// Step 2this.animationRunning = true;this.fadeIndexMarkeCallBack();}
}handleDelete(index: number) {// Step 1this.fadeIndexMarkeCallBack = () => {// Step 3this.deleteNode(index);};this.fadeIndexMarkeStatus = 'fadeOut';this.animationNodeIndex = index;
}deleteAnimationDone() {// Step 4if (this.animationRunning) {this.animationRunning = false;this.fadeIndexMarkeStatus = 'fadeIn';this.animationNodeIndex = -1;this.fadeIndexMarkeCallBack = () => {};}
}

这样动画的执行顺序就可以按照我们的需求来规划了。下面是完整的代码:

html

<div class="my-animation-container"><div class="list-container" [@fadeIndexMarke]="fadeIndexMarkeStatus" (@fadeIndexMarke.done)="fadeIndexMarkeDone()"><div class="list-item-container"*ngFor="let item of list"[@.disabled]="animationNodeIndex !== item.index"[@addNode][@deleteNode](@addNode.done)="addAnimationDone()"(@deleteNode.done)="deleteAnimationDone()"><div class="list-item-index" [ngStyle]="{ opacity: animationRunning ? 0 : 1 }">{{item.index}}</div><div class="list-item-value">{{item.value}}</div><div class="list-item-btn" (click)="handleDelete(item.index)">Delete</div></div></div><div class="list-active" (click)="handleAdd()">Add</div></div>

less

.my-animation-container {width: 100%;height: 100%;overflow: hidden;display: flex;flex-flow: column;justify-content: center;align-items: center;.list-container {width: 400px;height: 600px;border: 2px solid gray;overflow-x: hidden;overflow-y: auto;padding: 20px;.list-item-container {width: 100%;height: 50px;border: 1px solid #CCCCCC;display: flex;flex-flow: row nowrap;justify-content: space-between;align-items: center;padding: 0 20px;.list-item-index {font-size: 24px;font-weight: 800;color: #666666;opacity: 1;&.hide-index {opacity: 0;}}.list-item-value {font-size: 20px;font-weight: 500;color: #666666;}.list-item-btn {font-size: 14px;font-weight: 500;color: #666666;border: 2px solid skyblue;border-radius: 5px;padding: 5px;cursor: pointer;&:hover {background-color: skyblue;color: white;}&:active {background-color: white;color: skyblue;}}}}.list-active {font-size: 20px;font-weight: 500;color: #666666;border: 2px solid skyblue;border-radius: 5px;padding: 5px;cursor: pointer;margin-top: 20px;&:hover {background-color: skyblue;color: white;}&:active {background-color: white;color: skyblue;}}
}

typescript

import { Component, OnInit } from '@angular/core';
import { animate, style, transition, trigger, state, group, query } from '@angular/animations';import * as _ from 'lodash';@Component({selector: 'my-animation',templateUrl: './animation.component.html',styleUrls: ['./animation.component.less'],animations: [trigger('fadeIndexMarke', [transition('fadeIn => fadeOut', [query('.list-item-index', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true })]),transition('fadeOut => fadeIn', [query('.list-item-index', [style({ opacity: 0 }),animate('0.2s ease-in-out', style({ opacity: 1 }))], { optional: true })])]),trigger('addNode', [transition(':enter', [style({ height: '0px', width: '1%' }),query('.list-item-index', [style({ opacity: 0 })], { optional: true }),query('.list-item-value', [style({ opacity: 0 })], { optional: true }),query('.list-item-btn', [style({ opacity: 0 })], { optional: true }),group([style({ height: '0px', width: '1%' }),animate('0.2s ease-in-out', style({ height: '50px' }))]),group([style({ width: '1%' }),animate('0.2s 0.1s ease-in-out', style({ width: '100%' }))]),group([query('.list-item-index', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true }),query('.list-item-value', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true }),query('.list-item-btn', [style({ opacity: 0 }),animate('0.2s 0.3s ease-in-out', style({ opacity: 1 }))], { optional: true })])])]),trigger('deleteNode', [transition(':leave', [style({ width: '100%', height: '50px', overflow: 'hidden' }),query('.list-item-index', style({ opacity: 0 }), { optional: true }),group([query('.list-item-value', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true }),query('.list-item-btn', [style({ opacity: 1 }),animate('0.2s ease-in-out', style({ opacity: 0 }))], { optional: true })]),group([animate('0.2s 0.2s ease-in-out', style({ width: '0%' })),animate('0.2s 0.3s ease-in-out', style({ height: '0px' }))])])])]
})export class MyAnimationComponent implements OnInit {list: { index: number; value: string; }[] = [];animationRunning = false;animationNodeIndex: number = -1;fadeIndexMarkeStatus = 'fadeIn';fadeIndexMarkeCallBack = () => {};ngOnInit() {this.list = _.chain(3).range().map((num) => ({ index: num, value: `This is the ${num + 1}'s item` })).value();}fadeIndexMarkeDone() {if (this.fadeIndexMarkeStatus === 'fadeOut') {// Step 2this.animationRunning = true;this.fadeIndexMarkeCallBack();}}handleAdd() {this.animationNodeIndex = this.list?.length || 0;this.addNode();}handleDelete(index: number) {// Step 1this.fadeIndexMarkeCallBack = () => {// Step 3this.deleteNode(index);};this.fadeIndexMarkeStatus = 'fadeOut';this.animationNodeIndex = index;}addAnimationDone() {if (this.animationNodeIndex >= 0) {this.animationNodeIndex = -1;}}deleteAnimationDone() {// Step 4if (this.animationRunning) {this.animationRunning = false;this.fadeIndexMarkeStatus = 'fadeIn';this.animationNodeIndex = -1;this.fadeIndexMarkeCallBack = () => {};}}private addNode() {const targetIndex = (this.list?.length || 0);this.list = _.concat(this.list, [{ index: targetIndex, value: `This is the ${targetIndex + 1}'s item` }]);}private deleteNode(index: number) {this.list = _.reduce(this.list, (result: { index: number; value: string; }[], curr, currIndex) => {if (currIndex > index) {curr.index -= 1;curr.value = `This is the ${curr.index + 1}'s item`;result.push(curr);} else if (currIndex < index) {result.push(curr);} else {// currIndex === index, exclude node}return result;}, []);}
}

以上所谈到的是我在项目中遇到的主要问题以及解决的方案。

下面还有一些在后期优化时所遇到的问题:

动画的回调函数的时机

Angular Animation 的 done 可以监听动画完成时的回调。这是官方文档的说法,但实际上它监听的是 animation state 的改变。组件在初始化后动画的状态就会改变,如下所示:

我没有执行任何操作但是 done 就被调用了,所以在监听这个回调的时候我们需要额外的参数来进行判断。

过多的 DOM 元素导致过多的渲染

列表中的节点越多,重新渲染的性能就越低。甚至当组件过于复杂或者嵌套的子组件过多的时候,动画会出现卡顿。

解决的方法是对组件进行优化,尽量减少 DOM 元素。或者降低子组件数量和嵌套层数。Angular 在渲染时会解析组件中所有的子组件,这在性能上会造成极大的损耗,所以应当尽量减少动画所影响到的组件。

节点宽高不定时,如何设定动画宽高的变化值

如果节点的宽高是自适应的,那么我们动画关键帧的 style 就最好使用百分比来表示。或者使用 transform: scale 来进行缩放。

简单的动画细节使用 animation 过于繁琐

定义一个动画需要 trigger, state, style 等一系列属性,即便完成一个很细节的动画也需要写很多代码。这时可以使用 transition 来替代动画,减少代码量。

元素的定位问题

这是一个很容易被忽略的问题。当我们的元素中包含绝对定位时,不同的定位方向可能导致动画的错乱。有些元素可能在动画中被截断,也有一些会发生意想不到的偏移。所以如果绑定动画的组件中存在不同的定位,最好是都统一成一个方向的绝对定位。

这篇关于Angular进阶之八: Angular Animation在项目中的实践经验的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/828755

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定

[MySQL表的增删改查-进阶]

🌈个人主页:努力学编程’ ⛅个人推荐: c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 💻💻💻数据库约束 🔭🔭🔭约束类型 not null: 指示某列不能存储 NULL 值unique: 保证某列的每行必须有唯一的值default: 规定没有给列赋值时的默认值.primary key:

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能