Angular 4 - 如何在div进入视口时触发动画?

17

我正在使用Angular 4构建新网站,尝试重新创建一个效果:当一个div变为可见状态(屏幕向下滚动时),则可以触发一个Angular动画来从侧面滑动该div。

过去我能够使用Angular 4之外的jQuery实现这一点,但我想尝试使用本地的Angular 4动画创建相同的效果。

有人可以为我提供关于如何在div进入视口(即滚动到页面底部时)时触发动画的建议吗? 我已经编写了滑动动画,但我不知道如何在div稍后成为视口中可见的时候触发滚动以触发动画。

谢谢大家!


不确定,但这个链接是否有帮助 https://angular.io/docs/ts/latest/guide/animations.html#!#parallel-animation-groups? - sandyJoshi
嗨Sandy,我已经查看了那个并行动画组的方法,它可以帮助链接动画,但似乎没有一种方法可以在滚动到页面较低位置后div进入视野时触发,这可能会在用户决定向下滚动到div时以可变时间发生。你知道有关此UI行为的任何解决方案吗? - Ultronn
7个回答

20

我创建了一个指令,当元素完全在视图内或其上边缘到达视图的上边缘时,立即发出事件。

这是一个 Plunker 示例: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();
  }
}

为了兼容IE11,您可以将window.scrollY更改为document.documentElement.scrollTop。 - Martin Cremer
这在页面上有多个元素时不能正常工作。 - godblessstrawberry
document.documentElement.scrollTop在OSX Safari中无法正常工作,始终为0。 - godblessstrawberry
这个在2020年使用还可以吗?或者有更好的解决方案吗? - hellmit100
2
更新:这段代码必须进行更新以在2020年正常工作。 - hellmit100
更新:为适应2020年的需求,此代码需要使用IntersectionObserver。 - ssougnez

10

Martin Cremer 的回答已经更新,适用于最新的 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();
    }
}

2
我已经创建了一个基础组件,提供了一个标志appearedOnce,如果该组件完全在视图内或其上边缘已到达视图的上边缘,则该标志将变为true。
@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/ (编辑)我已经将其转化为指令,在下面的另一个答案中。

嗨马丁,这是一个非常有趣的解决方案。你能描述一下如何在现有组件中实现相同的逻辑以及该组件需要什么导入吗?我对Angular还比较新,所以非常希望能够得到有关实现细节的详细说明。谢谢。 - Ultronn
我创建了一个小的plnkr: https://embed.plnkr.co/yIpA1mI1b9kVoEXGy6Hh/其中有一个示例组件(TurnGreenWhenInViewComponent),它扩展了AppearOnce并使用其appearedOnce标志,在出现一次后将其变为绿色。 - Martin Cremer
我已经添加了另一个几乎相同的答案,但作为指令。 - Martin Cremer
我尝试实现这个解决方案。 然而,当我调用super()时它告诉我需要给它2个参数。 查看您使用的构造函数: private element: ElementRef,private cdRef:ChangeDetectorRef。 应该是什么? - J Agustin Barrachina
@AgustinBarrachina 我猜您在谈论ElementRef和ChangeDetectorRef这两个对象。 - Martin Cremer

2

如果你想在特定组件中实现,有一个简单的方法:

最初的回答:

@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中。

原始答案:最初的回答。


2

Martin Cremer的回答非常完美。

除非你想在使用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>
<!-- ... -->

1

有一个更新的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();
    }
}


0
这是一个简单的无限滚动示例;当元素进入视口时,它会触发 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++;
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接