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 来替代动画,减少代码量。

元素的定位问题

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/756939.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何从零开始拆解uni-app开发的vue项目(一)

uni-app项目分析: 背景:最近接手一个前同事留下的半拉子项目,出拿过来觉得很简单;当我看到app.vue的时候很确定是vue项目,心里不怎么慌,果断安装node.js,然后就去npm ;安装VS code,事实并不是我期盼的那样,或者说根本就不能运行。 报错:应用vs code打开文件,输入命…

智慧城市与数字孪生:科技融合助力城市可持续发展

随着信息技术的迅猛发展&#xff0c;智慧城市和数字孪生作为现代城市发展的重要理念和技术手段&#xff0c;正日益受到广泛关注。智慧城市通过集成应用先进的信息通信技术&#xff0c;实现城市管理、服务、运行的智能化&#xff0c;而数字孪生则是利用数字化手段对物理城市进行…

Stewart并联六自由度摇摆平台计算

六自由度并联Stewart Platform摇摆平台。Matlab GUI界面操作&#xff0c;动画显示河模拟仿真&#xff0c;可以手动设置设备系统参数。 Matlab 程序&#xff0c;源代码包含注释。 程序下载链接&#xff1a; https://download.csdn.net/download/panjinliang066333/88991928 …

GPT-4引领AI新纪元,Claude3、Gemini、Sora能否跟上步伐?

【最新增加Claude3、Gemini、Sora、GPTs讲解及AI领域中的集中大模型的最新技术】 2023年随着OpenAI开发者大会的召开&#xff0c;最重磅更新当属GPTs&#xff0c;多模态API&#xff0c;未来自定义专属的GPT。微软创始人比尔盖茨称ChatGPT的出现有着重大历史意义&#xff0c;不亚…

微服务高级篇(一):微服务保护+Sentinel

文章目录 一、初识Sentinel1.1 雪崩问题及解决方案1.2 微服务保护技术对比1.3 Sentinel介绍与安装1.4 微服务整合Sentinel 二、Sentinel的流量控制三、Sentinel的隔离与降级四、Sentinel的授权规则五、规则持久化5.1 规则管理模式【原始模式、pull模式、push模式】5.2 实现push…

web前端框架设计第二课-Vue.js简介

web前端框架设计第二课-Vue.js简介 一.预习笔记 1.Vue.js概述 Vue.js是一套用于构建用户界面的渐进式框架。本质上是一个用于开发Web前端界面的库&#xff0c;其本身具有响应式编程和组件化的特点。 Vue.js的特性&#xff1a; 轻量级 数据绑定 应用指令 插件化开发 2.V…

【linux】Debian访问Debian上的共享目录

要在Debian系统上访问共享目录&#xff0c;通常意味着要访问通过网络共享的文件夹&#xff0c;比如通过SMB/CIFS&#xff08;Server Message Block/Common Internet File System&#xff09;协议共享的Windows共享文件夹。以下是访问共享目录的步骤&#xff1a; 1. 安装必要的…

PCL 极大似然估计法拟合平面

目录 一、算法原理1、极大似然估计2、拟合过程3、参考文献二、代码实现三、结果展示一、算法原理 1、极大似然估计 在以最小二乘法为基础的估计算法中, 所有的参数都是确定值;而实际上,测量数据与未知参数都具有一定的随机性。这就导致了最小二乘法估计质量的缺陷以及对白噪…

【C++】Qt:WebSocket客户端示例

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍WebSocket客户端示例。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&…

[Halcon学习笔记]实现多边形绘图后自动闭合成斜矩形

1、介绍 在Halcon联合开发项目中&#xff0c;需要在Halcon窗口上绘制不同形状的ROI&#xff0c;但是Halcon自带的绘图操作不太方便&#xff0c;操作时交互感官较差&#xff0c;所以可以通过绘制多边形&#xff0c;通过点来绘制多边形&#xff0c;最后通过闭合算子将绘框形成闭…

大数据技术学习笔记(十三)—— HBase

