Angular 4 - 滚动动画

29

我正在创建一个具有全屏宽度/高度div的网页。 在向下滚动时,我有两种方法。

点击滚动

//HTML
<a (click)="goToDiv('about')"></a>

//JS
    goToDiv(id) {
        let element = document.querySelector("#"+id);
        element.scrollIntoView(element);
      }

HostListener 滚动事件

  @HostListener("window:scroll", ['$event'])
  onWindowScroll($event: any): void {
    this.topOffSet = window.pageYOffset;
    //window.scrollTo(0, this.topOffSet+662);
  }

1. 如何添加滚动动画效果?

就像这样:

$('.scroll').on('click', function(e) {
    $('html, body').animate({
        scrollTop: $(window).height()
    }, 1200);
});

2. 怎么使用HostListener实现滚动到下一个div?


我认为这里有解决方案 https://dev59.com/tJnga4cB1Zd3GeqPZ4Un - Obaidul Haque
@ObaidulHaque HostListener 正常工作。不确定如何添加动画。 - Sumit Ridhal
在Host Listener中调用函数时使用CSS动画。这里有一个很好的指南:https://css-tricks.com/aos-css-driven-scroll-animation-library/ - Andrew Stalker
4个回答

23

4
最简单的方法 - g1ji
2
我会尝试这个(代码更少,你们这些懒人?) - Antonin
1
最好链接英文的MDN页面 :) https://developer.mozilla.org/docs/Web/CSS/scroll-behavior - massic80
1
有一个打字错误,应该是:scroll-behavior: smooth; - Adrien LAMOTTE
2
“Scroll-behaviour” 在 Firefox 和 Chrome 中表现良好,但在 IE 或 Edge 中不受支持。 - Richh94

11
这个很有趣。解决方案与大多数Angular 2事物一样,都是使用可观察对象。
  getTargetElementRef(currentYPos: int): ElementRef {
      // you need to figure out how this works
      // I can't comment much on it without knowing more about the page
      // but you inject the host ElementRef in the component / directive constructor and use normal vanillaJS functions to find other elements
  }
  //capture the scroll event and pass to a function that triggers your own event for clarity and so you can manually trigger
  scrollToSource: Subject<int> = new Subject<int>();
  @HostListener("window:scroll", ['$event'])
  onWindowScroll($event: any): void {
    var target = getTargetElementRef(window.pageYOffset);
    this.scrollTo(target);
  }

  scrollTo(target: ElementRef): void {
     // this assumes you're passing in an ElementRef, it may or may not be appropriate, you can pass them to functions in templates with template variable syntax such as: <div #targetDiv>Scroll Target</div> <button (click)="scrollTo(targetDiv)">Click To Scroll</button>
     this.scrollToSource.next(target.nativeElement.offsetTop);
  }

  //switch map takes the last value emitted by an observable sequence, in this case, the user's latest scroll position, and transforms it into a new observable stream
  this.scrollToSource.switchMap(targetYPos => {
       return Observable.interval(100) //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
           .scan((acc, curr) =>  acc + 5, window.pageYOffset) // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
           .do(position => window.scrollTo(0, position)) /// here is where you scroll with the results from scan
           .takeWhile(val => val < targetYPos); // stop when you get to the target
  }).subscribe(); //don't forget!

点击即可使用,这很容易。只需将scrollTo绑定到单击事件即可。

这仅适用于单向滚动,但这应该可以让您开始。如果需要向上滚动,则可以使扫描更智能化,从而进行减法,并在takeWhile内部使用一个函数来确定正确的终止条件,具体取决于向上或向下移动。

编辑:rxjs 5+兼容版本

  this.scrollToSource.pipe(switchMap(targetYPos => 
       interval(100).pipe( //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
           scan((acc, curr) =>  acc + 5, window.pageYOffset), // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
           takeWhile(val => val < targetYPos)) // stop when you get to the target
  )).subscribe(position => window.scrollTo(0, position)); // here is where you scroll with the results from scan

