Angular 4 - 如何在 div 进入视口时触发动画?
Angular 4 - how to trigger an animation when a div comes into the viewport?
我一直在使用 Angular 4 构建一个新站点,我正在尝试重新创建一个效果,当 div 变得可见时(当您向下滚动屏幕时)然后然后可以触发 angular 动画以从侧面滑动 div。
我过去曾在 Angular 4 之外使用 jQuery 来做到这一点,但我想尝试使用原生 Angular 4 动画来创建相同的效果。
谁能告诉我如何在 div 进入视图时触发动画(即当它进入视口时向下滚动到页面的下部?)。我已经编写了幻灯片动画,但我不知道如何在 div 稍后在视口可见时通过滚动触发它。
谢谢大家!
我创建了一个基础组件,它提供了一个标志 appearedOnce,如果该组件完全在视图内或者它的上边缘已到达视图的上边缘,则该标志变为真一次。
@Injectable()
export class AppearOnce implements AfterViewInit, OnDestroy {
appearedOnce: boolean;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef, private cdRef: ChangeDetectorRef){}
onResize() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.clientHeight;
this.checkVisibility();
}
onScroll() {
this.scrollPos = window.scrollY;
this.windowHeight = window.innerHeight;
this.checkVisibility();
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(!this.appearedOnce){
if(this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight)){
this.appearedOnce = true;
this.unsubscribe();
this.cdRef.detectChanges();
}
}
}
subscribe(){
this.subscriptionScroll = Observable.fromEvent(window, 'scroll').startWith(null)
.subscribe(() => this.onScroll());
this.subscriptionResize = Observable.fromEvent(window, 'resize').startWith(null)
.subscribe(() => this.onResize());
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
您可以简单地扩展此组件并通过继承appearedOnce 属性来使用
@Component({
template: `
<div>
<div *ngIf="appearedOnce">...</div>
...
</div>
`
})
class MyComponent extends AppearOnceComponent {
...
}
如果需要覆盖构造函数或生命周期钩子,请记住调用 super()。
(编辑)plunker:
https://embed.plnkr.co/yIpA1mI1b9kVoEXGy6Hh/
(edit) 我已经在下面的另一个答案中将其变成指令。
我创建了一个指令,一旦元素完全在视图内或者它的上边缘已经到达视图的上边缘,它就会发出一个事件。
这是一个笨蛋:https://embed.plnkr.co/mlez1dXjR87FNBHXq1YM/
是这样使用的:
<div (appear)="onAppear()">...</div>
指令如下:
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/startWith';
@Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
@Output()
appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef){
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(this.isVisible()){
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if(this.isVisible()){
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible(){
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe(){
this.subscriptionScroll = Observable.fromEvent(window, 'scroll').startWith(null)
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = Observable.fromEvent(window, 'resize').startWith(null)
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
一个简单的方法,如果你想在特定的组件中使用它:
@ViewChild('chatTeaser') chatTeaser: ElementRef;
@HostListener('window:scroll')
checkScroll() {
const scrollPosition = window.pageYOffset + window.innerHeight;
if (this.chatTeaser && this.chatTeaser.nativeElement.offsetTop >= scrollPosition) {
this.animateAvatars();
}
}
并且在 html 中:
<div id="chat-teaser" #chatTeaser>
恰好在元素顶部滚动到该函数时被调用。如果您只想在完整 div 出现时调用该函数,请将 div 高度添加到 this.chatTeaser.nativeElement.offsetTop
.
这是一个无限卷轴的简单例子;当元素进入视口时,它会触发 handleScrollEvent()
。
里面item-grid.component.html
<span [ngClass]="{hidden: curpage==maxpage}" (window:scroll)="handleScrollEvent()" (window:resize)="handleScrollEvent()" #loadmoreBtn (click)="handleLoadMore()">Load more</span>
里面 item-grid.component.ts
:
@ViewChild('loadmoreBtn') loadmoreBtn: ElementRef;
curpage: number;
maxpage: number;
ngOnInit() {
this.curpage = 1;
this.maxpage = 5;
}
handleScrollEvent() {
const { x, y } = this.loadmoreBtn.nativeElement.getBoundingClientRect();
if (y < window.innerHeight && this.maxpage > this.curpage) {
this.curpage++;
}
}
答案已更新以适用于最新的 Rxjs 和 Angular 版本,希望这对您有所帮助
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '@angular/core';
import { Subscription } from 'rxjs';
import { fromEvent } from 'rxjs';
import { startWith } from 'rxjs/operators';
@Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
@Output() appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef) {
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any) {
let offsetTop = element.offsetTop || 0;
if (element.offsetParent) {
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility() {
if (this.isVisible()) {
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if (this.isVisible()) {
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible() {
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe() {
this.subscriptionScroll = fromEvent(window, 'scroll').pipe(startWith(null))
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = fromEvent(window, 'resize').pipe(startWith(null))
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe() {
if (this.subscriptionScroll) {
this.subscriptionScroll.unsubscribe();
}
if (this.subscriptionResize) {
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit() {
this.subscribe();
}
ngOnDestroy() {
this.unsubscribe();
}
}
Martin Cremer 给出的答案是完美的。
除非您希望它在使用 ssr
Angular Universal
的 angular 应用程序上运行
我已经修改了现有已接受的答案以在下面的 ssr 中工作
创建一个可注入服务以便能够在后端使用 window 对象
import { Injectable } from '@angular/core';
export interface ICustomWindow extends Window {
__custom_global_stuff: string;
}
function getWindow (): any {
return window;
}
@Injectable({
providedIn: 'root',
})
export class WindowService {
get nativeWindow (): ICustomWindow {
return getWindow();
}
}
现在,创建一个指令以在元素位于可视区域时发出通知
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
import { WindowService } from './window.service';
@Directive({
selector: '[appear]'
})
export class AppearDirective {
windowHeight: number = 0;
elementHeight: number = 0;
elementPos: number = 0;
@Output()
appear: EventEmitter<boolean>;
constructor(
private element: ElementRef,
private window: WindowService
) {
this.appear = new EventEmitter<boolean>();
}
checkVisible() {
if (this.elementPos < this.window.nativeWindow.scrollY + this.windowHeight) {
this.appear.emit(true);
this.appear.complete();
}
}
@HostListener('window:scroll', [])
onScroll() {
this.checkVisible();
}
@HostListener('window:load', [])
onLoad() {
this.windowHeight = (this.window.nativeWindow.innerHeight);
this.elementHeight = (this.element.nativeElement as HTMLElement).offsetHeight;
this.elementPos = (this.element.nativeElement as HTMLElement).offsetTop;
this.checkVisible();
}
@HostListener('window:resize', [])
onResize() {
this.windowHeight = (this.window.nativeWindow.innerHeight);
this.elementHeight = (this.element.nativeElement as HTMLElement).offsetHeight;
this.elementPos = (this.element.nativeElement as HTMLElement).offsetTop;
this.checkVisible();
}
}
在组件中创建一个新函数
onAppear() {
// TODO: do something
}
将指令添加到您的元素
<!-- ... -->
<h2 (appear)="onAppear()">Visible</h2>
<!-- ... -->
有一个更新的 API 旨在处理这个确切的问题:IntersevtionObserver。使用它可以让您摆脱所有手动偏移计算并保持本地状态。这是一个使用 API:
的简单示例
import { AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core';
/**
* @description
* Emits the `appear` event when the element comes into view in the viewport.
*
*/
@Directive({
selector: '[visibleSpy]',
})
export class OnVisibleDirective implements AfterViewInit, OnDestroy {
@Output() appear = new EventEmitter<void>();
private observer: IntersectionObserver;
constructor(private element: ElementRef) {}
ngAfterViewInit() {
const options = {
root: null,
rootMargin: '0px',
threshold: 0,
};
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.appear.next();
}
});
}, options);
this.observer.observe(this.element.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
}
我一直在使用 Angular 4 构建一个新站点,我正在尝试重新创建一个效果,当 div 变得可见时(当您向下滚动屏幕时)然后然后可以触发 angular 动画以从侧面滑动 div。
我过去曾在 Angular 4 之外使用 jQuery 来做到这一点,但我想尝试使用原生 Angular 4 动画来创建相同的效果。
谁能告诉我如何在 div 进入视图时触发动画(即当它进入视口时向下滚动到页面的下部?)。我已经编写了幻灯片动画,但我不知道如何在 div 稍后在视口可见时通过滚动触发它。
谢谢大家!
我创建了一个基础组件,它提供了一个标志 appearedOnce,如果该组件完全在视图内或者它的上边缘已到达视图的上边缘,则该标志变为真一次。
@Injectable()
export class AppearOnce implements AfterViewInit, OnDestroy {
appearedOnce: boolean;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef, private cdRef: ChangeDetectorRef){}
onResize() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.clientHeight;
this.checkVisibility();
}
onScroll() {
this.scrollPos = window.scrollY;
this.windowHeight = window.innerHeight;
this.checkVisibility();
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(!this.appearedOnce){
if(this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight)){
this.appearedOnce = true;
this.unsubscribe();
this.cdRef.detectChanges();
}
}
}
subscribe(){
this.subscriptionScroll = Observable.fromEvent(window, 'scroll').startWith(null)
.subscribe(() => this.onScroll());
this.subscriptionResize = Observable.fromEvent(window, 'resize').startWith(null)
.subscribe(() => this.onResize());
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
您可以简单地扩展此组件并通过继承appearedOnce 属性来使用
@Component({
template: `
<div>
<div *ngIf="appearedOnce">...</div>
...
</div>
`
})
class MyComponent extends AppearOnceComponent {
...
}
如果需要覆盖构造函数或生命周期钩子,请记住调用 super()。
(编辑)plunker: https://embed.plnkr.co/yIpA1mI1b9kVoEXGy6Hh/
(edit) 我已经在下面的另一个答案中将其变成指令。
我创建了一个指令,一旦元素完全在视图内或者它的上边缘已经到达视图的上边缘,它就会发出一个事件。
这是一个笨蛋:https://embed.plnkr.co/mlez1dXjR87FNBHXq1YM/
是这样使用的:
<div (appear)="onAppear()">...</div>
指令如下:
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/startWith';
@Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
@Output()
appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef){
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(this.isVisible()){
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if(this.isVisible()){
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible(){
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe(){
this.subscriptionScroll = Observable.fromEvent(window, 'scroll').startWith(null)
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = Observable.fromEvent(window, 'resize').startWith(null)
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
一个简单的方法,如果你想在特定的组件中使用它:
@ViewChild('chatTeaser') chatTeaser: ElementRef;
@HostListener('window:scroll')
checkScroll() {
const scrollPosition = window.pageYOffset + window.innerHeight;
if (this.chatTeaser && this.chatTeaser.nativeElement.offsetTop >= scrollPosition) {
this.animateAvatars();
}
}
并且在 html 中:
<div id="chat-teaser" #chatTeaser>
恰好在元素顶部滚动到该函数时被调用。如果您只想在完整 div 出现时调用该函数,请将 div 高度添加到 this.chatTeaser.nativeElement.offsetTop
.
这是一个无限卷轴的简单例子;当元素进入视口时,它会触发 handleScrollEvent()
。
里面item-grid.component.html
<span [ngClass]="{hidden: curpage==maxpage}" (window:scroll)="handleScrollEvent()" (window:resize)="handleScrollEvent()" #loadmoreBtn (click)="handleLoadMore()">Load more</span>
里面 item-grid.component.ts
:
@ViewChild('loadmoreBtn') loadmoreBtn: ElementRef;
curpage: number;
maxpage: number;
ngOnInit() {
this.curpage = 1;
this.maxpage = 5;
}
handleScrollEvent() {
const { x, y } = this.loadmoreBtn.nativeElement.getBoundingClientRect();
if (y < window.innerHeight && this.maxpage > this.curpage) {
this.curpage++;
}
}
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '@angular/core';
import { Subscription } from 'rxjs';
import { fromEvent } from 'rxjs';
import { startWith } from 'rxjs/operators';
@Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
@Output() appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef) {
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any) {
let offsetTop = element.offsetTop || 0;
if (element.offsetParent) {
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility() {
if (this.isVisible()) {
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if (this.isVisible()) {
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible() {
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe() {
this.subscriptionScroll = fromEvent(window, 'scroll').pipe(startWith(null))
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = fromEvent(window, 'resize').pipe(startWith(null))
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe() {
if (this.subscriptionScroll) {
this.subscriptionScroll.unsubscribe();
}
if (this.subscriptionResize) {
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit() {
this.subscribe();
}
ngOnDestroy() {
this.unsubscribe();
}
}
Martin Cremer 给出的答案是完美的。
除非您希望它在使用 ssr
Angular Universal
的 angular 应用程序上运行
我已经修改了现有已接受的答案以在下面的 ssr 中工作
创建一个可注入服务以便能够在后端使用 window 对象
import { Injectable } from '@angular/core';
export interface ICustomWindow extends Window {
__custom_global_stuff: string;
}
function getWindow (): any {
return window;
}
@Injectable({
providedIn: 'root',
})
export class WindowService {
get nativeWindow (): ICustomWindow {
return getWindow();
}
}
现在,创建一个指令以在元素位于可视区域时发出通知
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';
import { WindowService } from './window.service';
@Directive({
selector: '[appear]'
})
export class AppearDirective {
windowHeight: number = 0;
elementHeight: number = 0;
elementPos: number = 0;
@Output()
appear: EventEmitter<boolean>;
constructor(
private element: ElementRef,
private window: WindowService
) {
this.appear = new EventEmitter<boolean>();
}
checkVisible() {
if (this.elementPos < this.window.nativeWindow.scrollY + this.windowHeight) {
this.appear.emit(true);
this.appear.complete();
}
}
@HostListener('window:scroll', [])
onScroll() {
this.checkVisible();
}
@HostListener('window:load', [])
onLoad() {
this.windowHeight = (this.window.nativeWindow.innerHeight);
this.elementHeight = (this.element.nativeElement as HTMLElement).offsetHeight;
this.elementPos = (this.element.nativeElement as HTMLElement).offsetTop;
this.checkVisible();
}
@HostListener('window:resize', [])
onResize() {
this.windowHeight = (this.window.nativeWindow.innerHeight);
this.elementHeight = (this.element.nativeElement as HTMLElement).offsetHeight;
this.elementPos = (this.element.nativeElement as HTMLElement).offsetTop;
this.checkVisible();
}
}
在组件中创建一个新函数
onAppear() {
// TODO: do something
}
将指令添加到您的元素
<!-- ... -->
<h2 (appear)="onAppear()">Visible</h2>
<!-- ... -->
有一个更新的 API 旨在处理这个确切的问题:IntersevtionObserver。使用它可以让您摆脱所有手动偏移计算并保持本地状态。这是一个使用 API:
的简单示例import { AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output } from '@angular/core';
/**
* @description
* Emits the `appear` event when the element comes into view in the viewport.
*
*/
@Directive({
selector: '[visibleSpy]',
})
export class OnVisibleDirective implements AfterViewInit, OnDestroy {
@Output() appear = new EventEmitter<void>();
private observer: IntersectionObserver;
constructor(private element: ElementRef) {}
ngAfterViewInit() {
const options = {
root: null,
rootMargin: '0px',
threshold: 0,
};
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.appear.next();
}
});
}, options);
this.observer.observe(this.element.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
}