畫面更新率

影格速率 API 可讓應用程式向 Android 平台通知預期的影格速率,並顯示在目標版本為 Android 11 (API 級別 30) 以上版本的應用程式中。傳統上,大多數裝置僅支援單一螢幕刷新率 (通常為 60 Hz),但我們已能改變這一點。許多裝置現在都支援額外的刷新率,例如 90Hz 或 120Hz。部分裝置支援流暢切換刷新率,其他裝置則會短暫顯示黑色畫面,通常會持續一秒。

API 的主要用途,是讓應用程式能充分利用所有支援的螢幕刷新率。舉例來說,如果應用程式播放的 24Hz 影片呼叫 setFrameRate(),裝置可能會導致螢幕刷新率從 60Hz 變更為 120Hz。新的刷新率可讓您流暢地播放 24Hz 影片,無須使用 3:2 下拉式選單,就像在 60Hz 螢幕上播放同一部影片時需要此畫面。這樣可以提供更優質的使用者體驗。

基本用法

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 平台提供額外提示,應用程式會使用下拉功能,以配合不符合的螢幕刷新率 (這會導致看似不匹配)。

在某些情況下,影片途徑會停止提交影格,但會在螢幕上持續顯示一段時間。常見情況包括播放到影片結尾或使用者暫停播放時。在這種情況下,您可以呼叫 setFrameRate(),並將影格速率參數設為 0,將途徑的影格速率設定清除回預設值。刪除介面或因使用者切換至其他應用程式而隱藏時,不必清除這類畫面更新率設定。只有在途徑保持可見,且未使用的情況下,才能清除影格速率設定。

非流暢影格速率切換鈕

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

部分使用者偏好在長片的開頭和結尾處使用視覺幹擾元素。如此一來,螢幕的刷新率就能符合影片影格速率,並避免在播放電影時,避免產生畫面更新率的轉換失真,例如 3:2 的提取跳線。

因此,只要使用者和應用程式都選擇採用,即可啟用不流暢的重新整理頻率切換功能:

  • 使用者:如要選擇加入,使用者可以啟用「採用內容影格速率」使用者設定。
  • 應用程式:如要選擇加入,應用程式可以將 CHANGE_FRAME_RATE_ALWAYS 傳遞至 setFrameRate()

如果是電影等長時間執行的影片,建議您一律使用 CHANGE_FRAME_RATE_ALWAYS。這是因為比對影片影格速率的好處,高於變更刷新率時發生的中斷情形。

其他建議

請針對常見情境採用以下建議。

多個介面

如果多個途徑採用不同的影格速率設定,Android 平台就能妥善處理這類情境。如果應用程式有多個畫面更新率不同的介面,請呼叫 setFrameRate(),並提供每個途徑的正確影格速率。即使裝置會同時執行多個應用程式,只要使用分割畫面或子母畫面模式,每個應用程式都能安全地為自身的介面呼叫 setFrameRate()

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

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

螢幕刷新率與應用程式畫面更新率不符時,應用程式可自行判斷如何回應。以影片來說,畫面更新率固定為來源影片,您必須開啟下拉式選單才能顯示影片內容。遊戲可能會改為嘗試以螢幕刷新率執行,而非繼續使用偏好的畫面更新率。應用程式不應根據平台功能,變更傳送至 setFrameRate() 的值。無論應用程式如何處理平台未配合應用程式的要求進行調整時,系統應一律採用應用程式的偏好影格速率。這樣一來,如果裝置條件變更為允許使用額外的螢幕刷新率,平台便會取得正確的資訊,切換至應用程式的偏好影格速率。

如果應用程式無法或無法以螢幕刷新率執行,應用程式應使用平台的其中一種機制設定顯示時間戳記,為每個影格指定顯示時間戳記:

一旦使用這些時間戳記,平台就無法太早顯示應用程式影格,導致不必要的輪廓。正確使用影格顯示時間戳記並不容易。如果是遊戲,請參閱影格同步指南來進一步瞭解如何避免跳動,並考慮使用 Android Frame Pacing 程式庫

