Angular CDK Scrolling の ViewportRuler (ソースコード読み込み)


ViewportRuler は、ブラウザーのビューポートの境界を測定する注入可能なサービスです。

1. ソースコード

// 默认情况下限制调整大小事件的时间(单位为ms)。
export const DEFAULT_RESIZE_TIME = 20;

export interface ViewportScrollPosition {
    
    
  top: number;
  left: number;
}

/**
 * 获取浏览器视口边界的服务。
 * @docs-private
 */
@Injectable({
    
    providedIn: 'root'})
export class ViewportRuler implements OnDestroy {
    
    
  // 缓存浏览器视口大小
  private _viewportSize: {
    
    width: number; height: number} | null;

  // 视口改变的事件流
  private readonly _change = new Subject<Event>();

  // 事件监听器,用于处理视口更改事件
  private _changeListener = (event: Event) => {
    
    
    this._change.next(event);
  };

  // 用于引用正确的文档/窗口
  protected _document: Document;

  constructor(
    private _platform: Platform,
    ngZone: NgZone,
    @Optional() @Inject(DOCUMENT) document: any,
  ) {
    
    
    this._document = document;

    ngZone.runOutsideAngular(() => {
    
    
      if (_platform.isBrowser) {
    
    
        const window = this._getWindow();

        // 我们自己绑定事件,而不是通过诸如 RxJS 的 `fromEvent` 之类的东西,这样我们就可以确保它们被绑定在 NgZone 之外。
        window.addEventListener('resize', this._changeListener);
        window.addEventListener('orientationchange', this._changeListener);
      }

      // 清除缓存的位置,以便下次需要时重新测量视口。 我们不需要跟踪订阅,因为它是在销毁时完成的。
      this.change().subscribe(() => (this._viewportSize = null));
    });
  }

  ngOnDestroy() {
    
    
    if (this._platform.isBrowser) {
    
    
      const window = this._getWindow();
      window.removeEventListener('resize', this._changeListener);
      window.removeEventListener('orientationchange', this._changeListener);
    }

    this._change.complete();
  }

  // 返回视口的宽度和高度。实际上是 window.innerWidth 和 window.innerHeight。
  getViewportSize(): Readonly<{
    
    width: number; height: number}> {
    
    
    if (!this._viewportSize) {
    
    
      this._updateViewportSize();
    }

    const output = {
    
    width: this._viewportSize!.width, height: this._viewportSize!.height};

    // 如果我们不是在浏览器上,不要缓存大小,因为它会被模拟出来.
    if (!this._platform.isBrowser) {
    
    
      this._viewportSize = null!;
    }

    return output;
  }

  // Gets a ClientRect for the viewport's bounds.
  getViewportRect() {
    
    
    /**
     * 使用 document element's bounding rect,而不是 document element's bounding rect。
     * 因为在 Chrome and IE 中,window scroll properties 和 client coordinates (boundingClientRect, clientX/Y, etc.) are in different conceptual viewports。
     * 在大多数情况下,这些视口是等效的。但是当页面被捏缩放(在支持触摸的设备上)时,他们可能会不同的。
     * 我们使用 documentElement 而不是 body,因为默认情况下(没有css重置的情况下)浏览器通常会为文档正文提供 8px 的边距,getBoundingClientRect() 中不包含该边距。
     */

    const scrollPosition = this.getViewportScrollPosition();

    // window.innerWidth 和 window.innerHeight
    const {
    
    width, height} = this.getViewportSize();

    return {
    
    
      top: scrollPosition.top,
      left: scrollPosition.left,
      bottom: scrollPosition.top + height,
      right: scrollPosition.left + width,
      height,
      width,
    };
  }

