תמיכה במכשירים מתקפלים עם שלושה מסכים ובמכשירים מתקפלים לרוחב

מכשיר מתקפל במצב אופקי, במצב סגור ובמצב פתוח מלא, לצד מכשיר מתקפל לשלושה חלקים במצב סגור ובמצב פתוח מלא.

מפתחים נתקלים לעיתים קרובות בקשיים ייחודיים כשהם יוצרים אפליקציות למכשירים מתקפלים – במיוחד למכשירים כמו Samsung Trifold או Pixel Fold המקורי, שנפתח במצב אופקי (rotation_0 = landscape). דוגמאות לשגיאות של מפתחים:

  • הנחות שגויות לגבי כיוון המכשיר
  • תרחישי שימוש שלא נכללו
  • החישוב מחדש או השמירה במטמון של ערכים נכשלים אחרי שינויים בהגדרות

דוגמאות לבעיות ספציפיות שקשורות למכשיר:

  • חוסר התאמה בכיוון הטבעי של המכשיר בין המסכים החיצוני והפנימי (הנחות שמבוססות על rotation_0 = portrait), שגורם לכך שאפליקציות לא פועלות כשפותחים וסוגרים את המכשיר
  • טיפול בשינוי הגדרות של צפיפות שגויות וצפיפות מסך שונה
  • בעיות בתצוגה המקדימה של המצלמה שנגרמות בגלל התלות של חיישן המצלמה בכיוון הטבעי

כדי לספק חוויית משתמש באיכות גבוהה במכשירים מתקפלים, חשוב להתמקד בתחומים הקריטיים הבאים:

  • קביעת הכיוון של האפליקציה על סמך שטח המסך בפועל שהאפליקציה תופסת, ולא על סמך הכיוון הפיזי של המכשיר
  • עדכון התצוגות המקדימות של המצלמה כדי לנהל את כיוון המכשיר ויחסי הגובה-רוחב בצורה נכונה, למנוע תצוגות מקדימות לא ישרות ולמנוע תמונות מתוחות או חתוכות
  • שמירה על רציפות האפליקציה בזמן קיפול או פתיחה של המכשיר על ידי שמירת המצב באמצעות ViewModel או גישות דומות, או טיפול ידני בשינויים בצפיפות המסך ובשינויים בכיוון, כדי למנוע הפעלה מחדש של האפליקציה או אובדן של המצב
  • באפליקציות שמשתמשות בחיישני תנועה, צריך להתאים את מערכת הקואורדינטות כך שתתאים לאוריינטציה הנוכחית של המסך, ולהימנע מהנחות שמבוססות על rotation_0 = portrait, כדי להבטיח אינטראקציות מדויקות עם המשתמשים.

יצירת עיצוב דינמי

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

פריסות מותאמות

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

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

גדלים של חלונות

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

איור של אפליקציה במכשירים בגדלים קומפקטי, בינוני וגדול של חלונות.
איור 2. גדלים של חלונות.

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

val adaptiveInfo = currentWindowAdaptiveInfo()
val windowSizeClass = adaptiveInfo.windowSizeClass

when {
  windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND) -> // Expanded
  windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND) -> // Medium
  else -> // Compact
}

מידע נוסף זמין במאמר בנושא שימוש בסוגי גודל חלון.

איכות אדפטיבית של אפליקציות

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

‫Android מגרסה 16 ואילך

באפליקציות שמטרגטות ל-Android 16 (רמת API 36) ומעלה, המערכת מתעלמת מהגבלות על כיוון, שינוי גודל ויחס גובה-רוחב במסכים עם רוחב מינימלי של 600dp ומעלה. האפליקציות ממלאות את חלון התצוגה, בלי קשר ליחס הגובה-רוחב או להעדפת הכיוון של המשתמש, ולא נעשה יותר שימוש במצב התאימות letterbox.

שיקולים מיוחדים

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

תצוגה מקדימה של המצלמה

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

הנחות שונות

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

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

פתרון 1: Jetpack CameraX (המומלץ ביותר)

הפתרון הפשוט והיציב ביותר הוא שימוש בספריית Jetpack CameraX. רכיב ממשק המשתמש PreviewView נועד לטפל בכל המורכבויות של התצוגה המקדימה באופן אוטומטי:

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

מידע נוסף זמין במאמר הטמעה של תצוגה מקדימה במאמרי העזרה של CameraX.

פתרון 2: CameraViewfinder

אם אתם משתמשים בבסיס קוד קיים של Camera2, ספריית CameraViewfinder (תואמת לדורות קודמים עד לרמת API 21) היא פתרון מודרני נוסף. הוא מפשט את הצגת פיד המצלמה באמצעות TextureView או SurfaceView ומחיל את כל השינויים הנדרשים (יחס גובה-רוחב, קנה מידה וסיבוב) בשבילכם.

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

