קצב מסגרות

ה-API של קצב הפריימים מאפשר לאפליקציות להודיע לפלטפורמת Android על קצב הפריימים המיועד שלהן, והוא זמין באפליקציות שמטרגטות את Android 11 (רמת API 30) ואילך. בעבר, רוב המכשירים תמכו רק בקצב רענון מסך אחד, בדרך כלל 60Hz, אבל המצב הזה השתנה. במכשירים רבים יש עכשיו תמיכה בשיעורי רענון נוספים, כמו 90Hz או 120Hz. חלק מהמכשירים תומכים במעברים חלקים בין קצב רענון שונה, בעוד שבמכשירים אחרים מוצג מסך שחור לזמן קצר, בדרך כלל למשך שנייה.

המטרה העיקרית של ה-API היא לאפשר לאפליקציות לנצל בצורה טובה יותר את כל שיעורי הרענון הנתמכים של המסך. לדוגמה, אפליקציה שמפעילה וידאו של 24Hz שמפעילה את setFrameRate() עשויה לגרום למכשיר לשנות את קצב הרענון של המסך מ-60Hz ל-120Hz. קצב הרענון החדש מאפשר הפעלה חלקה של סרטונים באיכות 24Hz ללא רעידות, ללא צורך ב-3:2 pulldown, כפי שנדרש להפעלת אותו סרטון במסך 60Hz. כך חוויית המשתמש משתפרת.

שימוש בסיסי

ב-Android יש כמה דרכים לגשת למשטחים ולשלוט בהם, ולכן יש כמה גרסאות של ה-API‏ setFrameRate(). כל גרסה של ה-API מקבלת את אותם פרמטרים ופועלת באותו אופן כמו שאר הגרסאות:

כדי לקרוא בבטחה ל-setFrameRate(), האפליקציה לא צריכה להביא בחשבון את שיעורי הרענון בפועל של המסך הנתמכים, שאפשר לקבל על ידי קריאה ל-Display.getSupportedModes(). לדוגמה, גם אם המכשיר תומך רק ב-60Hz, צריך להפעיל את setFrameRate() עם קצב הפריימים המועדף על האפליקציה. במכשירים שלא מתאים להם קצב רענון טוב יותר של המסך לקצב הפריימים של האפליקציה, קצב הרענון של המסך לא ישתנה.

כדי לבדוק אם קריאה ל-setFrameRate() גורמת לשינוי בקצב הרענון של המסך, צריך להירשם לקבלת התראות על שינויים במסך באמצעות קריאה למספר DisplayManager.registerDisplayListener() או AChoreographer_registerRefreshRateCallback().

כשקוראים ל-setFrameRate(), עדיף להעביר את קצב הפריימים המדויק במקום לעגל למספר שלם. לדוגמה, כשמריצים רינדור של סרטון שצולם ב-29.97Hz, צריך להעביר את הערך 29.97 ולא לעגל ל-30.

באפליקציות וידאו, צריך להגדיר את פרמטר התאימות שמוענק ל-setFrameRate() לערך Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE כדי לספק לאפליקציה עוד רמז לפלטפורמת Android שהיא תשתמש בתפריט הנפתח כדי להתאים את עצמה לשיעור רענון המסך שאינו תואם (מה שעלול לגרום לתנודות).

בתרחישים מסוימים, שטח הסרטון יפסיק לשלוח פריימים, אבל הוא יישאר גלוי במסך למשך זמן מה. תרחישים נפוצים כוללים מקרים שבהם ההפעלה מגיעה לסוף הסרטון או מקרים שבהם המשתמש משהה את ההפעלה. במקרים כאלה, צריך להפעיל את setFrameRate() עם הפרמטר של קצב הפריימים מוגדר ל-0 כדי למחוק את ההגדרה של קצב הפריימים של פני השטח ולהחזיר אותה לערך ברירת המחדל. אין צורך לנקות את הגדרת קצב הפריימים באופן הזה כשמשמידים את פני השטח, או כשפני השטח מוסתרים כי המשתמש עובר לאפליקציה אחרת. צריך לנקות את הגדרת קצב הפריימים רק כשפני השטח נותרים גלויים בלי שימוש בהם.

