Cocos——UI 多端適配之道

前端同學通常都用媒體查詢或 rem 做多端適配,但是在 Cocos 上 CSS 不復存在。那你知道在 Cocos 上如何做到多端適配嗎?本文從需求背景出發,帶你領略 Cocos 的多端適配之道~

背景

某一天接到了新需求,自己看了設計同學給的設計稿後瞬間感覺頭大,分析了下主要有以下難點:

  1. 題目背景需爲同一張背景圖,在不同端上要顯示背景圖的不同區域

  2. 標題欄上的倒計時、題干與最小化按鈕的貼邊距離在各端各不相同

  3. 選項背景圖需根據選項長度自動拉伸,同時保證兩側圓角不被拉伸

如果這種適配方案採用 CSS 實現的話,肯定少不了一大堆的媒體查詢,作爲前端同學來說 CSS 實現肯定更爲熟悉,但這也會導致樣式代碼冗長繁瑣,對開發者來說是一種折磨。業務中這幾年引進了 Cocos 遊戲引擎來實現新題型,曾經我們那樣熟悉的 CSS 在 Cocos 中將不復存在,這時在 Cocos 上我們要如何實現這種多端適配呢?

分析

針對這個需求,我們將適配的過程拆分成以下幾點:

  1. 多端適配背景圖

  2. 多端適配貼邊節點

  3. 選項背景圖九宮格切割

