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

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

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

מידע על החלון

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

‫WindowManager מספק תמיכה באיסוף WindowLayoutInfo נתונים באמצעות Kotlin flows ו-Java callbacks.

תהליכים ב-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

שכבת התאימות של הקריאה החוזרת שכלולה בתלות androidx.window:window-java מאפשרת לכם לאסוף עדכוני WindowLayoutInfo בלי להשתמש ב-Kotlin flow. הארטיפקט כולל את המחלקה WindowInfoTrackerCallbackAdapter, שמתאימה את WindowInfoTracker כדי לתמוך ברישום (ובביטול הרישום) של קריאות חוזרות (callback) לקבלת עדכונים של 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), תוכלו להשתמש בארטיפקטים שמאפשרים לכם להשתמש ב-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 כ-true, כי המסך מחולק לשני אזורי תצוגה.isSeparating בנוסף, הערך של isSeparating הוא תמיד True במכשיר עם שני מסכים כשהאפליקציה מוצגת בשני המסכים.

המאפיין 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);
}

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

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

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

איך משתמשים בסיווגים של גודל החלון

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

טעימות

  • ‫Jetpack WindowManager: דוגמה לשימוש בספריית Jetpack WindowManager
  • Jetcaster : הטמעה של תנוחת ישיבה נכונה באמצעות Compose

Codelabs