Angular CDK Overlay 彈出覆蓋物

爲何使用Overlay?

Overlay中文翻譯過來意思是覆蓋物,它是Material Design components for Angular中針對彈出動態內容這一場景的封裝,功能強大、使用方便,尤爲在開發本身的組件庫時,可讓你少寫許多代碼,能夠說只要是彈出內容的場景基本均可以使用Overlay.
咱們本身的組件庫中彈出場景基本都已經使用Overlay,如自定義Select、Cascader、Tree Select、Tooltip、Dialog等,總結最重要的的兩點好處:html

  1. 讓使用者再也不進行繁瑣的位置計算,而簡單經過參數配置就實現內容的定位,並且關於位置的各類狀況都有考慮到.
  2. 組件的彈出內容都是用Overlay實現,避免了各自實現的產生的不兼容,如相互遮蓋問題.

簡單示例 - 連結位置源的彈出

下面經過一個示例代碼來展現Overlay的使用,這種彈出場景相似於Tooltip,彈出的overlay內容是基於一個參照的位置源origin元素.node

安裝而且導入模塊

項目中若是沒有安裝CDK,要先安裝git

npm install @angular/cdk
複製代碼
導入OverlayModule
import {OverlayModule} from '@angular/cdk/overlay';

@NgModule({
  imports: [
    OverlayModule,
    // ...
  ]
})
export class AppModule {
}
複製代碼
示例模板內容
<div class="demo-trigger">
  <!--觸發位置源-->
  <button mat-raised-button
      cdkOverlayOrigin
      type="button"
      [disabled]="overlayRef"
      (click)="openWithConfig()">Open</button>
</div>

<!--彈出動態內容模板-->
<ng-template #overlay>
  <div class="demo-overlay">
    <div style="overflow: auto;">
      <ul><li *ngFor="let item of itemArray; index as i">{{itemText}} {{i}}</li></ul>
    </div>
  </div>
</ng-template>
複製代碼

除了彈出模板,上面模板中還有一個Open按鈕,後面要用到它做爲位置源origingithub

注入Overlay服務

在組件的constructor構造函數中注入Overlay服務,下面代碼包括組件的定義npm

@Component({
  selector: 'overlay-demo',
  templateUrl: 'connected-overlay-demo.html'
})
export class ConnectedOverlayDemo {
  @ViewChild(CdkOverlayOrigin, {static: false}) _overlayOrigin: CdkOverlayOrigin;
  @ViewChild('overlay', {static: false}) overlayTemplate: TemplateRef<any>;
  /**
   * 注入Overlay服務
   */
  constructor(
      public overlay: Overlay) { }  

  openWithConfig() {
  }
}
複製代碼

處理注入服務,上面代碼還經過ViewChild取到模板中的兩個對象,後面用到的時候再解釋.編程

構建位置策略

首先建立一個位置策略,這裏使用的是FlexibleConnectedPositionStrategy策略,先看代碼數組

const positionStrategy = this.overlay.position()
        .flexibleConnectedTo(this._overlayOrigin.elementRef)
        .withPositions([
        {
          originX: 'start',
          originY: 'bottom',
          overlayX: 'start',
          overlayY: 'top',
        }
      ]);
複製代碼

建立FlexibleConnectedPositionStrategy策略的方法flexibleConnectedTo必需要提供一個位置源參數,這裏使用的是
this._overlayOrigin.elementRef,彈出內容的位置是基於這個位置源的,this._overlayOrigin其實就是經過ViewChild取的模板中的Open按鈕.瀏覽器

調用建立方法
this.overlayRef = this.overlay.create({
      positionStrategy, // 位置策略
      scrollStrategy: this.overlay.scrollStrategies.reposition(), // 滾動策略
      direction: this.dir.value, // 可用性方面的設置,不用太關注
      minWidth: 200, // overlay層的最小寬度
      minHeight: 50 // overlay層的最小高度
      hasBackdrop: false // 是否顯示遮罩層
    });