מעבר לא חלק בין קצב פריימים

במכשירים מסוימים, המעבר בין שיעורי הרענון עשוי לגרום להפרעות חזותיות, כמו מסך שחור למשך שנייה או שתיים. התרחיש הזה קורה בדרך כלל בממירים, במסכי טלוויזיה ובמכשירים דומים. כברירת מחדל, מסגרת Android לא עוברת בין המצבים כשקוראים לממשק ה-API Surface.setFrameRate() כדי למנוע הפרעות חזותיות כאלה.

חלק מהמשתמשים מעדיפים הפרעה חזותית בתחילת הסרטון ובסופו, במיוחד בסרטונים ארוכים. כך קצב הרענון של המסך תואם לקצב הפריימים של הסרטון, ומונעים פגמים בהמרה של קצב הפריימים, כמו רעידות בתמונה בזמן הורדה של 3:2 בהפעלת סרט.

לכן, אפשר להפעיל את המעבר בין שיעורי רענון לא חלקים רק אם גם המשתמש וגם האפליקציות מביעים הסכמה:

מומלץ תמיד להשתמש ב-CHANGE_FRAME_RATE_ALWAYS בסרטונים ארוכים כמו סרטים. הסיבה לכך היא שהיתרון של התאמת קצב הפריימים של הסרטון עולה על החיסרון של ההפרעה שמתרחשת כשמשנים את קצב הרענון.

המלצות נוספות

מומלץ לפעול לפי ההמלצות הבאות בתרחישים נפוצים.

מספר משטחים

פלטפורמת Android תוכננה לטפל בצורה נכונה בתרחישים שבהם יש כמה משטחים עם הגדרות שונות של קצב פריימים. אם באפליקציה יש כמה משטחים עם שיעורי פריימים שונים, צריך לבצע קריאה ל-setFrameRate() עם שיעור הפריימים הנכון לכל משטח. גם אם במכשיר פועלות כמה אפליקציות בו-זמנית, במצב מסך מפוצל או במצב 'תמונה בתוך תמונה', כל אפליקציה יכולה להפעיל את setFrameRate() בבטחה כדי להציג את הפלטפורמות שלה.

הפלטפורמה לא משתנה לקצב הפריימים של האפליקציה

גם אם המכשיר תומך בקצב הפריימים שהאפליקציה מציינת בקריאה ל-setFrameRate(), יש מקרים שבהם המכשיר לא יעביר את המסך לקצב הרענון הזה. לדוגמה, למשטח עם עדיפות גבוהה יותר עשויה להיות הגדרה שונה של קצב הפריימים, או שהמכשיר עשוי להיות במצב חיסכון בסוללה (הגדרת הגבלה על קצב הרענון של המסך כדי לחסוך בסוללה). האפליקציה עדיין צריכה לפעול בצורה תקינה כשהמכשיר לא מחליף את קצב הרענון של המסך להגדרת קצב הפריימים של האפליקציה, גם אם המכשיר עושה זאת בנסיבות רגילות.

האפליקציה מחליטה איך להגיב כשקצב הרענון של המסך לא תואם לקצב הפריימים של האפליקציה. בסרטון, קצב הפריימים קבוע וקבוע לפי קצב הפריימים של סרטון המקור, ויהיה צורך בתנועה למטה כדי להציג את תוכן הסרטון. במקום זאת, המשחק עשוי לנסות לפעול בקצב הרענון של המסך במקום להישאר בקצב הפריימים המועדף עליו. האפליקציה לא צריכה לשנות את הערך שהיא מעבירה אל setFrameRate() על סמך הפעולות של הפלטפורמה. הוא צריך להישאר מוגדר לשיעור הפריימים המועדף של האפליקציה, ללא קשר לאופן שבו האפליקציה מטפלת במקרים שבהם הפלטפורמה לא מתאימה את עצמה לבקשת האפליקציה. כך, אם התנאים במכשיר ישתנו ויאפשרו להשתמש בקצבי רענון נוספים של המסך, בפלטפורמה יהיה המידע הנכון כדי לעבור לקצב הפריימים המועדף של האפליקציה.

