畫面更新率

影格速率 API 可讓應用程式向 Android 平台告知其預期的影格速率,且適用於指定 Android 11 (API 級別 30) 以上版本的應用程式。傳統上,大多數裝置只支援單一螢幕刷新率,通常為 60 Hz,但這項情況已開始改變。許多裝置現在支援額外的刷新率,例如 90 Hz 或 120 Hz。部分裝置支援無縫切換刷新率,其他裝置則會短暫顯示黑色畫面,通常持續一秒。

API 的主要用途是讓應用程式能更有效地利用所有支援的螢幕更新率。舉例來說,如果應用程式播放的 24Hz 影片會呼叫 setFrameRate(),裝置可能會將顯示器的螢幕刷新率從 60Hz 變更為 120Hz。這項新刷新率可讓 24 Hz 影片流暢播放,且不會出現抖動,也不需要 3:2 的下拉式縮放,因為在 60 Hz 螢幕上播放相同影片時,需要使用這項技術。這樣即可提供更優質的使用者體驗。

基本用法

Android 提供多種方式存取及控制介面,因此 setFrameRate() API 有幾個版本。每個 API 版本都會使用相同的參數,並以相同方式運作:

應用程式不需要考量實際支援的螢幕更新率,只要呼叫 Display.getSupportedModes() 即可取得,以便安全地呼叫 setFrameRate()。舉例來說,即使裝置只支援 60 Hz,您仍可呼叫 setFrameRate(),並使用應用程式偏好的影格速率。如果裝置與應用程式的影格速率不相符,則會維持目前的顯示器刷新率。

如要查看呼叫 setFrameRate() 是否會導致顯示器更新率變更,請呼叫 DisplayManager.registerDisplayListener()AChoreographer_registerRefreshRateCallback(),註冊顯示器變更通知。

呼叫 setFrameRate() 時,請盡量傳入確切的幀率,而非四捨五入為整數。舉例來說,在以 29.97Hz 錄製的影片中,請傳入 29.97,而非四捨五入為 30。

針對影片應用程式,傳遞至 setFrameRate() 的相容性參數應設為 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,以便向 Android 平台提供額外提示,指出應用程式會使用下拉功能,以便適應不相符的螢幕更新率 (這會導致畫面抖動)。

在某些情況下,影片介面會停止提交影格,但仍會在畫面上顯示一段時間。常見的情況包括播放影片結束時,或使用者暫停播放時。在這種情況下,請將影格速率參數設為 0,並呼叫 setFrameRate(),藉此清除畫面的影格速率設定,並將其還原為預設值。在銷毀途徑時,或當使用者切換至其他應用程式而導致途徑隱藏時,就不需要清除這類影格速率設定。只有在途徑仍可見但未使用時,才需要清除影格速率設定。

非流暢的畫格速率切換

在某些裝置上,切換刷新率可能會造成視覺中斷,例如畫面會黑一兩秒。這種情況通常發生在機上盒、電視面板和類似裝置上。根據預設,Android 架構不會在呼叫 Surface.setFrameRate() API 時切換模式,以避免發生這類視覺中斷情形。

部分使用者偏好在較長影片的開頭和結尾加入視覺中斷畫面。這樣一來,螢幕的刷新率就能與影片影格速率相符,並避免影格速率轉換產生的瑕疵,例如在播放電影時出現 3:2 的下拉鋸齒。

因此,如果使用者和應用程式都選擇加入,即可啟用非無縫的螢幕更新率切換功能:

  • 使用者:如要啟用這項功能,使用者可以啟用「Match content frame rate」使用者設定。
  • 應用程式:如要選擇加入,應用程式可以將 CHANGE_FRAME_RATE_ALWAYS 傳遞至 setFrameRate()

建議您一律使用 CHANGE_FRAME_RATE_ALWAYS 播放長時間的影片,例如電影。這是因為比起變更更新率時造成的中斷,比對影片影格速率的好處更為重要。

其他建議

請按照下列建議處理常見情境。

多個表面

Android 平台可正確處理有多個介面且設定的顯示幀率不同的情境。如果應用程式有多個具有不同影格速率的介面,請為每個介面以正確的影格速率呼叫 setFrameRate()。即使裝置同時執行多個應用程式,使用分割畫面或子母畫面模式,每個應用程式都能安全地為自己的途徑呼叫 setFrameRate()

平台未變更為應用程式的影格速率

即使裝置支援應用程式在呼叫 setFrameRate() 時指定的影格速率,裝置仍可能不會將螢幕切換至該刷新率。舉例來說,優先順序較高的途徑可能會有不同的影格速率設定,或是裝置可能處於省電模式 (針對顯示器的螢幕更新率設定限制,以便節省電力)。即使裝置在正常情況下會切換顯示更新率,應用程式仍必須在裝置未將顯示更新率切換為應用程式的影格率設定時正常運作。

