Angular CDK Overlay 彈出覆蓋物

Worktile發表於2019-06-13

為什麼使用Overlay?

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

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

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

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

安裝並且匯入模組

專案中如果沒有安裝CDK,要先安裝

  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按鈕,後面要用到它作為位置源origin

注入Overlay服務

在元件的constructor建構函式中注入Overlay服務,下面程式碼包括元件的定義

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

通過overlayRef附加模板

一切準備就緒後,這裡就是需要告訴Overlay彈出層要顯示的內容,直接彈出模板,在模板中定義,這裡用到的是overlayRef的attach方法,程式碼如下

 this.overlayRef.attach(new TemplatePortal(this.overlayTemplate, this.viewContainerRef));

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

注:attach方法用到了CDK裡面的Protals,attach方法接收的引數型別其實是TemplatePortal,因為這個說到底是動態建立元件,除此之外它還支援元件型別的ComponentPortal,關於Portals可以參考我前面的文章https://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.  withDefaultOffsetX 、 withDefaultOffsetY 設定相對基礎位置的偏移。
  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();

還有 top 、 left 、 bottom 、 right 方法提供各個方位的絕對定位,引數是在這個方位上的偏移值,如居上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 - 不提供任何處理

在滾動不做任何事情,呼叫 scrollStrategies 的 noop 方法

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

一旦使用者有滾動行為,立即關閉overlay彈層,呼叫 scrollStrategies 的 close 方法

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

可以配置config中的引數 threshold ,設定一個滾動畫素的臨界點,只有當滾動距離大於此引數時才會關閉overlay.

BlockScrollStrategy - 阻止滾動策略

該策略會阻止頁面級的滾動,呼叫 scrollStrategies 的 block 方法

   block = () => new BlockScrollStrategy(this._viewportRuler, this._document);

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

position: fixed;
width: 100%;
overflow-y: scroll;

RepositionScrollStrategy - 重定位滾動策略

一旦使用者有滾動行為,該策略會根據滾動的位置更新彈出層的位置,效果就是彈出層會跟隨滾動而滾動,相對於位置源的位置不變,呼叫 scrollStrategies 的 reposition 方法

   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 https://github.com/angular/components
cd components
yarn install // 如果提示沒有yarn 需要全域性裝下yarn,node版本要求10.x
npm run dev-app

 

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

文章來源:Worktile技術部落格

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

文章轉載請註明出處。

相關文章