多端適配背景圖

  1. 什麼是設計分辨率和屏幕分辨率?

    在 Cocos 上做多端適配需要先了解什麼是設計分辨率和屏幕分辨率。根據 Cocos 官方文檔的介紹,設計分辨率 是內容生產者在製作場景時使用的分辨率藍本,而 屏幕分辨率 是遊戲在設備上運行時的實際屏幕顯示分辨率。在實際開發中,設計分辨率其實就是設計同學在設計稿中使用最多的尺寸,一般來說都是 iPhone 6 的 667*375,幾乎所有的設計稿都以這個尺寸來出圖,然後纔會針對不同端( PC 、iPad、iPhoneX 等)來單獨出適配的設計稿圖。所以我們在 Cocos 中 canvas 的大小通常就設置成寬爲 667,高爲 375 的設計分辨率,在此分辨率上完成基本的功能開發。

  2. 設計分辨率和屏幕分辨率的關係?

    假設我們的設計分辨率與屏幕分辨率同爲 667 x 375,這時候 canvas 不用縮放就可以完美適配屏幕;假設我們的設計分辨率爲 667 x 375,而實際屏幕分辨率爲 1334 x 750,這個時候 canvas 就需要放大兩倍才能夠完美適配屏幕。canvas 進行縮放時,場景下所有的節點都能夠享受到基於設計分辨率的智能縮放

  3. Fit Height 和 Fit Width

    上一點舉出的例子中,當設計分辨率爲 667 x 375 且屏幕分辨率爲 1334 x 750 時,場景需要放大兩倍才能夠完美適配屏幕,但這個的前提是設計分辨率和屏幕分辨率的寬高比一致。假設設計分辨率的寬高比與屏幕分辨率的寬高比不一致(這是在多端適配下常見的問題),這個時候該怎麼辦呢?canvas 組件提供了 Fit Height 與 Fit Width 兩種適配模式來幫助我們解決。該怎麼理解這兩種適配模式?

    我們以下面這個場景作爲基礎場景,紫色框爲我們的設計分辨率,藍色框爲實際場景:

    先看看屏幕分辨率寬高比小於設計分辨率寬高比的情況(iPad 情況)。

    我們先設置爲 Fit Height 模式看看效果,會發現設計分辨率的高度會自動撐滿屏幕的高度,而由於屏幕分辨率寬高比比設計分辨率小,所以屏幕兩邊也會被裁掉一部分背景圖。

    我們再設置爲 Fit Width 模式看看效果,會發現設計分辨率的寬度會自動撐滿屏幕的寬度,而由於屏幕分辨率寬高比比設計分辨率小,所以屏幕上下會多顯示一部分背景圖。

    再看看屏幕分辨率寬高比大於設計分辨率寬高比的情況(iPhoneX 情況)

    我們先設置爲 Fit Height 模式看看效果,會發現設計分辨率的高度會自動撐滿屏幕的高度,而由於屏幕分辨率寬高比比設計分辨率大,所以屏幕兩邊也會多顯示一部分背景圖。

    我們再設置爲 Fit Width 模式看看效果,會發現設計分辨率的寬度會自動撐滿屏幕的寬度,而由於屏幕分辨率寬高比比設計分辨率大,所以屏幕上下會被裁掉一部分背景圖。

  4. 背景多端適配用什麼模式?

    經過上面 Fit Height 與 Fit Width 兩種模式的解釋,現在你能確定本文第一張圖中每個端使用哪種模式進行適配嗎?在屏幕分辨率寬高比小於設計分辨率寬高比(iPad 情況)時,我們希望在寬度一致的情況下在上下兩側展示更多的背景區域,這個時候就需要使用 Fit Width;在屏幕分辨率寬高比大於設計分辨率寬高比(iPhoneX 情況)時,我們希望在高度一致的情況下在左右兩側展示更多的背景區域,這個時候就需要使用 Fit Height。

    在代碼中我們可以通過獲取當前視圖大小來得到實際屏幕分辨率的寬高比,根據寬高比來決定是使用 Fit Height 模式還是 Fit Width 模式。

    exportfunction setCanvasScaleMode(canvas: cc.Canvas) {
      const standardRadio = 16 / 9; // 標準寬高比,差不多就是iPhone6的寬高比(橫屏),一般設計稿以此爲標準  const screenSize = cc.view.getFrameSize();
      const currentRadio = screenSize.width / screenSize.height; // 寬高比  if (currentRadio <= standardRadio) {
        // 偏方形的屏幕,代表是iPad之類的。
        canvas.fitHeight = false;
        canvas.fitWidth = true;
      } else {
        // 偏長的屏幕,代表是ipx。太長了,高度顯得小,所以優先適配高度
        canvas.fitWidth = false;
        canvas.fitHeight = true;
      }
    }
  5. 確定了模式之後我們還需要確保背景不會出現黑邊,因爲當在 iPad 情況下使用 Fit Width 模式時,上下兩側會展示更多的背景區域,如果背景圖片沒有那麼高的話上下兩側就會出現黑邊;同理當在 iPhoneX 情況下使用 Fit Height 模式時,左右兩側會展示更多的背景區域,如果背景圖片沒有那麼寬的話左右兩側也會出現黑邊。這時我們需要設計同學提供的背景圖片時能夠覆蓋 iPad 的高度與 iPhoneX 的寬度,背景圖片應大於設計分辨率,並在上下左右四個方向都預留一定的長度來保證背景適配時不會出現黑邊。

  6. 特殊情況

    細心的同學可能已經發現了, PC 端與 iPhone7 端的寬高比其實是一樣的,按照我們上面的想法這兩端應該顯示一樣的背景區域,同時由於 PC 端的寬高比 iPhone7 的寬高要大,而場景中的所有節點都能享受到基於設計分辨率的智能縮放,所以 PC 端的節點應該比 iPhone7 的節點大。但是在第一張設計稿圖中,設計同學要求 PC 端要佔據更多的背景區域,同時其中節點的大小也與 iPhone7 中節點的大小保持相同,以保證 PC 端題目顯示的美觀,這個時候我們就需要單獨對 PC 端的情況做適配,將背景圖與節點都做下縮放。