當螢幕刷新率不符合應用程式影格速率時,應用程式會決定如何回應。對於影片,影格速率會固定為來源影片的速率,且必須下拉才能顯示影片內容。遊戲可能會選擇以螢幕更新率執行,而非維持偏好的影格速率。應用程式不應根據平台的運作方式,變更傳遞至 setFrameRate() 的值。無論應用程式如何處理平台未調整以符合應用程式要求的情況,都應將其設為應用程式偏好的影格速率。這樣一來,如果裝置條件變更,可使用其他顯示器更新率,平台就會取得正確資訊,切換至應用程式偏好的影格速率。

如果應用程式不會或無法以顯示器更新率執行,則應使用平台的其中一個機制來設定呈現時間戳記,為每個影格指定呈現時間戳記:

使用這些時間戳記可避免平台過早顯示應用程式影格,進而導致不必要的抖動。正確使用影格呈現時間戳記有點棘手。如為遊戲,請參閱影格間隔指南,進一步瞭解如何避免卡頓,並考慮使用 Android Frame Pacing 程式庫

在某些情況下,平台可能會切換為應用程式在 setFrameRate() 中指定的影格速率的倍數。舉例來說,應用程式可能會以 60Hz 呼叫 setFrameRate(),而裝置可能會將螢幕切換為 120Hz。發生這種情況的其中一個原因,是其他應用程式具有影格速率設定為 24Hz 的途徑。在這種情況下,以 120Hz 執行螢幕時,60Hz 途徑和 24Hz 途徑都能執行,且不需要下拉。

如果螢幕以應用程式影格速率的倍數執行,應用程式應為每個影格指定呈現時間戳記,以免產生不必要的鋸齒。對於遊戲而言,Android Frame Pacing 程式庫可協助正確設定影格顯示時間戳記。

setFrameRate() 與 preferredDisplayModeId

WindowManager.LayoutParams.preferredDisplayModeId 是另一種應用程式可向平台指示其影格速率的方式。有些應用程式只想變更螢幕更新率,而非變更其他顯示模式設定,例如螢幕解析度。一般來說,請使用 setFrameRate() 而非 preferredDisplayModeIdsetFrameRate() 函式更容易使用,因為應用程式不必搜尋顯示模式清單,即可找到具有特定影格速率的模式。

在有多個途徑以不同影格速率執行的情況下,setFrameRate() 可讓平台有更多機會選擇相容的影格速率。舉例來說,假設有兩個應用程式在 Pixel 4 上以分割畫面模式執行,其中一個應用程式播放 24Hz 影片,另一個應用程式則向使用者顯示可捲動的清單。Pixel 4 支援兩種螢幕刷新率:60 Hz 和 90 Hz。使用 preferredDisplayModeId API 時,影片介面會強制選取 60Hz 或 90Hz。透過以 24 Hz 呼叫 setFrameRate(),影片介面會向平台提供更多來源影片影格速率的資訊,讓平台選擇 90 Hz 做為顯示刷新率,這在這種情況下比 60 Hz 更為理想。

不過,在某些情況下,應使用 preferredDisplayModeId 而非 setFrameRate(),例如:

  • 如果應用程式想要變更解析度或其他顯示模式設定,請使用 preferredDisplayModeId
  • 如果模式切換功能輕量且不太可能讓使用者察覺,平台只會在回應對 setFrameRate() 的呼叫時切換顯示模式。如果應用程式偏好切換螢幕更新率,即使需要重模式切換 (例如在 Android TV 裝置上),請使用 preferredDisplayModeId
  • 如果應用程式無法處理以應用程式影格速率的倍數執行的顯示作業,就必須在每個影格上設定呈現時間戳記,因此應使用 preferredDisplayModeId

setFrameRate() 與 preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate 會在應用程式視窗上設定偏好的影格速率,且該速率適用於視窗內的所有介面。應用程式應指定其偏好的影格速率,無論裝置支援的刷新率為何 (類似 setFrameRate()),以便讓排程器更清楚瞭解應用程式預期的影格速率。

對於使用 setFrameRate() 的介面,系統會忽略 preferredRefreshRate。一般來說,請盡可能使用 setFrameRate()

preferredRefreshRate 與 preferredDisplayModeId

如果應用程式只想變更偏好的更新率,建議使用 preferredRefreshRate,而非 preferredDisplayModeId

避免過度頻繁地呼叫 setFrameRate()

