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()。

(编辑)plunkerhttps://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();
    }
}