多端適配貼邊節點

  1. Widget 組件爲何物?

    Widget 組件爲 Cocos 中的一個 UI 佈局組件,用於將當前節點對齊到父節點的任意位置,我們通過設置 Widget 組件的各種數值可以讓節點對齊上邊界、對齊下邊界、對齊左邊界、對齊右邊界、水平方向居中和豎直方向居中。當場景中有節點需要貼邊時 Widget 組件是不二的選擇。

  1. 哪個節點作爲貼邊節點對齊的父節點?

    當有節點需要貼邊時,我們希望的是無論屏幕分辨率如何改變,節點總是能在屏幕的固定位置出現。比如第一張設計稿圖中的倒計時節點,我們希望在不同屏幕分辨率的情況下它都能夠固定在屏幕左上角,不會出現隨着屏幕分辨率的改變而移到右上角的情況。所以貼邊節點指定的父節點大小要跟屏幕的大小一致,哪個節點最合適呢?

    沒錯,答案就是 canvas 節點!在我們使用 Fit Height 和 Fit Width 模式時,canvas 節點會佔據屏幕的大小,這時需要貼邊的節點相對於 canvas 節點設置貼邊距離實際上就是相對屏幕設置貼邊距離。

  2. 多端貼邊距離設置

    根據設計同學的要求,貼邊節點(例如倒計時節點)在 PC 端、iPad 端、iPhoneX 端和 iPhone7 端貼邊的距離都是不一樣的,這個時候我們如何根據不同端分別設置貼邊距離呢?

    首先明確,設置多端節點的貼邊距離實際上就是更改節點 Widget 組件在各個方向上對齊的數值,例如倒計時組件,我們先判斷當前是哪一個端,然後再根據該端來改變其 Widget 組件的 top 與 left 數值。

    由於 Widget 的設置邏輯不僅需要在不同貼邊節點執行,還需要在每個貼邊節點的不同端情況下執行,使用場景衆多,所以我們可以把這段邏輯抽出來作爲一個通用腳本組件,再分別添加到需要的節點上。以下是我們抽出來的一個 UIAdaptor 腳本組件:

    @ccclassexportdefaultclass UIAdaptor extends cc.Component {
      @property({
        type: Number,
        tooltip: `app: ${PlatformTypes.APP}, iPad: ${PlatformTypes.iPad},  PC : ${PlatformTypes. PC }, iPhoneX: ${PlatformTypes.iPhoneX}, AndroidiPad: ${PlatformTypes.AndroidiPad}`,
      })
      platform: PlatformTypes = -1;
        
      @property({
        type: [Number],
        tooltip: 'left, top, right, bottom, horizontalCenter, verticalCenter',
      })
      widgetOptions: Array<number> = [
        Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
        Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
      ];
        
      @property({
        type: Number,
        tooltip: `ONCE: ${WidgetAlignModes.ONCE}, ON_WINDOW_RESIZE: ${WidgetAlignModes.ON_WINDOW_RESIZE}
          ALWAYS: ${WidgetAlignModes.ALWAYS}
        `,
      })
      widgetAlignModes = WidgetAlignModes.ON_WINDOW_RESIZE;
        
      @property({
        type: [Number],
        tooltip: 'width, height',
      })
      size: Array<number> = [];
        
      @property({
        type: [Number],
        tooltip: 'x, y',
      })
      scale: Array<number> = [];
        
      @property({
        type: Number,
      })
      fontSize = -1;
        // 對節點的 Widget 組件做適配  private fitWidget() {
        const widget = this.getComponent(cc.Widget);
        if (!widget) {
          return;
        }
        enum WidgetOptions {
          left,
          top,
          right,
          bottom,
          horizontalCenter,
          verticalCenter,
        }
        this.widgetOptions.forEach((value: number, index: number) => {
          if (typeof value !== 'number' || value >= Number.MAX_SAFE_INTEGER - 1) {
            return;
          }
          const optionType = WidgetOptions[index];
          widget[optionType] = value;
          widget[`isAlign${optionType.replace(optionType[0], optionType[0].toUpperCase())}`] = true;
        });
        
        widget.alignMode = this.widgetAlignModes;
      }
        // 對節點的大小做適配  private fitSize() {
        const [width, height] = this.size;
        this.node.setContentSize(
          width,
          height
        );
      }
        // 對節點的字體大小做適配  private fitFontSize() {
        const label = this.getComponent(cc.Label);
        label.fontSize = this.fontSize;
      }
        // 對節點的縮放做適配  private fitScale() {
        const { scaleX: originalScaleX, scaleY: originalScaleY } = this.node;
        const [x, y = x] = this.scale;
        this.node.setScale(originalScaleX * x, originalScaleY * y);
      }
        
      privatefit() {
        if (this.size.length) {
          this.fitSize();
        }
        
        if (this.fontSize >= 0) {
          this.fitFontSize();
        }
        
        if (this.scale.length) {
          this.fitScale();
        }
        
        this.fitWidget();
        
        
        const widget = this.getComponent(cc.Widget);
        if (widget) {
          widget.updateAlignment();
        }
      }
        
      start() {
        // 獲取當前場景所在的端    const platform = getPlatformType();
        if (platform === this.platform) {
          this.fit();
        }
      }
    }

    可以看到這個腳本組件不僅可以設置節點的 Widget 組件,還可以設置節點的 size、scale、fontSize 等,使用這一個腳本組件就可以實現多端下節點貼邊距離、大小、縮放等的設置。我們還是看回剛纔說到的倒計時節點,使用 UIAdaptor 腳本組件後會是什麼樣子?

    由於每個端上倒計時節點的貼邊距離都不相同,所以我們針對每個端都在倒計時節點上添加一個 UIAdaptor 腳本組件,填寫不同的 Platform( PC 、iPhoneX、iPad、Android iPad)與 Widget options(left、top、right、bottom、horizontalCenter、verticalCenter),當場景加載執行到這些腳本時會逐個去判斷當前場景所在的端是不是與 UIAdaptor 選擇的端一致,如果不是則跳過,如果是則根據填寫的數值進行設置。通過這種方式我們可以無代碼實現貼邊節點的多端適配。

