應用程式可透過影格速率 API 將預期影格速率告知 Android 平台,這項 API 適用於目標為 Android 11 (API 級別 30) 以上版本的應用程式。傳統上,大多數裝置只支援單一螢幕更新率 (通常為 60 Hz),但這種情況已有所改變。許多裝置現在支援其他刷新率,例如 90 Hz 或 120 Hz。部分裝置支援流暢切換刷新率,其他裝置則會短暫顯示黑畫面,通常只會持續一秒。
這項 API 的主要用途是讓應用程式充分運用所有支援的螢幕更新率。舉例來說,如果應用程式播放 24Hz 影片並呼叫 setFrameRate()
,裝置可能會將螢幕更新率從 60Hz 變更為 120Hz。這項新刷新率可流暢播放 24Hz 影片,不會出現畫面抖動,也不需要像在 60Hz 螢幕上播放相同影片時,進行 3:2 降轉。這樣即可提供更優質的使用者體驗。
基本用法
Android 提供多種存取及控制介面的方式,因此 setFrameRate()
API 有多個版本。每個版本的 API 都會採用相同的參數,且運作方式與其他版本相同:
Surface.setFrameRate()
SurfaceControl.Transaction.setFrameRate()
ANativeWindow_setFrameRate()
ASurfaceTransaction_setFrameRate()
應用程式不需要考慮實際支援的螢幕更新率,呼叫 Display.getSupportedModes()
即可取得,以便安全地呼叫 setFrameRate()
。舉例來說,即使裝置只支援 60Hz,也請使用應用程式偏好的影格速率呼叫 setFrameRate()
。如果裝置沒有更符合應用程式影格速率的刷新率,就會維持目前的螢幕刷新率。
如要查看呼叫 setFrameRate()
是否會導致螢幕更新率變更,請呼叫 DisplayManager.registerDisplayListener()
或 AChoreographer_registerRefreshRateCallback()
註冊螢幕變更通知。
呼叫 setFrameRate()
時,最好傳遞確切的影格速率,而不是四捨五入為整數。舉例來說,如果以 29.97 Hz 錄製影片,請傳遞 29.97,而非四捨五入至 30。
如果是影片應用程式,傳遞至 setFrameRate()
的相容性參數應設為 Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
,向 Android 平台提供額外提示,指出應用程式會使用下拉式選單來配合不相符的螢幕更新率 (這會導致畫面抖動)。
在某些情況下,影片介面會停止提交影格,但仍會在畫面上顯示一段時間。常見情況包括播放到影片結尾,或使用者暫停播放。在這些情況下,請呼叫 setFrameRate()
,並將影格速率參數設為 0,將介面的影格速率設定清除回預設值。在銷毀 Surface 時,或在使用者切換至其他應用程式而隱藏 Surface 時,不必清除影格速率設定。只有在 Surface 保持顯示狀態但未被使用時,才需要清除影格速率設定。
非無縫畫格速率切換
在部分裝置上,切換刷新率時可能會出現視覺中斷情形,例如螢幕會短暫變黑一兩秒。這種情況通常發生在機上盒、電視面板和類似裝置上。根據預設,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。如果其他應用程式的介面影格速率設定為 24 Hz,就可能發生這種情況。在這種情況下,以 120Hz 執行螢幕可讓 60Hz 表面和 24Hz 表面執行,不需要下拉式選單。
如果螢幕的影格速率是應用程式影格速率的倍數,應用程式應為每個影格指定顯示時間戳記,避免不必要的抖動。對於遊戲,Android Frame Pacing 程式庫有助於正確設定影格顯示時間戳記。
setFrameRate() 與 preferredDisplayModeId
WindowManager.LayoutParams.preferredDisplayModeId
應用程式也可以透過這種方式向平台指出影格速率。部分應用程式只會變更螢幕更新率,不會變更其他顯示模式設定,例如螢幕解析度。一般來說,請使用 setFrameRate()
,而非 preferredDisplayModeId
。setFrameRate()
函式更容易使用,因為應用程式不必搜尋顯示模式清單,就能找到特定影格速率的模式。
setFrameRate()
可讓平台在多個表面以不同影格速率執行的情況下,有更多機會選擇相容的影格速率。舉例來說,假設 Pixel 4 處於分割畫面模式,其中一個應用程式正在播放 24 Hz 的影片,另一個應用程式則向使用者顯示可捲動的清單。Pixel 4 支援兩種螢幕刷新率:60Hz 和 90Hz。使用 preferredDisplayModeId
API 時,影片 Surface 會強制選擇 60Hz 或 90Hz。呼叫 24Hz 時,影片介面會提供更多來源影片的影格速率資訊給平台,讓平台選擇 90Hz 做為螢幕刷新率,這比這個情境下的 60Hz 更合適。setFrameRate()
不過,在某些情況下,應使用 preferredDisplayModeId
而非 setFrameRate()
,例如:
- 如果應用程式想變更解析度或其他顯示模式設定,請使用
preferredDisplayModeId
。 - 只有在模式切換作業輕量,且使用者不太可能察覺時,平台才會回應
setFrameRate()
的呼叫,切換顯示模式。如果應用程式偏好切換螢幕更新率,即使需要大量模式切換 (例如在 Android TV 裝置上),請使用preferredDisplayModeId
。 - 如果應用程式無法處理以應用程式影格速率的倍數執行的螢幕,且需要在每個影格上設定顯示時間戳記,則應使用
preferredDisplayModeId
。
setFrameRate() 與 preferredRefreshRate 的比較
WindowManager.LayoutParams#preferredRefreshRate
會在應用程式視窗中設定偏好的影格速率,且該速率適用於視窗內的所有介面。無論裝置支援的刷新率為何,應用程式都應指定偏好的影格速率,類似於 setFrameRate()
,以便為排程器提供應用程式預期影格速率的更佳提示。
如果 Surface 使用 setFrameRate()
,系統會忽略 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 執行,避免遊戲以 60 Hz 執行,但螢幕以 90 Hz 執行時可能發生的抖動。
使用 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()
。
影片播放應用程式的整合範例
建議您按照下列步驟,在影片播放應用程式中整合螢幕更新率切換功能:
- 決定
changeFrameRateStrategy
:- 如果播放電影等長時間影片,請使用
MATCH_CONTENT_FRAMERATE_ALWAYS
- 如果播放短片 (例如電影預告片),請使用
CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
- 如果播放電影等長時間影片,請使用
- 如果
changeFrameRateStrategy
為CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
,請前往步驟 4。 - 如要偵測是否即將發生非無縫的螢幕更新率切換,請檢查下列兩項事實是否皆為真:
- 目前無法從目前的更新率 (假設為 C) 無縫切換至影片的影格速率 (假設為 V)。如果 C 和 V 不同,且
Display.getMode().getAlternativeRefreshRates
不含 V 的倍數,就會發生這種情況。 - 使用者已選擇接受非無縫的螢幕更新率變更。如要偵測這種情況,請檢查
DisplayManager.getMatchContentFrameRateUserPreference
是否傳回MATCH_CONTENT_FRAMERATE_ALWAYS
- 目前無法從目前的更新率 (假設為 C) 無縫切換至影片的影格速率 (假設為 V)。如果 C 和 V 不同,且
- 如要進行無縫切換,請按照下列步驟操作:
- 呼叫
setFrameRate
,然後傳遞fps
、FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
和changeFrameRateStrategy
,其中fps
是影片的影格速率。 - 開始播放影片
- 呼叫
- 如果即將發生非無縫模式變更,請執行下列操作:
- 顯示 UX 來通知使用者。請注意,建議您實作一種方式,讓使用者可以關閉這個使用者體驗,並略過步驟 5.d 中的額外延遲。這是因為在切換時間較快的螢幕上,建議的延遲時間會大於必要時間。
- 呼叫
setFrameRate
,然後傳遞fps
、FRAME_RATE_COMPATIBILITY_FIXED_SOURCE
和CHANGE_FRAME_RATE_ALWAYS
,其中fps
是影片的影格速率。 - 等待
onDisplayChanged
回呼。 - 等待 2 秒,讓模式切換完成。
- 開始播放影片
僅支援無縫切換的虛擬程式碼如下:
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();
}