複製代碼

方法會生成一個OverlayRef類型的對象overlayRef,用overlayRef來管理Overlay。bash

經過overlayRef附加模板

一切準備就緒後,這裏就是須要告訴Overlay彈出層要顯示的內容,直接彈出模板,在模板中定義,這裏用到的是overlayRef的attach方法,代碼以下app

this.overlayRef.attach(new TemplatePortal(this.overlayTemplate, this.viewContainerRef));
複製代碼

代碼中用到了this.overlayTemplate,經過ViewChild取到的顯示彈出內容的模板定義.

注:attach方法用到了CDK裏面的Protals,attach方法接收的參數類型實際上是TemplatePortal,由於這個說究竟是動態建立組件,除此以外它還支持組件類型的ComponentPortal,關於Portals能夠參考我前面的文章zhuanlan.zhihu.com/p/59719621

經過以上簡單的幾個步驟就實現動態內容的彈出,效果圖以下所示

屏幕錄製 2019-06-11 下午10.30.13.2019-06-11 22_38_52.gif

簡單示例 - 全局彈出

與上面的徹底不一樣,如今介紹下經過Overlay直接彈出內容在窗口上,不連結任何位置源,很是簡單隻須要更改下位置策略,看下使用的新位置策略的代碼

const positionStrategy = this.overlay.position()
      .global()
      .height('300px')
      .centerHorizontally()
      .top('70px');
複製代碼

調用global()返回的是全局的位置策略GlobalPositionStrategy,基於瀏覽器窗口絕對定位的位置策略。
以上代碼實現:水平居中,距離頂部70px,效果圖以下

屏幕錄製 2019-06-11 下午10.44.31.2019-06-11 22_47_32.gif

Overlay 位置策略

Overlay經過OverlayPositionBuilder服務提供了三個方法分別對應三種位置策略,OverlayPositionBuilder經過構造函數注入到了Overlay服務中,前面代碼this.overlay.position()返回的就是OverlayPositionBuilder類型的對象

ConnectedPositionStrategy - 連結點位置策略

注:該策略已被棄用,使用FlexibleConnectedPositionStrategy策略代替,可是這裏的討論能夠繼續,用以說明鏈接點的位置關係

connectedTo方法返回ConnectedPositionStrategy策略實例,該策略實現基於一個操做源上的位置點到Overlay彈出層的位置點鏈接關係的位置策略,建立策略的代碼以下

connectedTo(
      elementRef: ElementRef,
      originPos: OriginConnectionPosition,
      overlayPos: OverlayConnectionPosition): ConnectedPositionStrategy {
    return new ConnectedPositionStrategy(
        originPos, overlayPos, elementRef, this._viewportRuler, this._document, this._platform,
        this._overlayContainer);
  }
複製代碼

參數分別是:

  1. elementRef要鏈接的元素,通常是觸發彈出的元素
  2. originPos 鏈接元素的位置點
  3. overlayPos overlay的位置點

用圖表達它所維繫的位置關係

image.png

上圖所示的位置點組合只是其中一種狀況(左下點 - 左上點),位置配置代碼以下

{
  "originX": "start",
  "originY": "bottom",
  "overlayX": "start",
  "overlayY": "top"
}
複製代碼

在x方向上可枚舉值定義以下

export type HorizontalConnectionPos = 'start' | 'center' | 'end';
複製代碼

在y方向上可枚舉值定義

export type VerticalConnectionPos = 'top' | 'center' | 'bottom';
複製代碼

基於以上枚舉值能夠實現各類位置組合。

FlexibleConnectedPositionStrategy - 靈活的鏈接點位置策略

注:如今的源代碼ConnectedPositionStrategy策略最終也是經過關聯FlexibleConnectedPositionStrategy策略實現的,因此推薦直接使用該策略