選項背景圖九宮格切割

  1. 爲什麼要對背景圖做處理?

    設計同學會給出第一張設計稿圖中選項的切圖,切圖長下面這個樣子:

    如果我們不對圖片做任何處理,直接將它扔進選項,讓它根據選項長度自動拉伸,會有什麼狀況發生呢?

    可以看到,在選項長度較大的情況下,選項的背景圖展現出了一個很詭異的形狀,四個圓角被拉伸地很不協調,如果被設計同學看到又少不了一通吐槽... 我們希望的是無論選項有多長,四個圓角都能夠保持原始狀態,不被選項長度所影響,而這種未經處理的圖片顯然不符合我們的需求。

  2. 何爲九宮格切割?

    爲了讓開發者能夠製作可任意拉伸的 UI 圖像,Cocos Creator 中提供了針對圖像資源的九宮格切割方式。我們在 Cocos Creator 中選中圖像資源進行編輯,會出現一個編輯圖像的彈窗:

    在這裏我們可以移動綠色線條將圖片資源切割成九部分,每個部分的拉伸規則如下:

    我們將選項按鈕的四個圓角切割到九宮格的四個角落,這樣無論選項如何拉伸,四個圓角始終能夠保持原始狀態,不會因爲選項長度的變化而縮放拉伸。來看看對圖像資源進行九宮格切割後的效果:

    怎麼樣,選項看起來是不是比剛纔協調了很多?

  3. 九宮格切割注意事項

    通常來說設計同學提供切圖時會提供切圖的一倍圖、二倍圖和三倍圖,選擇選項按鈕切圖的時候最好選擇跟設計分辨率下按鈕大小相近的倍圖。假設按鈕切圖的一倍圖高度爲 44,二倍圖高度爲 88,三倍圖高度爲 132,而在設計分辨率下按鈕的高度爲 88,這個時候我們就要選擇按鈕切圖的二倍圖。

    如果選擇一倍圖做九宮格切割,由於一倍圖的尺寸過小,四個圓角也會變得很小,如下圖:

    如果選擇三倍圖做九宮格切割,由於三倍圖尺寸過大,四個圓角也會變得很大,如下圖:

參考文章

  1. 多分辨率適配方案

  2. 製作可任意拉伸的 UI 圖像

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/brReU9a45tfZB5_2TxUzxg