雖然 setFrameRate() 呼叫在效能方面並不會造成太大負擔,但應用程式應避免在每個影格或每秒多次呼叫 setFrameRate()。呼叫 setFrameRate() 可能會導致顯示器更新率發生變化,進而導致轉換期間影格遺失。您應事先找出正確的畫面更新率,並呼叫 setFrameRate() 一次。

用於遊戲或其他非影片應用程式

雖然影片是 setFrameRate() API 的主要用途,但也可以用於其他應用程式。舉例來說,如果遊戲不打算以超過 60Hz 的頻率執行 (以減少電力消耗並延長遊戲時段),則可以呼叫 Surface.setFrameRate(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)。如此一來,預設以 90Hz 運作的裝置會在遊戲運作時以 60Hz 運作,避免遊戲以 60Hz 運作,而螢幕以 90Hz 運作時,會發生的畫面抖動現象。

使用 FRAME_RATE_COMPATIBILITY_FIXED_SOURCE

FRAME_RATE_COMPATIBILITY_FIXED_SOURCE 僅適用於影片應用程式。如果是用於非影片用途,請使用 FRAME_RATE_COMPATIBILITY_DEFAULT

選擇變更影格速率的策略

  • 強烈建議應用程式在顯示長時間播放的影片 (例如電影) 時,呼叫 setFrameRate(fps, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, CHANGE_FRAME_RATE_ALWAYS),其中 fps 是影片的幀率。
  • 如果您預期影片播放時間為數分鐘或更短,強烈建議應用程式不要使用 CHANGE_FRAME_RATE_ALWAYS 呼叫 setFrameRate()

影片播放應用程式整合範例

如要在影片播放應用程式中整合螢幕更新率切換器,建議您按照下列步驟操作:

  1. 決定 changeFrameRateStrategy
    1. 如果播放長時間的影片 (例如電影),請使用 MATCH_CONTENT_FRAMERATE_ALWAYS
    2. 如要播放短片 (例如電影預告片),請使用 CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
  2. 如果 changeFrameRateStrategyCHANGE_FRAME_RATE_ONLY_IF_SEAMLESS,請前往步驟 4。
  3. 如要偵測是否即將發生非無縫的更新率切換,請檢查下列兩個事實是否為真:
    1. 從目前的螢幕更新率 (我們稱為 C) 切換至影片的幀率 (我們稱為 V) 時,無法使用無縫模式。如果 C 和 V 不同,且 Display.getMode().getAlternativeRefreshRates 不包含 V 的倍數,就會發生這種情況。
    2. 使用者已選擇啟用非無縫的螢幕更新率變更。您可以檢查 DisplayManager.getMatchContentFrameRateUserPreference 是否傳回 MATCH_CONTENT_FRAMERATE_ALWAYS,藉此偵測此情況。
  4. 如果要進行無縫切換,請按照下列步驟操作:
    1. 呼叫 setFrameRate,並傳入 fpsFRAME_RATE_COMPATIBILITY_FIXED_SOURCEchangeFrameRateStrategy,其中 fps 是影片的幀率。
    2. 開始播放影片
  5. 如果即將進行非無縫模式變更,請執行下列操作:
    1. 顯示使用者體驗,通知使用者。請注意,我們建議您在步驟 5.d 中實作一種方法,讓使用者關閉此使用者體驗,並略過額外的延遲時間。這是因為在顯示器切換時間較快的情況下,建議的延遲時間會比實際所需時間還要長。
    2. 呼叫 setFrameRate,並傳入 fpsFRAME_RATE_COMPATIBILITY_FIXED_SOURCECHANGE_FRAME_RATE_ALWAYS,其中 fps 是影片的畫面更新率。
    3. 等待 onDisplayChanged 回呼。
    4. 等待 2 秒,讓模式切換完成。
    5. 開始播放影片

支援無縫切換的虛擬程式碼如下:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
transaction.setFrameRate(surfaceControl,
    contentFrameRate,
    FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
    CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
transaction.apply();
beginPlayback();

如上述所述,支援無縫和非無縫切換的虛擬程式碼如下:

SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
if (isSeamlessSwitch(contentFrameRate)) {
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS);
  transaction.apply();
  beginPlayback();
} else if (displayManager.getMatchContentFrameRateUserPreference()
      == MATCH_CONTENT_FRAMERATE_ALWAYS) {
  showRefreshRateSwitchUI();
  sleep(shortDelaySoUserSeesUi);
  displayManager.registerDisplayListener();
  transaction.setFrameRate(surfaceControl,
      contentFrameRate,
      FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
      CHANGE_FRAME_RATE_ALWAYS);
  transaction.apply();
  waitForOnDisplayChanged();
  sleep(twoSeconds);
  hideRefreshRateSwitchUI();
  beginPlayback();
}