經過flexibleConnectedTo方法返回FlexibleConnectedPositionStrategy策略實例,這是Overlay最複雜的一個位置策略,因此能稱上Flexible,經過指令方式使用Overlay時就是使用的這個策略,它在位置策略上有更多的控制,特性以下:

  1. withDefaultOffsetXwithDefaultOffsetY設置相對基礎位置的偏移。
  2. withPositions參數爲ConnectionPositionPair類型的數組,提供多種位置組合,當某一種位置組合的彈出內容超出窗口,就會應用對應其它的位置組合來避免內容不可見。
  3. withFlexibleDimensions 控制Overlay彈出層寬度和高度是否被限制在瀏覽器窗口內,參數設置爲ture時,寬度和高度會自適應到瀏覽器邊界,以滾動條形式展示內容。
  4. 等等位置方面其它可預見的細節的處理

建立策略的代碼以下

/**
   * Creates a flexible position strategy.
   * @param origin Origin relative to which to position the overlay.
   */
  flexibleConnectedTo(origin: FlexibleConnectedPositionStrategyOrigin):
    FlexibleConnectedPositionStrategy {
    return new FlexibleConnectedPositionStrategy(origin, this._viewportRuler, this._document,
        this._platform, this._overlayContainer);
  }
複製代碼

只有一個origin參數,提供了要連結的位置源元素引用。

GlobalPositionStrategy

全局位置策略,global方法返回GlobalPositionStrategy策略實例,無任何參數,建立策略的代碼以下

/**
   * Creates a global position strategy.
   */
  global(): GlobalPositionStrategy {
    return new GlobalPositionStrategy();
  }
複製代碼

GlobalPositionStrategy提供了全局定位的各類方法,而且能夠經過鏈式的方式調用,以下代碼

const strategy = this.overlay
  .position()
  .global()
  .width('500px')
  .height('100px')
  .centerHorizontally()
  .centerVertically();
複製代碼

還有topleftbottomright方法提供各個方位的絕對定位,參數是在這個方位上的偏移值,如居上10px參數就是'10px',這是這個偏移值會打破水平或者垂直方向上的居中。

PositionStrategy 位置策略接口

位置策略接口定義以下

import {OverlayReference} from '../overlay-reference';

/** Strategy for setting the position on an overlay. */
export interface PositionStrategy {
  /** 附加位置策略到overlay */
  attach(overlayRef: OverlayReference): void;

  /** 更新overlay element 元素的位置. */
  apply(): void;

  /** 當overlay調用detach時調用 */
  detach?(): void;

  /** Cleans up any DOM modifications made by the position strategy, if necessary. */
  dispose(): void;
}
複製代碼

接口定義了位置策略必須包含的方法簽名,這是面向對象編程中的經常使用的抽象方式。
OverlayRef在實現時只依賴PositionStrategy接口而不具體依賴某一個策略的實現,在建立OverlayRef須要提供一個具體的位置策略的實例(通常是在建立Overlay時配置),若是有須要還能夠實現本身的位置策略,實現本身的位置策略只須要實現這個接口而且定義接口簽名的具體實現。
符合面向對象的三大特性 封裝繼承多態,符合五大原則中的單一職責原則開放封閉原則,這種抽象思想很是值得學習

滾動策略

Overlay提供了全局服務ScrollStrategyOptions ,用它提供處理overlay滾動時的處理策略。

NoopScrollStrategy - 不提供任何處理

在滾動不作任何事情,調用scrollStrategiesnoop方法

noop = () => new NoopScrollStrategy();
複製代碼
CloseScrollStrategy - 關閉滾動策略

一旦用戶有滾動行爲,當即關閉overlay彈層,調用scrollStrategiesclose方法

close = (config?: CloseScrollStrategyConfig) => new CloseScrollStrategy(this._scrollDispatcher,
      this._ngZone, this._viewportRuler, config)
複製代碼

能夠配置config中的參數threshold,設置一個滾動像素的臨界點,只有當滾動距離大於此參數時纔會關閉overlay.