פתרון 3: הטמעה ידנית של Camera2

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

  • מקבלים את כיוון חיישן המצלמה (לדוגמה, 0, 90, 180, 270 מעלות) מ-CameraCharacteristics.
  • קבלת זווית הסיבוב הנוכחית של המסך במכשיר (לדוגמה, 0, 90, 180, 270 מעלות).
  • משתמשים בשני הערכים האלה כדי לקבוע את השינויים הנדרשים עבור SurfaceView או TextureView.
  • כדי למנוע עיוות, חשוב לוודא שיחס הגובה-רוחב של הפלט Surface זהה ליחס הגובה-רוחב של התצוגה המקדימה של המצלמה.
  • יכול להיות שאפליקציית המצלמה פועלת בחלק מהמסך, במצב ריבוי חלונות או במצב ממשק מחשב, או במסך מחובר. לכן, אסור להשתמש בגודל המסך כדי לקבוע את המימדים של עינית המצלמה, אלא צריך להשתמש במדדי החלון.

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

פתרון 4: ביצוע פעולות בסיסיות במצלמה באמצעות Intent

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

מידע נוסף זמין במאמר בנושא Camera intents.

הגדרה והמשכיות

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

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

שינויי התצורה שמופעלים לעיתים קרובות כוללים את screenSize,‏ smallestScreenSize,‏ screenLayout,‏ orientation,‏ density,‏ fontScale,‏ touchscreen ו-keyboard.

אפשר לעיין במאמרים בנושא android:configChanges וטיפול בשינויים בהגדרות. מידע נוסף על ניהול מצב האפליקציה זמין במאמר שמירת מצבי ממשק המשתמש.

שינויים בהגדרות הצפיפות

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

הגדרה של AndroidManifest.xml

  • density: הצהרה שהאפליקציה תטפל בשינוי צפיפות המסך
  • שינויים אחרים בהגדרות: מומלץ גם להצהיר על שינויים אחרים בהגדרות שמתרחשים לעיתים קרובות, למשל screenSize, orientation, keyboardHidden, fontScale וכו'.

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

הטמעה של onConfigurationChanged()‎

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

  • מוודאים שרזולוציית ה-DPI השתנתה ל-newConfig.densityDpi
  • איפוס תצוגות מותאמות אישית, רכיבים גרפיים מותאמים אישית וכו' לצפיפות החדשה

פריטים של משאבים לעיבוד

  • משאב תמונה: החלפת מפות סיביות ומשאבים מסוג drawable במשאבים ספציפיים לצפיפות, או התאמת קנה המידה ישירות
  • יחידת פריסה (המרת dp ל-px): חישוב מחדש של גודל התצוגה, השוליים והריווח הפנימי
  • גודל הגופן והטקסט: החלה מחדש של גודל הטקסט ביחידת sp
  • ציור מותאם אישית View/Canvas: עדכון הערכים מבוססי הפיקסלים שמשמשים לציור Canvas

קביעת הכיוון של האפליקציה

כשמפתחים אפליקציה מותאמת, אסור להסתמך על סיבוב פיזי של המכשיר, כי הפעולה הזו מתעלמת ממכשירים עם מסך גדול, ולאפליקציה במצב ריבוי חלונות יכול להיות כיוון שונה מזה של המכשיר. במקום זאת, אפשר להשתמש ב-Configuration.orientation או ב-WindowMetrics כדי לזהות אם האפליקציה מוצגת כרגע במצב לרוחב או לאורך, על סמך גודל החלון.

פתרון 1: שימוש ב-Configuration.orientation

המאפיין הזה מזהה את הכיוון שבו האפליקציה מוצגת כרגע.

פתרון 2: שימוש ב-WindowMetrics#getBounds()

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

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

תנוחות ומצבי תצוגה

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

כדי להבדיל בין האפליקציה שלכם במכשירים מתקפלים שתומכים ב-HALF_OPENED, אתם יכולים להשתמש בממשקי API של Jetpack WindowManager כמו FoldingFeature.

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

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

נעילת הכיוון לכיוון החיישן הטבעי

בתרחישי שימוש ספציפיים מאוד – במיוחד באפליקציות שצריכות להשתלט על כל המסך בלי קשר למצב הקיפול של המכשיר – אפשר להשתמש בדגל nosensor כדי לנעול את האפליקציה לכיוון הטבעי של המכשיר. לדוגמה, ב-Pixel Fold, האוריינטציה הטבעית של המכשיר כשהוא מקופל היא לאורך, ואילו האוריינטציה הטבעית כשהוא פתוח היא לרוחב. הוספת הדגל nosensor גורמת לכך שהאפליקציה תינעל במצב אנכי כשהיא פועלת במסך החיצוני, ותינעל במצב אופקי כשהיא פועלת במסך הפנימי.

<activity
  android:name=".MainActivity"
  android:screenOrientation="nosensor">

מיפוי מחדש של חיישני XR במשחקים

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

כדי לפתור את הבעיה, בודקים את הערך הנוכחי של Display.getRotation()‎ וממפים מחדש את הצירים בהתאם:

  • סיבוב 0: x=x, y=y
  • Rotation 90: x=-y, y=x
  • סיבוב של 180 מעלות: x=-x, y=-y
  • Rotation 270: x=y, y=-x

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

תאימות האפליקציה

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

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

מידע נוסף על פיתוח אפליקציות מותאמות זמין בהנחיות בנושא איכות אפליקציות מותאמות.