你的代码对我非常有用,谢谢。只是想补充一下,对于 Angular 4 及以上版本,你需要使用导入 'rxjs/add/operator/switchMap';,否则你会遇到 "..switchMap is not a function" 的问题。 - KeaganFouche
有没有想法如何将这段代码转换为适用于rxjs 6的代码? - user2304483
大部分都是相同的,只需在主要序列中使用管道运算符,直接导入间隔,并将 do 替换为 tap,尽管现在看来,我可能会将 do/tap 操作移动到订阅处理程序中。 - bryan60

1
我花了好几天的时间才搞明白这个问题。作为一个新手,我尝试了很多方法,但都不起作用。最终,我找到了一个解决方案,现在在这里发布出来。
有两个步骤:
1.当事物出现时进行动画处理。
2.滚动时让事物出现。
第一部分:我找到了这两个适合新手的教程:
1. 最基础的教程 2. 实际上当事物出现时会进行动画处理的教程
第二部分:我只是在这个答案中找到了解决方案。

第一部分:逐步操作:

  1. 将以下代码行添加到/src/app/app.module.ts文件中:import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  // Other arrays removed
  imports: [
    // Other imports
    BrowserAnimationsModule
  ],
})

在需要动画的组件中的component.ts文件中添加以下代码:import { trigger,state,style,transition,animate } from '@angular/animations'; 然后:
@Component({
  // Here goes the selector and templates and etc.
  animations: [
    trigger('fadeInOut', [
      state('void', style({
        opacity: 0
      })),
      transition('void <=> *', animate(1000)),
    ]),
  ]
})
  1. 最后,在您想要动画的HTML项目中添加[@fadeInOut]

如果一切正确,您现在应该有一个动画(但它会在网页加载时立即发生,而不是在滚动时发生)。

第二部分逐步操作:

  1. 创建一个名为appear.ts的文件,并复制粘贴此代码:
import {
    ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
  } from '@angular/core';
  import { Observable, Subscription, fromEvent } from 'rxjs';
  import { startWith } from 'rxjs/operators';
  //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 = 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();
    }
  }
  1. 使用import {AppearDirective} from './timeline/appear';导入它,并将其添加到imports中:
@NgModule({
  declarations: [
    // Other declarations
    AppearDirective
  ],
  // Imports and stuff

在类中的某个地方执行以下操作:
hasAppeared : boolean = false;
onAppear(){
    this.hasAppeared = true;
    console.log("I have appeared!");   // This is a good idea for debugging
  }

最后,在HTML中添加以下两个内容:
(appear)="onAppear()" *ngIf="hasAppeared" 

你可以通过在控制台查看消息“I have appeared!”来检查它是否起作用。

0

@bryan60的答案是可行的,但我不太喜欢它,我更喜欢使用TimerObservable,因为这似乎对其他团队成员来说更容易理解,并且未来的使用也更容易定制。

我建议您为涉及DOM操作、滚动和其他HTML元素相关问题的时间创建一个共享服务;然后您可以在该服务上拥有此方法(否则,在组件上拥有它不会有任何问题)。

  // Choose the target element (see the HTML code bellow):
  @ViewChild('myElement') myElement: ElementRef;

  this.scrollAnimateAvailable:boolean;

animateScrollTo(target: ElementRef) {
    if (this.helperService.isBrowser()) {
      this.scrollAnimateAvailable = true;
      TimerObservable
        .create(0, 20).pipe(
        takeWhile(() => this.scrollAnimateAvailable)).subscribe((e) => {
        if (window.pageYOffset >= target.nativeElement.offsetTop) {
          window.scrollTo(0, window.pageYOffset - e);
        } else if (window.pageYOffset <= target.nativeElement.offsetTop) {
          window.scrollTo(0, window.pageYOffset + e);
        }

        if (window.pageYOffset + 30 > target.nativeElement.offsetTop && window.pageYOffset - 30 < target.nativeElement.offsetTop) {
          this.scrollAnimateAvailable = false;
        }

      });
    }

  }



 scrollToMyElement(){
   this.animateScrollTo(this.myElement)
  }

你需要将元素传递给这个方法,以下是如何实现:

<a (click)="scrollToMyElement()"></a>
<!-- Lots of things here... -->
<div #myElement></div>

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