BlockScrollStrategy - 阻止滾動策略

該策略會阻止頁面級的滾動,調用scrollStrategiesblock方法

block = () => new BlockScrollStrategy(this._viewportRuler, this._document);
複製代碼

經過給頁面的html標籤增長樣式cdk-global-scrollblock來阻止頁面級別的滾動,樣式定義以下

position: fixed;
width: 100%;
overflow-y: scroll;
複製代碼

RepositionScrollStrategy - 重定位滾動策略

一旦用戶有滾動行爲,該策略會根據滾動的位置更新彈出層的位置,效果就是彈出層會跟隨滾動而滾動,相對於位置源的位置不變,調用scrollStrategiesreposition方法

reposition = (config?: RepositionScrollStrategyConfig) => new RepositionScrollStrategy(
      this._scrollDispatcher, this._viewportRuler, this._ngZone, config)
複製代碼

config能夠配置兩個參數,scrollThrottle參數控制滾動事件觸發從新更新位置的抖動頻率,autoClose參數配置當滾動事件發生時是否關閉overlay彈層(實現 關閉滾動策略 的功能)

滾動的觸發

不管是關閉Overlay仍是更新Overlay位置,都須要檢測滾動事件的觸發,這裏要用到CDK提供的處理滾動的服務(cdk/scrolling目錄下),主要用到ScrollDispatcher,一個處理全局滾動事件的觸發器.
關閉滾動策略重定位滾動策略 都是訂閱ScrollDispatcher的scrolled方法返回的流(後面統一叫scrolled流),有幾點要明確

  1. 全局頁面文檔的滾動定會觸發scrolled流
  2. overlay是否顯示backdrop(遮罩層)對 重定位滾動策略 有影響,顯示backdrop會阻止頁面局部元素的滾動,對全局頁面文檔滾動沒有影響,因此能夠看到的效果是,官方的overlay示例即便顯示backdrop層,reposition仍然起做用,但本身在實現overlay跟隨滾動的時候可能會失敗,由於根本觸發不了局部滾動事件。
  3. scrolled流是一個全局的滾動監聽,任何注入的CdkScrollable所關聯的元素滾動都會觸發scrolled流(若是滾動跟overlay沒有關係,那從新計算位置也沒有影響,計算後仍是原來的位置,不過這塊感受有待優化)
實現局部元素滾動Overlay層重定位

由於國內的軟件大部分是把整個窗口固定,而後再經過局部元素設置樣式overflow:scroll實現內容滾動,這裏僅提供思路

  1. 關閉backdrop,至於點擊backdrop後overlay關閉,就需本身實現了
  2. 根據滾動區域構造CdkScrollable示例,能夠用代碼遍歷origin元素的可滾動父元素

這塊理解起來並無那麼容易,很抽象,又是由overlay 、scroll、position策略、scroll策略組合起來的,如今能夠先作了解,須要瞭解細節時再翻看源代碼。

總結

文字首先介紹了使用Overlay的好處,以及在咱們的組件庫中都有那些組件使用了Overlay,而後經過簡單的示例代碼帶你們瞭解Overlay的使用,後面又介紹了Overlay的位置策略和滾動策略,但願看到最後的各位有些幫助。
Overlay須要說的內容很是的多,並且總體封裝的思想也很值得學習,這裏就簡單介紹這麼多,有任何建議或者疑問歡迎留言討論。
另外文中的示例基本是在Material Design Components for angular 中針對Overlay的Demo,有須要能夠自行clone代碼學習

示例運行命令
git clone github.com/angular/com…
cd components
yarn install // 若是提示沒有yarn 須要全局裝下yarn,node版本要求10.x
npm run dev-app



本文做者:Worktile工程師 楊振興

文章來源:Worktile技術博客

歡迎訪問交流更多關於技術及協做的問題。

文章轉載請註明出處。

相關文章
相關標籤/搜索