במקרים שבהם האפליקציה לא תפעל או לא תוכל לפעול בקצב הרענון של המסך, צריך לציין באפליקציה חותמות זמן של הצגה לכל פריים, באמצעות אחד המנגנונים של הפלטפורמה להגדרת חותמות זמן של הצגה:

השימוש בחותמות הזמן האלה מונע מהפלטפורמה להציג פריים של האפליקציה מוקדם מדי, מה שעלול לגרום לתנודות מיותרות. השימוש הנכון בחותמות זמן של הצגת פריימים הוא קצת מסובך. במשחקים, תוכלו לעיין במדריך לבחירת קצב הפריימים כדי לקבל מידע נוסף על מניעת רעידות, ולשקול להשתמש בספריית קצב הפריימים של Android.

במקרים מסוימים, הפלטפורמה עשויה לעבור למכפיל של קצב הפריימים שהאפליקציה ציינה ב-setFrameRate(). לדוגמה, אפליקציה עשויה להפעיל את setFrameRate() עם 60Hz, והמכשיר עשוי להעביר את התצוגה ל-120Hz. אחת מהסיבות לכך היא שאפליקציה אחרת כוללת משטח עם הגדרת קצב פריימים של 24Hz. במקרה כזה, הפעלת המסך ב-120Hz תאפשר להפעיל גם את פני השטח של 60Hz וגם את פני השטח של 24Hz בלי צורך ב-pulldown.

כשהתצוגה פועלת במכפיל של קצב הפריימים של האפליקציה, האפליקציה צריכה לציין חותמות זמן של הצגה לכל פריים כדי למנוע רעידות מיותרות. במשחקים, ספריית Android Frame Pacing עוזרת להגדיר בצורה נכונה את חותמות הזמן של הצגת הפריימים.

setFrameRate()‎ לעומת preferredDisplayModeId

WindowManager.LayoutParams.preferredDisplayModeId היא דרך נוספת שבה אפליקציות יכולות לציין את קצב הפריימים שלהן לפלטפורמה. באפליקציות מסוימות רוצים לשנות רק את קצב הרענון של המסך, ולא לשנות הגדרות אחרות של מצב התצוגה, כמו רזולוציית המסך. באופן כללי, מומלץ להשתמש ב-setFrameRate() במקום ב-preferredDisplayModeId. קל יותר להשתמש בפונקציה setFrameRate() כי האפליקציה לא צריכה לחפש ברשימת מצבי התצוגה כדי למצוא מצב עם קצב פריימים ספציפי.

setFrameRate() מאפשר לפלטפורמה לבחור בקצב פריימים תואם בתרחישים שבהם יש כמה משטחים שפועלים בקצב פריימים שונה. לדוגמה, נניח ששתי אפליקציות פועלות במצב מסך מפוצל ב-Pixel 4, כאשר באחת מהן מופעל סרטון באיכות 24Hz ובשנייה מוצגת למשתמשים רשימה שאפשר לגלול בה. Pixel 4 תומך בשני קצב רענון של התצוגה: 60Hz ו-90Hz. באמצעות ה-API של preferredDisplayModeId, משטח הווידאו נדרש לבחור בין 60Hz ל-90Hz. כשמפעילים את setFrameRate() עם 24Hz, פלטפורמת הווידאו מספקת לפלטפורמה מידע נוסף על קצב הפריימים של סרטון המקור, ומאפשרת לפלטפורמה לבחור 90Hz כקצב הרענון של התצוגה, שהוא טוב יותר מ-60Hz בתרחיש הזה.

עם זאת, יש תרחישים שבהם צריך להשתמש ב-preferredDisplayModeId במקום ב-setFrameRate(), למשל:

  • אם האפליקציה רוצה לשנות את הרזולוציה או הגדרות אחרות של מצב התצוגה, צריך להשתמש ב-preferredDisplayModeId.
  • הפלטפורמה תשנה את מצבי התצוגה רק בתגובה לקריאה ל-setFrameRate() אם שינוי המצב קל ולא צפוי להיות מורגש למשתמש. אם האפליקציה מעדיפה להחליף את קצב הרענון של המסך גם אם נדרשת החלפת מצב כבדה (לדוגמה, במכשיר Android TV), משתמשים ב-preferredDisplayModeId.
  • באפליקציות שלא יכולות לטפל במסך שפועל במכפיל של קצב הפריימים של האפליקציה, ועל כן מחייב הגדרת חותמות זמן של הצגה בכל פריים, צריך להשתמש ב-preferredDisplayModeId.

