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

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

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

פרטי החלון

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

WindowManager מספק תמיכה באיסוף נתוני WindowLayoutInfo באמצעות תהליכים של Kotlin וקריאות חזרה (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.
                    }
            }
        }
    }
}

קריאה חוזרת (callback) ב-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), אפשר להשתמש בפריטי מידע שנוצרו בתהליך פיתוח (Artifact) שמאפשרים להשתמש 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 תמיד יהיה 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);
}

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

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

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

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

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

דוגמיות

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

שיעורי Lab