הפעלת מודעות לקיפול באפליקציה

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

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

פרטי החלון

הממשק WindowInfoTracker ב-Jetpack WindowManager חושף מידע על הפריסה של החלון. ה-method windowLayoutInfo() בממשק מחזירה זרם של נתונים WindowLayoutInfo שמיידעים את האפליקציה על מצב קיפול של מכשיר מתקפל. השיטה WindowInfoTracker#getOrCreate() יוצרת מכונה של WindowInfoTracker.

windowManager תומך באיסוף נתוני WindowLayoutInfo באמצעות תהליכי Kootlin ו-callbacks ב-Java.

תהליכים ב-Kotlin

כדי להתחיל ולהפסיק את איסוף הנתונים של WindowLayoutInfo, אפשר להשתמש בקורוטין בהתאם למחזור החיים שאפשר להפעיל מחדש, שבו מופעל בלוק הקוד repeatOnLifecycle כשמחזור החיים הוא STARTED לפחות ומופסק כשמחזור החיים הוא STOPPED. כשמחזור החיים חוזר להיות STARTED, הביצוע של בלוק הקוד מתחיל מחדש באופן אוטומטי. ��דוגמה הבאה, בלוק הקוד אוסף נתוני WindowLayoutInfo ומשתמש בהם:

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

קריאות חזרה (callbacks) ב-Java

שכבת התאימות לקריאה חוזרת (callback) שכלולה בתלות androidx.window:window-java מאפשרת לאסוף עדכוני WindowLayoutInfo בלי להשתמש בתהליך של Kotlin. הארטיפקט כולל את המחלקה WindowInfoTrackerCallbackAdapter, שמתאימה את WindowInfoTracker כדי לתמוך ברישום (ובביטול הרישום) של קריאות חזרה (callbacks) לקבלת עדכוני WindowLayoutInfo, לדוגמה:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

תמיכה ב-RxJava

אם אתם כבר משתמשים ב-RxJava (גרסה 2 או 3), תוכלו להשתמש ב-artifacts שמאפשרים להשתמש ב-Observable או ב-Flowable כדי לאסוף עדכונים של WindowLayoutInfo בלי להשתמש בתהליך של Kotlin.

שכבת התאימות שסיפקנו באמצעות יחסי התלות androidx.window:window-rxjava2 ו-androidx.window:window-rxjava3 כוללת את השיטות WindowInfoTracker#windowLayoutInfoFlowable() ו-WindowInfoTracker#windowLayoutInfoObservable(), שמאפשרות לאפליקציה לקבל עדכוני WindowLayoutInfo. לדוגמה:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable.
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates.
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout.
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose of the WindowLayoutInfo observable.
        disposable?.dispose()
   }
}

תכונות של מסכים מתקפלים

הכיתה WindowLayoutInfo של Jetpack WindowManager מאפשרת להציג את התכונות של חלון תצוגה כרשימה של רכיבי DisplayFeature.

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

  • state: המצב המקופל של המכשיר, FLAT או HALF_OPENED

  • orientation: הכיוון של הקיפול או הציר, HORIZONTAL או VERTICAL

  • occlusionType: אם הקיפול או הציר מסתירים חלק מהמסך, NONE או FULL

  • isSeparating: אם הצירוף או הציר יוצרים שתי אזורי תצוגה לוגיים, ‎true או ‎false

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

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

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker when the lifecycle is
            // STARTED and stops collection when the lifecycle is STOPPED.
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information.
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }

        }
    }
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout.
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                // Use information from the feature object.
            }
        }
    }
}

מצב שולחני

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

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

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

משתמשים ב-FoldingFeature.State וב-FoldingFeature.Orientation כדי לקבוע אם המכשיר נמצא במצב שולחני:

Kotlin

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Java

boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

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

ב-Android 15 ואילך (רמת API‏ 35 ואילך), אפשר להפעיל ממשק API סינכרוני כדי לזהות אם המכשיר תומך במצב שולחני, ללא קשר למצב הנוכחי של המכשיר.

ה-API מציג רשימה של מצבים שבהם המכשיר תומך. אם הרשימה מכילה את המיקום 'שולחן', תוכלו לפצל את פריסת האפליקציה כך שתתמוך במיקום הזה ולהריץ בדיקות A/B בממשק המשתמש של האפליקציה לפריסת שולחן ולפריסת מסך מלא.

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

דוגמאות

מצב הספר

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

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

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

Kotlin

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

Java

boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

שינויים בגודל החלון

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

בעזרת הכיתה WindowManager של Jetpack‏ WindowMetricsCalculator אפשר לאחזר את מדדי החלון הנוכחי והמקסימלי. בדומה לפלטפורמה WindowMetrics שהוצגה ברמת API ‏30, ה-WindowManager‏WindowMetrics מספק את גבולות החלון, אבל ה-API תואם לאחור עד לרמת API ‏14.

מידע נוסף זמין בקטע שימוש בסיווגים של גדלים של חלונות.

מקורות מידע נוספים

דוגמיות

  • WindowManager ב-Jetpack: דוגמה לשימוש בספריית WindowManager של Jetpack
  • Jetcaster : הטמעת מצב 'על משטח, מסך למעלה' באמצעות 'כתיבה'

Codelabs