setFrameRate() לעומת preferredRefreshRate

WindowManager.LayoutParams#preferredRefreshRate מגדיר את קצב הפריימים המועדף בחלון של האפליקציה, והקצב רלוונטי לכל המשטחים בחלון. האפליקציה צריכה לציין את קצב הפריימים המועדף עליה, ללא קשר לקצבי הרענון הנתמכים במכשיר, בדומה ל-setFrameRate(), כדי לתת לתזמון רמז טוב יותר לגבי קצב הפריימים המיועד של האפליקציה.

המערכת מתעלמת מ-preferredRefreshRate בפלטפורמות שמשתמשות ב-setFrameRate(). באופן כללי, מומלץ להשתמש ב-setFrameRate() אם אפשר.

preferredRefreshRate לעומת preferredDisplayModeId

אם האפליקציות רוצות לשנות רק את קצב הרענון המועדף, עדיף להשתמש ב-preferredRefreshRate במקום ב-preferredDisplayModeId.

הימנעות משימוש בתדירות גבוהה מדי ב-setFrameRate()

הקריאה ל-setFrameRate() לא יקרה מאוד מבחינת הביצועים, אבל רצוי להימנע מהפעלת קריאה ל-setFrameRate() בכל פריים או כמה פעמים בשנייה באפליקציות. קריאות ל-setFrameRate() צפויות להוביל לשינוי של קצב הרענון של המסך, שעלול לגרום לירידה במספר הפריימים במהלך המעבר. כדאי לחשב מראש את קצב הפריימים הנכון ולקרוא ל-setFrameRate() פעם אחת.

שימוש במשחקים או באפליקציות אחרות שאינן וידאו

הווידאו הוא התרחיש העיקרי לשימוש ב-API setFrameRate(), אבל אפשר להשתמש בו גם באפליקציות אחרות. לדוגמה, משחק שאמור לפעול בקצב של עד 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 הוא קצב הפריימים של הסרטון.
  • אנחנו לא ממליצים לאפליקציות לבצע קריאה ל-setFrameRate() עם CHANGE_FRAME_RATE_ALWAYS כשצפוי שהפעלת הסרטון תימשך כמה דקות או פחות.

דוגמה לשילוב באפליקציות להפעלת סרטונים

אנחנו ממליצים לבצע את השלבים הבאים כדי לשלב מתגים של קצב רענון באפליקציות להפעלת וידאו:

  1. קובעים את changeFrameRateStrategy:
    1. אם מפעילים סרטון ארוך, כמו סרט, משתמשים ב-MATCH_CONTENT_FRAMERATE_ALWAYS
    2. אם מפעילים סרטון קצר, כמו טריילר של סרט, משתמשים ב-CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
  2. אם הערך של changeFrameRateStrategy הוא CHANGE_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 ומעבירים אליו את הערכים fps,‏ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE ו-changeFrameRateStrategy, כאשר fps הוא קצב הפריימים של הסרטון.
    2. הפעלת הסרטון
  5. אם עומד להתרחש שינוי מצב לא חלק, צריך לבצע את הפעולות הבאות:
    1. הצגת חוויית משתמש כדי להודיע למשתמש. חשוב לזכור: מומלץ להטמיע דרך שבה המשתמש יוכל לסגור את ממשק המשתמש הזה ולדלג על העיכוב הנוסף בשלב 5.ד. הסיבה לכך היא שההשהיה המומלצת שלנו גדולה מהנדרש במסכים עם זמני מעבר מהירים יותר.
    2. קוראים ל-setFrameRate ומעבירים אליו את הערכים fps,‏ FRAME_RATE_COMPATIBILITY_FIXED_SOURCE ו-CHANGE_FRAME_RATE_ALWAYS, כאשר fps הוא קצב הפריימים של הסרטון.
    3. ממתינים לקריאה החוזרת (callback) של 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();
}