記事ディレクトリ
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;
}