偶然中在百度图片看到一个有意思的动画效果,如下图所示
正好闲来无事,就想着实现个类似的动画,也好学习下 Angular 的动画系统。
首先是分析这个动画的一些特点。
- 鼠标悬停显示工具栏
- 鼠标移出隐藏工具栏
- 动画方向跟随鼠标方向
1 和 2 很容易通过 css 的 hover 伪类 实现,3却没办法使用 css 实现,因为 css 无法获取鼠标坐标。看来只能使用 Javascript 来实现了。
动手的第一步当然是创建个 Demo 项目,加点 css 把架子搭好。
然后添加 Angular 指令,给每个框框都附加上鼠标事件监听。
hover.directive.ts
@HostListener("mouseover", ["$event"]) mouseover(e: MouseEvent){ ... } @HostListener("mouseout", ["$event"]) mouseout(e: MouseEvent){ ... }
这里使用的HostListener在 Angular 中用于监听宿主元素事件,["$event"]则是将事件对象传递到函数内。
关于判断鼠标的方向,我这里用了一个比较简单的方法,通过找到距离鼠标最近的边来判定。
// `x,y`分别是鼠标相对于元素的 X 坐标和 Y 坐标 // `w,h`分别是元素的宽度和高度 getDirection([x, y, w, h]: number[]) { return [ { name: "top", value: y }, { name: "bottom", value: h - y }, { name: "left", value: x }, { name: "right", value: w - x } ].sort((a, b) => a.value - b.value)[0].name; }
在事件回调中传递事件对象和元素对象的属性,即可得到事件方向。
const direction = this.getDirection([ e.offsetX, e.offsetY, this.el.offsetWidth, this.el.offsetHeight ]);
前期准备工作都完成后,就要开始写动画了。
这里我用的是 Angular 的BrowserAnimationsModule模块来实现动画,所以要先在AppModule中导入BrowserAnimationsModule。
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @NgModule({ declarations: [AppComponent, HoverDirective], imports: [BrowserModule, BrowserAnimationsModule], providers: [], bootstrap: [AppComponent] })
BrowserAnimationsModule中有两套动画系统:
- animations 基于
CSS Animations,使用时附加于组件的元数据中,通过切换预定义的状态来实现动画过度 - AnimationBuilder。基于
Web Animations API,使用时依赖AnimationBuilder服务,通过脚本代码动态创建动画并执行。
刚开始我是用animations实现了动画效果,却发现因为动画要跟随鼠标方向,使用animations会显得繁琐而效果也不甚满意,所以改用AnimationBuilder实现,代价是在老旧的浏览器上需要附带 polyfills 以支持Web Animations API,好在 Angular6 之后的版本会自动附带,无需任何配置。
先实现鼠标悬停动画
@Input("appHover") backgroundImage: string; @Input("timings") timings: string | number = 200; get positions() { const [w, h] = [this.el.offsetWidth, this.el.offsetHeight]; return { top: `0 -${h}px`, bottom: `0 ${h}px`, left: `-${w}px 0`, right: `${w}px 0` }; } @HostListener("mouseover", ["$event"]) mouseover(e: MouseEvent) { const direction = this.getDirection([ e.offsetX, e.offsetY, this.el.offsetWidth, this.el.offsetHeight ]); this.builder .build([ style({ backgroundImage: this.backgroundImage, backgroundRepeat: "no-repeat", backgroundSize: "100% 100%", backgroundPosition: this.positions[direction] }), animate(this.timings, style({ backgroundPosition: "0" })) ]) .create(this.el) .play(); }
backgroundImage,timings为输入属性,用于设置背景图片和动画时间。下面是使用例子
<div *ngFor="let i of arr; index as index" class="item" appHover="url(/assets/e1370ff509f36594499e5a6a98de3d6a.jpeg)" [timings]="'500ms ease-in-out'" ></div>
positions会根据元素宽高返回背景图初始位置,我所实现的动画过程就是在鼠标移入时,背景图会从鼠标移入方向展开。例如鼠标从元素上方移入,背景图从{x:0, y:-100%}移动到{x:0, y:0},因为backgroundPosition的局限性,在设置backgroundSize为百分比后,无法再使用百分比确定位置,只好使用元素的宽高像素确定位置。
builder.build就是整个动画的关键部分了,其实是很简单的三步调用
- 设置初始样式和动画样式
- 绑定到当前元素
- 执行动画
移出的动画就更好设置了,只需要将背景图移回初始位置即可。这个初始位置并不是固定的,而是根据移出方向变动的。
@HostListener("mouseout", ["$event"]) mouseout(e: MouseEvent) { const direction = this.getDirection([ e.offsetX, e.offsetY, this.el.offsetWidth, this.el.offsetHeight ]); this.builder .build([ animate( this.timings, style({ backgroundPosition: this.positions[direction] }) ) ]) .create(this.el) .play(); }
这样整个动画就实现了,最终效果如下