目录 1 Hbase 概述1.1 Hbase 定义1.2 HBase 数据模型1.2.1 HBase 逻辑结构1.2.2 HBase 物理存储结构1.2.3 数据模型 1.3 HBase 基本架构 2 HBase Shell 操作2.1 基本操作2.2 namespace 操作2.3 表操作 3 HBase 原理深入3.1 RegionServer 架构3.2 HBase 写流程3.3 MemStore Flus…

WPF —— 控件模版和数据模版

1:控件模版简介: 自定义控件模版&#xff1a;自己添加的样式、标签&#xff0c;控件模版也是属于资源的一种&#xff0c; 每一个控件模版都有一唯一的 key&#xff0c;在控件上通过template属性进行绑定 什么场景下使用自定义控件模版&#xff0c;当项目里面多个地方…

Sentinel(流控模式:直接关联链路,流控效果:直接预热排队)

Sentinel能够对流量进行控制&#xff0c;主要是监控应用的QPS流量或者并发线程数等指标&#xff0c;如果达到指定的阈值时&#xff0c;就会被流量进行控制&#xff0c;以避免服务被瞬时的高并发流量击垮&#xff0c;保证服务的高可靠性。 1.流控模式: 直接模式测试案例 表示1…

微信小程序项目实战遇到的问题

我们以学生成绩平台来作为例子。这是我们想得到的效果。 以下是完整代码&#xff1a; index.js // index.js Page({//页面的初始数据data: {hello: 欢迎进入微信小程序的编程世界,score: 80,userArray: [{name: 张三,score: [66, 77, 86, 70, 90]},{name: 李四,score: [88, 7…

如何让自己上百度百科?个人百科词条创建

百度百科&#xff0c;作为我国最大的中文百科全书&#xff0c;其影响力和权威性不言而喻。能够登上百度百科&#xff0c;意味着个人的知名度、成就和社会影响力得到了广泛认可。那么&#xff0c;如何才能让自己上百度百科呢&#xff1f;接下来伯乐网络传媒就来给大家讲解一下。…

MyBatis3源码深度解析(十六)SqlSession的创建与执行(三)Mapper方法的调用过程

文章目录 前言5.9 Mapper方法的调用过程5.10 小结 前言 上一节【MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取】已经知道&#xff0c;调用SqlSession对象的getMapper(Class)方法&#xff0c;传入指定的Mapper接口对应的Class对象…

C#,图论与图算法,计算无向连通图中长度为n环的算法与源代码

1 无向连通图中长度为n环 给定一个无向连通图和一个数n,计算图中长度为n的环的总数。长度为n的循环仅表示该循环包含n个顶点和n条边。我们必须统计存在的所有这样的环。 为了解决这个问题,可以有效地使用DFS(深度优先搜索)。使用DFS,我们可以找到特定源(或起点)的长度…

十一、MYSQL 基于MHA的高可用集群

目录 一、MHA概述 1、简介 2、MHA 特点 3、MHA 工作原理&#xff08;流程&#xff09; 二、MHA高可用结构部署 1、环境准备 2、安装MHA 监控manager 3、在manager管理机器上配置管理节点&#xff1a; 4、编master_ip_failover脚本写 5、在master上创建mha这个用户来访…

web容器导论

一、基础概念 1.Web容器是什么&#xff1f; 让我们先来简单回顾一下Web技术的发展历史&#xff0c;可以帮助你理解Web容器的由来。 早期的Web应用主要用于浏览新闻等静态页面&#xff0c;HTTP服务器&#xff08;比如Apache、Nginx&#xff09;向浏览器返回静态HTML&#xff…

轻松解锁微博视频:基于Perl的下载解决方案

引言 随着微博成为中国最受欢迎的社交平台之一&#xff0c;其内容已经变得丰富多彩&#xff0c;特别是视频内容吸引了大量用户的关注。然而&#xff0c;尽管用户对微博上的视频内容感兴趣&#xff0c;但却面临着无法直接下载这些视频的难题。本文旨在介绍一个基于Perl的解决方…