在某些情況下,平台可能會切換至 setFrameRate() 中指定的應用程式影格速率的倍數。舉例來說,應用程式可以使用 60Hz 呼叫 setFrameRate(),而裝置可能會將螢幕切換為 120Hz。這可能是因為另一個應用程式的介面,其畫面更新率設為 24Hz。在這種情況下,以 120 Hz 執行螢幕將同時執行 60Hz 表面和 24Hz 表面,而無需下拉。

如果螢幕以應用程式畫面更新率的倍數執行,應用程式應為每個影格指定顯示時間戳記,以避免不必要的判斷。如果是遊戲,Android Frame Pacing 程式庫可正確設定影格顯示時間戳記。

setFrameRate() 與 preferredDisplayModeId

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

如果有多個途徑以不同的影格速率執行,setFrameRate() 可讓平台有更多機會選擇相容的影格速率。舉例來說,假設在 Pixel 4 上,有兩個應用程式以分割畫面模式執行,其中有一個應用程式播放 24Hz 影片,另一個應用程式顯示了可捲動的清單。Pixel 4 支援兩種螢幕刷新率:60 Hz 和 90Hz。使用 preferredDisplayModeId API 時,系統會強制選擇 60Hz 或 90Hz 影片介面。透過 24Hz 呼叫 setFrameRate() 可讓平台進一步瞭解來源影片的影格速率,使平台能夠為螢幕刷新率選擇 90Hz,在上述情境中會大於 60 Hz。

但在某些情況下,應該使用 preferredDisplayModeId 而不是 setFrameRate(),例如:

  • 如果應用程式要變更解析度或其他顯示模式設定,請使用 preferredDisplayModeId
  • 只有在模式切換按鈕較輕重,且使用者不太可能注意到時,平台才會切換顯示模式來回應對 setFrameRate() 的呼叫。如果應用程式偏好切換螢幕刷新率,即使需要頻繁切換裝置 (例如在 Android TV 裝置) 的情況下使用,請使用 preferredDisplayModeId
  • 如果應用程式無法處理以應用程式畫面更新率的倍數執行螢幕,必須為每個影格設定顯示時間戳記,則應使用 preferredDisplayModeId

setFrameRate() 與 PreferredRefreshRate 的比較

WindowManager.LayoutParams#preferredRefreshRate 會在應用程式視窗上設定偏好的影格速率,且該速率適用於視窗內的所有介面。無論裝置支援的刷新率為何,應用程式都應指定偏好的影格速率 (與 setFrameRate() 類似),為排程器提供更多有關應用程式預期影格速率的提示。

使用 setFrameRate() 的 Surface 會略過 preferredRefreshRate。請盡可能使用 setFrameRate()

PreferredRefreshRate 與 preferredDisplayModeId

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

避免太常呼叫 setFrameRate()

雖然 setFrameRate() 呼叫的效能的成本並不高,但應用程式應避免在每個影格呼叫 setFrameRate(),或每秒呼叫多次。呼叫 setFrameRate() 可能會導致螢幕刷新率改變,而可能會導致畫面在轉換期間遺失。應提前找出正確的影格速率,並呼叫 setFrameRate() 一次。

遊戲或其他非影片應用程式的使用情形

雖然影片是 setFrameRate() API 的主要用途,但可用於其他應用程式。舉例來說,如果遊戲意圖不高於 60 Hz (為了降低耗電量並達到較長的遊戲工作階段時間),可呼叫 Surface.setFrameRate(60, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)。如此一來,在遊戲處於啟用狀態時,預設以 90 Hz 執行的裝置將改為以 60 Hz 執行,避免在螢幕以 90 Hz 執行時,以 60Hz 執行時遇到的波動。

「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),其中每秒影格數為影片畫面更新率。
  • 如果希望影片播放持續數分鐘以內,強烈建議不要使用 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();
}