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

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