  // 获取视口的(上、左)滚动位置。
  getViewportScrollPosition(): ViewportScrollPosition {
    
    
    // 虽然我们可以在 SSR 期间获得对假文档的引用,但它没有 getBoundingClientRect。
    if (!this._platform.isBrowser) {
    
    
      return {
    
    top: 0, left: 0};
    }

    /**
     * 视口的左上角由 document body 的滚动位置决定,通常只是 (scrollLeft, scrollTop)。
     * 但是,Chrome 和 Firefox 对 document.body 或 document.documentElement 是否是滚动元素存在分歧,因此阅读 scrollTop 和 scrollLeft 是不一致的。
     * 但是,使用 document.documentElement 的边界矩形始终有效,其中 top 和 left 值将等于滚动位置的负数。
     */
    const document = this._document;
    const window = this._getWindow();
    const documentElement = document.documentElement!;
    const documentRect = documentElement.getBoundingClientRect();

    const top =
      -documentRect.top ||
      document.body.scrollTop ||
      window.scrollY ||
      documentElement.scrollTop ||
      0;

    const left =
      -documentRect.left ||
      document.body.scrollLeft ||
      window.scrollX ||
      documentElement.scrollLeft ||
      0;

    return {
    
    top, left};
  }

  /**
   * 返回视口大小变化时发出的流。 此流在 Angular ngZone 之外发出。
   * @param throttleTime 阻塞流的时间,以毫秒为单位。
   */
  change(throttleTime: number = DEFAULT_RESIZE_TIME): Observable<Event> {
    
    
    return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change;
  }

  // 如果可用,请使用注入文档的默认视图,或者回退到全局窗口引用
  private _getWindow(): Window {
    
    
    return this._document.defaultView || window;
  }

  // 更新缓存的视口大小
  private _updateViewportSize() {
    
    
    const window = this._getWindow();
    this._viewportSize = this._platform.isBrowser
      ? {
    
    width: window.innerWidth, height: window.innerHeight}
      : {
    
    width: 0, height: 0};
  }
}

二、核心

ビューポートの変更を監視する

  constructor(
    private _platform: Platform,
    ngZone: NgZone,
    @Optional() @Inject(DOCUMENT) document: any,
  ) {
    
    
    this._document = document;

    ngZone.runOutsideAngular(() => {
    
    
      if (_platform.isBrowser) {
    
    
        const _window = this._document.defaultView || window;
         
        // 监听视口的大小变化
        _window.addEventListener('resize', (event: Event) => {
    
    
          this._change.next(event);
        });

        // 监听方向改变事件,当设备的方向变化(设备横向持或纵向持)此事件被触发
        _window.addEventListener('orientationchange', (event: Event) => {
    
    
          this._change.next(event);
        });
      }

      this.change().subscribe(() => (this._viewportSize = null));
    });
  }

getViewportSize ビューポートの幅と高さを取得する

  getViewportSize(): Readonly<{
    
    width: number; height: number}> {
    
    
    return {
    
    width: window.innerWidth, height: window.innerHeight};
  }

getViewportScrollPosition ビューポートの左上のスクロール位置を取得します

  getViewportScrollPosition(): ViewportScrollPosition {
    
    
    if (!this._platform.isBrowser) {
    
    
      return {
    
    top: 0, left: 0};
    }

    const top =
      -this._document.documentElement.getBoundingClientRect().top ||
      this._document.body.scrollTop ||
      this._window.scrollY ||
      this._document.documentElement.scrollTop ||
      0;

    const left =
      -this._document.documentElement.getBoundingClientRect().left ||
      this._document.body.scrollLeft ||
      this._window.scrollX ||
      this._document.documentElement.scrollLeft ||
      0;

    return {
    
    top, left};
  }

getViewportRect ビューポートの ClientRect を取得します

  getViewportRect() {
    
    
    const scrollPosition = this.getViewportScrollPosition();
    const {
    
    width, height} = this.getViewportSize();

    return {
    
    
      top: scrollPosition.top,
      left: scrollPosition.left,
      bottom: scrollPosition.top + height,
      right: scrollPosition.left + width,
      height,
      width,
    };
  }

change ビューポートの変更を購読する

  change(throttleTime: number = DEFAULT_RESIZE_TIME): Observable<Event> {
    
    
    return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change;
  }

おすすめ

転載: blog.csdn.net/Kate_sicheng/article/details/125364257