הטמעת פעילות

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

איור 1. אפליקציית ההגדרות עם הפעילויות זו לצד זו.

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

הטמעה של פעילות לא דורשת שינוי של קוד. כדי לקבוע איך הפעילויות באפליקציה יוצגו – זו לצד זו או אחת מעל השנייה – צריך ליצור קובץ תצורת XML או לבצע קריאות ל-API של Jetpack WindowManager.

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

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

הטמעת פעילות נתמכת ברוב המכשירים עם מסך גדול שפועלת בהם מערכת Android 12L (רמת API‏ 32) ומעלה.

פיצול חלון המשימה

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

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

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

איור 2. שתי פעילויות זו לצד זו ופעילות אחת מעל השנייה.

פעילות שתופסת את כל חלון המשימות יכולה ליצור פיצול על ידי הפעלת פעילות חדשה לצדה:

איור 3. פעילות א' מתחילה פעילות ב' בצד.

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

  • בצד, מעל פעילות אחרת:

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

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

    איור 6. פעילות ב' מתחילה את פעילות ג' ללא דגלים נוספים של כוונות.
  • הפעלת פעילות בחלון מלא באותה משימה:

    איור 7. פעילות א' או פעילות ב' מתחילות פעילות ג' שממלאת את חלון המשימות.

ניווט אחורה

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

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

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

לניווט באמצעות תנועות:

  • ‫Android 14 (רמת API‏ 34) ומטה – אירוע החזרה נשלח לפעילות שבה התרחשה התנועה. כשמשתמשים מחליקים מהצד הימני של המסך, אירוע החזרה נשלח לפעילות בחלונית הימנית של המסך המפוצל. כשמשתמשים מחליקים מהצד הימני של המסך, אירוע החזרה נשלח לפעילות בחלונית הימנית.

  • ‫Android מגרסה 15 (רמת API ‏35) ואילך

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

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

פריסה עם חלוניות מרובות

‫Jetpack WindowManager מאפשר לכם ליצור פריסת חלוניות מרובות להטמעת פעילות במכשירים עם מסך גדול שמותקנת בהם גרסה Android 12L‏ (API ברמה 32) ומעלה, ובחלק מהמכשירים עם גרסאות קודמות של הפלטפורמה. אפליקציות קיימות שמבוססות על פעילויות מרובות ולא על פרגמנטים או פריסות מבוססות-תצוגה כמו SlidingPaneLayout יכולות לספק חוויית משתמש משופרת במסכים גדולים בלי לשכתב את קוד המקור.

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

איור 8. שתי פעילויות שהתחילו בו-זמנית בפריסה מרובת חלונות.

פיצול מאפיינים

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

לכללים שמוגדרים בקובץ תצורה מסוג XML, מגדירים את המאפיינים הבאים:

  • splitRatio: הגדרת הפרופורציות של הקונטיינר. הערך הוא מספר ממשי (float) בטווח הפתוח (0.0, 1.0).
  • splitLayoutDirection: מציין את הפריסה של הקונטיינרים המפוצלים ביחס זה לזה. הערכים כוללים:
    • ltr: משמאל לימין
    • rtl: מימין לשמאל
    • locale: הערך ltr או rtl נקבע לפי הגדרת הלוקאל

דוגמאות מופיעות בקטע הגדרת XML.

כדי ליצור כללים באמצעות ממשקי WindowManager API, יוצרים אובייקט SplitAttributes עם SplitAttributes.Builder ומפעילים את שיטות ה-Builder הבאות:

  • setSplitType(): הגדרת הפרופורציות של המאגדים המפוצלים. אפשר לעיין בSplitAttributes.SplitType כדי לראות את הארגומנטים התקינים, כולל השיטה SplitAttributes.SplitType.ratio().
  • setLayoutDirection(): הגדרת הפריסה של הקונטיינרים. במאמר SplitAttributes.LayoutDirection מפורטים הערכים האפשריים.

דוגמאות מופיעות בקטע WindowManager API.

איור 9. שני פיצולים של פעילות מוצגים משמאל לימין, אבל עם יחסי פיצול שונים.

כיוון הפיצול

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

אפשר לציין את כיוון החלוקה באמצעות מחשבון SplitController SplitAttributes. המחשבון מחשב את SplitAttributes עבור SplitRule הפעיל.

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

Kotlin

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val parentConfiguration = params.parentConfiguration
        val builder = SplitAttributes.Builder()
        return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build()
        } else {
            // Fallback to expand the secondary container.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        Configuration parentConfiguration = params.getParentConfiguration();
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build();
        } else {
            // Fallback to expand the secondary container.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

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

Kotlin

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val tag = params.splitRuleTag
        val parentWindowMetrics = params.parentWindowMetrics
        val parentConfiguration = params.parentConfiguration
        val foldingFeatures =
            params.parentWindowLayoutInfo.displayFeatures.filterIsInstance<FoldingFeature>()
        val feature = if (foldingFeatures.size == 1) foldingFeatures[0] else null
        val builder = SplitAttributes.Builder()
        builder.setSplitType(SPLIT_TYPE_HINGE)
        return@setSplitAttributesCalculator if (feature?.isSeparating == true) {
            // Horizontal split for tabletop posture.
            builder
                .setSplitType(SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
                        SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                    } else {
                        SplitAttributes.LayoutDirection.LOCALE
                    }
                )
                .build()
        } else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else {
            // No split for tall displays.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Java

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        String tag = params.getSplitRuleTag();
        WindowMetrics parentWindowMetrics = params.getParentWindowMetrics();
        Configuration parentConfiguration = params.getParentConfiguration();
        List<FoldingFeature> foldingFeatures =
            params.getParentWindowLayoutInfo().getDisplayFeatures().stream().filter(
                    item -> item instanceof FoldingFeature)
                .map(item -> (FoldingFeature) item)
                .collect(Collectors.toList());
        FoldingFeature feature = foldingFeatures.size() == 1 ? foldingFeatures.get(0) : null;
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        builder.setSplitType(SplitType.SPLIT_TYPE_HINGE);
        if (feature != null && feature.isSeparating()) {
            // Horizontal slit for tabletop posture.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    feature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL
                        ? SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                        : SplitAttributes.LayoutDirection.LOCALE)
                .build();
        }
        else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else {
            // No split for tall displays.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

פלייסהולדרים

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

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

איור 10. מכשיר מתקפל עובר למצב מקופל ולמצב לא מקופל. פעילות Placeholder מסיימת את הפעולה ונוצרת מחדש כשגודל המסך משתנה.

עם זאת, המאפיין stickyPlaceholder של המתודה SplitPlaceholderRule או setSticky() של SplitPlaceholder.Builder יכול לבטל את התנהגות ברירת המחדל. אם המאפיין או השיטה מציינים ערך של true, המערכת מציגה את ה-placeholder כפעילות העליונה ביותר בחלון המשימה כשהתצוגה משתנה מתצוגה של שני חלונות לתצוגה של חלון אחד (בהגדרת הפיצול יש דוגמה).

איור 11. מכשיר מתקפל עובר למצב מקופל ולמצב לא מקופל. Placeholder activity is sticky.

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

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

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

אפשר להציג פעילויות זו על גבי זו כי WindowManager מסדר את הפעילויות לפי ציר Z בחלונית המשנית מעל הפעילויות בחלונית הראשית.

פעילויות מרובות בחלונית המשנית

פעילות ב' מתחילה את פעילות ג' במקום בלי דגלים נוספים של כוונות:

פיצול הפעילות שמכיל את הפעילויות א&#39;, ב&#39; ו-ג&#39;, כאשר ג&#39; מוצגת מעל ב&#39;.

כך ייראה סדר הפעילויות באותה משימה:

רשימת פעילויות משנית שמכילה את פעילות C שמופיעה מעל פעילות B.
          רשימת הפעילויות המשנית מוצגת מעל רשימת הפעילויות הראשית,
          שכוללת את הפעילות א&#39;.

לכן, בחלון משימות קטן יותר, האפליקציה מצטמצמת לפעילות אחת עם C בחלק העליון של המערך:

חלון קטן שבו מוצגת רק פעילות ג&#39;.

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

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

פיצולים מוערמים

פעילות ב' מתחילה פעילות ג' בצד ומזיזה את המסך המפוצל לצדדים:

חלון המשימה שבו מוצגות פעילויות א&#39; ו-ב&#39;, ואז פעילויות ב&#39; ו-ג&#39;.

התוצאה היא סדר ה-Z הבא של פעילויות באותה משימה:

פעילויות א&#39;, ב&#39; ו-ג&#39; ברשימה אחת. הפעילויות מוצגות זו מעל זו בסדר הבא מלמעלה למטה: C, ‏ B, ‏ A.

בחלון משימות קטן יותר, האפליקציה מצטמצמת לפעילות אחת עם C בחלק העליון:

חלון קטן שבו מוצגת רק פעילות ג&#39;.

כיוון קבוע לאורך

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

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

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

איור 13. פעילות A במצב אנכי קבוע מתחילה פעילות B בצד.

תמיד צריך להוסיף את המאפיין android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED לקובץ המניפסט של האפליקציה כדי ליידע את המכשירים שהאפליקציה תומכת בהטמעת פעילויות (ראו את הקטע הגדרת פיצול). מכשירים בהתאמה אישית של OEM יכולים לקבוע אם להוסיף מסגרת שחורה לפעילויות במצב אנכי קבוע.

הגדרת פיצול

כללי פיצול מגדירים פיצולים של פעילויות. אפשר להגדיר כללי פיצול בקובץ תצורת XML או באמצעות קריאות ל-API של Jetpack WindowManager.

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

מבצעים את הפעולות הבאות:

  1. מוסיפים את התלות בספריית WindowManager העדכנית לקובץ build.gradle ברמת המודול של האפליקציה, לדוגמה:

    implementation 'androidx.window:window:1.1.0-beta02'

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

  2. מודיעים למערכת שהאפליקציה הטמיעה פעילות.

    מוסיפים את המאפיין android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED לאלמנט <application> בקובץ המניפסט של האפליקציה, ומגדירים את הערך שלו כ-true. לדוגמה:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <property
                android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
                android:value="true" />
        </application>
    </manifest>
    

    ב-WindowManager מגרסה 1.1.0-alpha06 ואילך, הפיצול של הטמעת פעילויות מושבת אלא אם המאפיין מתווסף למניפסט ומוגדר כ-true.

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

הגדרות XML

כדי ליצור הטמעה של הטמעת פעילות שמבוססת על XML, מבצעים את השלבים הבאים:

  1. יוצרים קובץ משאבי XML שמבצע את הפעולות הבאות:

    • הגדרת פעילויות שמשתפות פיצול
    • הגדרת אפשרויות הפיצול
    • יוצר placeholder למאגר המשני של הפיצול כשתוכן לא זמין
    • מציין פעילויות שאסור שיהיו חלק מפיצול

    לדוגמה:

    <!-- main_split_config.xml -->
    
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Define a split for the named activities. -->
        <SplitPairRule
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:finishPrimaryWithSecondary="never"
            window:finishSecondaryWithPrimary="always"
            window:clearTop="false">
            <SplitPairFilter
                window:primaryActivityName=".ListActivity"
                window:secondaryActivityName=".DetailActivity"/>
        </SplitPairRule>
    
        <!-- Specify a placeholder for the secondary container when content is
             not available. -->
        <SplitPlaceholderRule
            window:placeholderActivityName=".PlaceholderActivity"
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:stickyPlaceholder="false">
            <ActivityFilter
                window:activityName=".ListActivity"/>
        </SplitPlaceholderRule>
    
        <!-- Define activities that should never be part of a split. Note: Takes
             precedence over other split rules for the activity named in the
             rule. -->
        <ActivityRule
            window:alwaysExpand="true">
            <ActivityFilter
                window:activityName=".ExpandedActivity"/>
        </ActivityRule>
    
    </resources>
    
  2. יוצרים פונקציית אתחול.

    רכיב WindowManager‏ RuleController מנתח את קובץ התצורה בפורמט XML ומאפשר למערכת להשתמש בכללים. ספריית Startup של Jetpack‏ Initializer מאפשרת לקובץ ה-XML להיות זמין ל-RuleController בזמן הפעלת האפליקציה, כך שהכללים יהיו בתוקף כשמתחילות פעילויות.

    כדי ליצור מאתחל:

    1. מוסיפים את התלות של ספריית Jetpack Startup העדכנית לקובץ build.gradle ברמת המודול, לדוגמה:

      implementation 'androidx.startup:startup-runtime:1.1.1'

    2. יוצרים מחלקה שמטמיעה את הממשק Initializer.

      הפונקציה initializer מעבירה את המזהה של קובץ התצורה של ה-XML ‏ (main_split_config.xml) אל השיטה RuleController.parseRules(), וכך הופכת את כללי הפיצול לזמינים ל-RuleController.

      Kotlin

      class SplitInitializer : Initializer<RuleController> {
      
          override fun create(context: Context): RuleController {
              return RuleController.getInstance(context).apply {
                  setRules(RuleController.parseRules(context, R.xml.main_split_config))
              }
          }
      
          override fun dependencies(): List<Class<out Initializer<*>>> {
              return emptyList()
          }
      }

      Java

      public class SplitInitializer implements Initializer<RuleController> {
      
          @NonNull
          @Override
          public RuleController create(@NonNull Context context) {
              RuleController ruleController = RuleController.getInstance(context);
              ruleController.setRules(
                  RuleController.parseRules(context, R.xml.main_split_config)
              );
               return ruleController;
           }
      
           @NonNull
           @Override
           public List<Class<? extends Initializer<?>>> dependencies() {
               return Collections.emptyList();
           }
      }

  3. יוצרים ספק תוכן להגדרות הכללים.

    מוסיפים את androidx.startup.InitializationProvider לקובץ המניפסט של האפליקציה בתור <provider>. צריך לכלול הפניה להטמעה של פונקציית האתחול RuleController, SplitInitializer:

    <!-- AndroidManifest.xml -->
    
    <provider android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <!-- Make SplitInitializer discoverable by InitializationProvider. -->
        <meta-data android:name="${applicationId}.SplitInitializer"
            android:value="androidx.startup" />
    </provider>
    

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

WindowManager API

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

כדי ליצור פיצול פעילות באופן פרוגרמטי:

  1. יוצרים כלל פיצול:

    1. יוצרים SplitPairFilter שמזהה את הפעילויות שמשתפות את הפיצול:

      Kotlin

      val splitPairFilter = SplitPairFilter(
          ComponentName(this, ListActivity::class.java),
          ComponentName(this, DetailActivity::class.java),
          null
      )

      Java

      SplitPairFilter splitPairFilter = new SplitPairFilter(
         new ComponentName(this, ListActivity.class),
         new ComponentName(this, DetailActivity.class),
         null
      );

    2. מוסיפים את המסנן לקבוצת מסננים:

      Kotlin

      val filterSet = setOf(splitPairFilter)

      Java

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
      ```

    3. יוצרים מאפייני פריסה לפיצול:

      Kotlin

      val splitAttributes: SplitAttributes = SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build()

      Java

      SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();

      SplitAttributes.Builder יוצר אובייקט שמכיל מאפייני פריסה:

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

      Kotlin

      val splitPairRule = SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build()

      Java

      SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build();

      SplitPairRule.Builder יוצר ומגדיר את הכלל:

      • filterSet: מכיל מסנני זוגות מפוצלים שקובעים מתי להחיל את הכלל על ידי זיהוי פעילויות שמשתפות פיצול.
      • setDefaultSplitAttributes(): החלת מאפייני פריסה על הכלל.
      • setMinWidthDp(): הגדרת רוחב התצוגה המינימלי (בפיקסלים בלתי תלויים בדחיסות, dp) שמאפשר פיצול.
      • setMinSmallestWidthDp(): הגדרת הערך המינימלי (ב-dp) שצריך להיות לממד הקטן מבין שני ממדי המסך כדי לאפשר פיצול, ללא קשר לכיוון המכשיר.
      • setMaxAspectRatioInPortrait(): הגדרת יחס הגובה-רוחב המקסימלי של המסך (גובה:רוחב) במצב אנכי שבו מוצגים פיצולים של פעילויות. אם יחס הגובה-רוחב של מסך אורכי חורג מיחס הגובה-רוחב המקסימלי, הפיצול מושבת ללא קשר לרוחב המסך. הערה: ערך ברירת המחדל הוא 1.4, וכתוצאה מכך הפעילויות תופסות את כל חלון המשימות במצב אנכי ברוב הטאבלטים. אפשר לעיין גם במאמרים בנושא SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT וsetMaxAspectRatioInLandscape(). ערך ברירת המחדל של landscape הוא ALWAYS_ALLOW.
      • setFinishPrimaryWithSecondary(): מגדיר איך סיום כל הפעילויות במאגר המשני משפיע על הפעילויות במאגר הראשי. ‫NEVER מציין שהמערכת לא צריכה לסיים את הפעילויות העיקריות כשכל הפעילויות במאגר המשני מסתיימות (ראו סיום פעילויות).
      • setFinishSecondaryWithPrimary(): מגדיר איך סיום כל הפעילויות בקונטיינר הראשי משפיע על הפעילויות בקונטיינר המשני. ‫ALWAYS מציין שהמערכת תמיד צריכה לסיים את הפעילויות במאגר המשני כשכל הפעילויות במאגר הראשי מסתיימות (ראו סיום פעילויות).
      • setClearTop(): מציין אם כל הפעילויות בקונטיינר המשני מסתיימות כשפעילות חדשה מופעלת בקונטיינר. ערך של false מציין שפעילויות חדשות מוערמות על פעילויות שכבר נמצאות במאגר המשני.
    5. מקבלים את מופע הסינגלטון של WindowManager RuleController ומוסיפים את הכלל:

      Kotlin

      val ruleController = RuleController.getInstance(this)
      ruleController.addRule(splitPairRule)

      Java

      RuleController ruleController = RuleController.getInstance(this);
      ruleController.addRule(splitPairRule);

    6. יוצרים placeholder לקונטיינר המשני כשתוכן לא זמין:

    7. יוצרים ActivityFilter שמזהה את הפעילות שאיתה משתף ה-placeholder את החלון המפוצל של המשימה:

      Kotlin

      val placeholderActivityFilter = ActivityFilter(
          ComponentName(this, ListActivity::class.java),
          null
      )

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );

    8. מוסיפים את המסנן לקבוצת מסננים:

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);

    9. יצירת SplitPlaceholderRule:

      Kotlin

      val splitPlaceholderRule = SplitPlaceholderRule.Builder(
          placeholderActivityFilterSet,
          Intent(context, PlaceholderActivity::class.java)
      ).setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
          .setSticky(false)
          .build()

      Java

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            new Intent(this, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();

      SplitPlaceholderRule.Builder יוצר ומגדיר את הכלל:

      • placeholderActivityFilterSet: מכיל מסנני פעילות שקובעים מתי להחיל את הכלל על ידי זיהוי פעילויות שהפעילות של placeholder משויכת אליהן.
      • Intent: מציין את ההשקה של פעילות placeholder.
      • setDefaultSplitAttributes(): החלת מאפייני פריסה על הכלל.
      • setMinWidthDp(): מגדיר את רוחב התצוגה המינימלי (בפיקסלים בלתי תלויים בדחיסות, dp) שמאפשר פיצול.
      • setMinSmallestWidthDp(): מגדיר את הערך המינימלי (ב-dp) שצריך להיות למימד הקטן מבין שני מימדי המסך כדי לאפשר פיצול, ללא קשר לכיוון המכשיר.
      • setMaxAspectRatioInPortrait(): הגדרת יחס הגובה-רוחב המקסימלי של המסך (גובה:רוחב) במצב אנכי שבו מוצגים פיצולים של פעילויות. הערה: ערך ברירת המחדל הוא 1.4, וכתוצאה מכך הפעילויות ממלאות את חלון המשימות ברוב הטאבלטים במצב אנכי. אפשר לעיין גם במאמרים בנושא SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT וsetMaxAspectRatioInLandscape(). ערך ברירת המחדל של landscape הוא ALWAYS_ALLOW.
      • setFinishPrimaryWithPlaceholder(): מגדיר איך סיום הפעילות של placeholder משפיע על הפעילויות במאגר הראשי. ‫ALWAYS מציין שהמערכת תמיד צריכה לסיים את הפעילויות במאגר הראשי כשה-placeholder מסתיים (ראו סיום פעילויות).
      • setSticky(): קובעת אם פעילות ה-placeholder תופיע בראש ערימת הפעילויות במסכים קטנים אחרי שה-placeholder הופיע לראשונה בפיצול עם רוחב מינימלי מספיק.
    10. מוסיפים את הכלל ל-WindowManager RuleController:

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);

  2. ציון פעילויות שאסור לכלול בפיצול:

    1. יוצרים תג ActivityFilter שמזהה פעילות שתמיד צריכה לתפוס את כל האזור של תצוגת המשימות:

      Kotlin

      val expandedActivityFilter = ActivityFilter(
          ComponentName(this, ExpandedActivity::class.java),
          null
      )

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
          new ComponentName(this, ExpandedActivity.class),
          null
      );

    2. מוסיפים את המסנן לקבוצת מסננים:

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);

    3. יצירת ActivityRule:

      Kotlin

      val activityRule = ActivityRule.Builder(expandedActivityFilterSet)
          .setAlwaysExpand(true)
          .build()

      Java

      ActivityRule activityRule = new ActivityRule.Builder(
          expandedActivityFilterSet
      ).setAlwaysExpand(true)
       .build();

      ActivityRule.Builder יוצר ומגדיר את הכלל:

      • expandedActivityFilterSet: מכיל מסנני פעילות שקובעים מתי להחיל את הכלל על ידי זיהוי פעילויות שרוצים להחריג מהפיצולים.
      • setAlwaysExpand(): מציין אם הפעילות צריכה למלא את כל חלון המשימה.
    4. מוסיפים את הכלל ל-WindowManager RuleController:

      Kotlin

      ruleController.addRule(activityRule)

      Java

      ruleController.addRule(activityRule);

הטמעה באפליקציות שונות

ב-Android 13 (רמת API‏ 33) ומעלה, אפליקציות יכולות להטמיע פעילויות מאפליקציות אחרות. הטמעת פעילות בין אפליקציות, או בין מזהי UID, מאפשרת שילוב חזותי של פעילויות מכמה אפליקציות ל-Android. המערכת מציגה פעילות של אפליקציית המארח ופעילות מוטמעת מאפליקציה אחרת במסך זו לצד זו או זו מעל זו, בדיוק כמו בהטמעה של פעילות באפליקציה יחידה.

לדוגמה, אפליקציית ההגדרות יכולה להטמיע את פעילות בחירת הטפט מאפליקציית WallpaperPicker:

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

מודל אמון

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

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

מארחים מהימנים

כדי לאפשר לאפליקציות אחרות להטמיע פעילויות מהאפליקציה שלכם ולשלוט באופן מלא בהצגה שלהן, צריך לציין את אישור ה-SHA-256 של אפליקציית המארח במאפיין android:knownActivityEmbeddingCerts של הרכיבים <activity> או <application> בקובץ המניפסט של האפליקציה.

מגדירים את הערך של android:knownActivityEmbeddingCerts כמחרוזת:

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@string/known_host_certificate_digest"
    ... />

או, כדי לציין כמה אישורים, מערך של מחרוזות:

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@array/known_host_certificate_digests"
    ... />

שמפנה למשאב כמו זה:

<resources>
    <string-array name="known_host_certificate_digests">
      <item>cert1</item>
      <item>cert2</item>
      ...
    </string-array>
</resources>

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

מארחים לא מהימנים

כדי לאפשר לכל אפליקציה להטמיע את הפעילויות של האפליקציה שלכם ולשלוט באופן ההצגה שלהן, צריך לציין את המאפיין android:allowUntrustedActivityEmbedding באלמנטים <activity> או <application> במניפסט של האפליקציה, לדוגמה:

<activity
    android:name=".MyEmbeddableActivity"
    android:allowUntrustedActivityEmbedding="true"
    ... />

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

אימות בהתאמה אישית

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

משתמשים בשיטה ActivityEmbeddingController#isActivityEmbedded() מהספרייה Jetpack WindowManager כדי לבדוק אם מארח מטמיע את הפעילות שלכם, למשל:

Kotlin

fun isActivityEmbedded(activity: Activity): Boolean {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
}

Java

boolean isActivityEmbedded(Activity activity) {
    return ActivityEmbeddingController.getInstance(context).isActivityEmbedded(activity);
}

הגבלת גודל מינימלי

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

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

<activity-alias>

כדי שהטמעת פעילות מהימנה או לא מהימנה תפעל עם הרכיב <activity-alias>, צריך להחיל את android:knownActivityEmbeddingCerts או את android:allowUntrustedActivityEmbedding על פעילות היעד ולא על הכינוי. המדיניות שמאמתת את האבטחה בשרת המערכת מבוססת על הדגלים שהוגדרו ביעד, ולא על הכינוי.

אפליקציה מארחת

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

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

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

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

דוגמאות לפיצול

פיצול מהחלון המלא

איור 15. פעילות א' מתחילה פעילות ב' בצד.

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

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

פיצול כברירת מחדל

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

איור 16. מסך מפוצל שנוצר כשפותחים שתי פעילויות בו-זמנית. פעילות אחת היא placeholder.

כדי ליצור פיצול עם placeholder, יוצרים placeholder ומשייכים אותו לפעילות הראשית:

<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity">
    <ActivityFilter
        window:activityName=".MainActivity"/>
</SplitPlaceholderRule>

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

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

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

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    RuleController.getInstance(this)
        .addRule(new SplitPairRule.Builder(filterSet).build());
    startActivity(new Intent(this, DetailActivity.class));
}

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

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

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

במקום זאת, אפשר לסיים את שתי הפעילויות בו-זמנית באמצעות המאפיין finishPrimaryWithSecondary:

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".ListActivity"
        window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

אפשר לעיין בקטע מאפייני הגדרה.

כמה פעילויות במאגרי תגים מפוצלים

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

איור 18. הפעילות נפתחה במקום בחלונית המשנית של חלון המשימות.

Kotlin

class DetailActivity : AppCompatActivity() {
    fun onOpenSubdetail() {
        startActivity(Intent(this, SubdetailActivity::class.java))
    }
}

Java

public class DetailActivity  extends AppCompatActivity {
    void onOpenSubdetail() {
        startActivity(new Intent(this, SubdetailActivity.class));
    }
}

פעילות המשנה ממוקמת מעל פעילות הפרטים ומסתירה אותה:

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

איור 19. הפעילות הוסרה מהחלק העליון של המקבץ.

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

פעילויות במשימה חדשה

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

איור 20. מתחילים את פעילות C במשימה חדשה מפעילות B.

החלפת פעילות

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

איור 21. פעילות ניווט ברמה העליונה בחלונית הראשית מחליפה פעילויות ביעד בחלונית המשנית.

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

במקרים כאלה, צריך להסיר את מסך א' ממחסנית החזרה.

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

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

Kotlin

inner class MenuActivity : AppCompatActivity() {
    fun onMenuItemSelected(selectedMenuItem: Int) {
        startActivity(Intent(this, classForItem(selectedMenuItem)))
    }
}

Java

public class MenuActivity extends AppCompatActivity{
    void onMenuItemSelected(int selectedMenuItem) {
        startActivity(new Intent(this, classForItem(selectedMenuItem)));
    }
}

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

פיצולים מרובים

אפליקציות יכולות לספק ניווט עמוק בכמה רמות על ידי הפעלת פעילויות נוספות בצד.

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

איור 22. פעילות ב' מתחילה פעילות ג' בצד.

מחסנית החזרה מכילה את כל הפעילויות שנפתחו קודם, כך שהמשתמשים יכולים לנווט לפיצול A/B אחרי סיום הפעילות C.

פעילויות א&#39;, ב&#39; ו-ג&#39; ברשימת פעילויות. הפעילויות מוצגות זו מעל זו בסדר הבא, מלמעלה למטה: C,‏ B,‏ A.

כדי ליצור פיצול חדש, מפעילים את הפעילות החדשה בצד ממאגר התגים המשני הקיים. מצהירים על ההגדרות של פיצולי A/B ו-B/C ומפעילים את פעילות C כרגיל מ-B:

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
    <SplitPairFilter
        window:primaryActivityName=".B"
        window:secondaryActivityName=".C"/>
</SplitPairRule>

Kotlin

class B : AppCompatActivity() {
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B extends AppCompatActivity{
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

תגובה לשינויים במצב המסך המפוצל

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

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

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

איור 24. רכיבי ממשק משתמש כפולים בפילוח הפעילות.

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

Kotlin

val layout = layoutInflater.inflate(R.layout.activity_main, null)
val view = layout.findViewById<View>(R.id.infoButton)
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        splitController.splitInfoList(this@SplitDeviceActivity) // The activity instance.
            .collect { list ->
                view.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
            }
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    new SplitControllerCallbackAdapter(SplitController.getInstance(this))
        .addSplitListener(
            this,
            Runnable::run,
            splitInfoList -> {
                View layout = getLayoutInflater().inflate(R.layout.activity_main, null);
                layout.findViewById(R.id.infoButton).setVisibility(
                    splitInfoList.isEmpty() ? View.VISIBLE : View.GONE);
            });
}

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

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

חלון עזר במסך מלא

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

אפשר להגדיר פעילות כך שתמיד תתפוס את כל חלון המשימות באמצעות ההגדרה expand:

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

השלמת פעילויות

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

איור 25. תנועת החלקה שמסיימת את פעילות ב'.
איור 26. תנועת החלקה שמסיימת את פעילות א'.

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

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

מאפייני ההגדרה

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

  • window:finishPrimaryWithSecondary — איך סיום כל הפעילויות במאגר המשני משפיע על הפעילויות במאגר הראשי
  • window:finishSecondaryWithPrimary — איך השלמת כל הפעילויות במאגר הראשי משפיעה על הפעילויות במאגר המשני

הערכים האפשריים של המאפיינים כוללים:

  • always — תמיד מסיימים את הפעילויות בקונטיינר המשויך
  • never — הפעילויות אף פעם לא מסתיימות בקונטיינר המשויך
  • adjacent — סיום הפעילויות במאגר המשויך כששני המאגרים מוצגים זה לצד זה, אבל לא כששני המאגרים מוצגים אחד מעל השני

לדוגמה:

<SplitPairRule
    <!-- Do not finish primary container activities when all secondary container activities finish. -->
    window:finishPrimaryWithSecondary="never"
    <!-- Finish secondary container activities when all primary container activities finish. -->
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

הגדרות ברירת המחדל

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

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

חלון מפוצל שמכיל את הפעילויות A ו-B. החלון A מסתיים, והחלון B תופס את כל החלון.

חלון מפוצל שמכיל את הפעילויות A ו-B. החלון B מסתיים, והחלון A תופס את כל החלון.

איך מסיימים פעילויות ביחד

הפעילויות במאגר הראשי מסתיימות אוטומטית כשכל הפעילויות במאגר המשני מסתיימות:

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

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

חלון מפוצל שמכיל את הפעילויות A ו-B. משימה א&#39; מסתיימת, ומשימה ב&#39; נשארת לבד בחלון המשימות.

השלמת הפעילויות במאגר המשני באופן אוטומטי כשכל הפעילויות במאגר הראשי מסתיימות:

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

חלון מפוצל שמכיל את הפעילויות A ו-B. משימה א&#39; מסתיימת, וכך גם משימה ב&#39;, והחלון של המשימות נשאר ריק.

חלון מפוצל שמכיל את הפעילויות A ו-B. משימה B מסתיימת, ומשימה A נשארת לבד בחלון המשימות.

סיום הפעילויות יחד כשכל הפעילויות במיכל הראשי או המשני מסתיימות:

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

חלון מפוצל שמכיל את הפעילויות A ו-B. משימה א&#39; מסתיימת, וכך גם משימה ב&#39;, והחלון של המשימות נשאר ריק.

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

השלמת כמה פעילויות במאגרי פעילויות

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

לדוגמה, אם שתי פעילויות נמצאות במאגר המשני, C מעל B:

רשימת פעילויות משנית שמכילה את פעילות C שמופיעה מעל B
          מופיעה מעל רשימת הפעילויות הראשית שמכילה את פעילות
          A.

ההגדרה של הפיצול מוגדרת על ידי ההגדרה של הפעילויות א' וב':

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

אם מסיימים את הפעילות העליונה, המסך יישאר מפוצל.

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, פעילות ג&#39; מוצגת מעל פעילות ב&#39;. משתתף ג&#39; מסיים את הפעילות ועוזב, ומשתתפים א&#39; וב&#39; נשארים בפעילות המפוצלת.

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

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, פעילות ג&#39; מוצגת מעל פעילות ב&#39;. משתמש ב&#39; מסיים את הפעילות, ומשתמשים א&#39; וג&#39; נשארים בפעילות המפוצלת.

כללים נוספים לסיום פעילויות משותפות, כמו סיום הפעילות המשנית עם הפעילות הראשית, מופעלים גם הם:

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, כאשר ג&#39; מוצגת מעל ב&#39;. ‫A מסתיימת, וגם B ו-C מסתיימות.

וכשהפיצול מוגדר כך שהפעולות הראשיות והמשניות יסתיימו יחד:

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, פעילות ג&#39; מוצגת מעל פעילות ב&#39;. משתתף ג&#39; מסיים את הפעילות ועוזב, ומשתתפים א&#39; וב&#39; נשארים בפעילות המפוצלת.

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, פעילות ג&#39; מוצגת מעל פעילות ב&#39;. משתמש ב&#39; מסיים את הפעילות, ומשתמשים א&#39; וג&#39; נשארים בפעילות המפוצלת.

פיצול עם פעילות א&#39; במאגר הראשי ופעילויות ב&#39; ו-ג&#39; במאגר המשני, פעילות ג&#39; מוצגת מעל פעילות ב&#39;. משימה A מסתיימת, וכך גם משימות B ו-C.

שינוי מאפייני הפיצול בזמן הריצה

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

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

מאפייני פיצול דינמי

‫Android 15 (רמת API‏ 35) ומעלה נתמכות על ידי Jetpack WindowManager 1.4 ומעלה, ומציעות תכונות דינמיות שמאפשרות להגדיר את הפיצולים של הטמעת הפעילות, כולל:

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

הרחבת החלונית

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

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

  1. יצירת מופע של DividerAttributes

  2. התאמה אישית של מאפייני המחיצה:

    • color: הצבע של המפריד בין החלוניות שאפשר לגרור.

    • widthDp: רוחב המפריד של החלונית שאפשר לגרור. מגדירים את הערך כ-WIDTH_SYSTEM_DEFAULT כדי לאפשר למערכת לקבוע את רוחב הקו המפריד.

    • טווח הגרירה: האחוז המינימלי מהמסך שכל אחד מהחלקים יכול לתפוס. הערך יכול להיות בין 0.33 ל-0.66. מגדירים את הערך כ-DRAG_RANGE_SYSTEM_DEFAULT כדי לאפשר למערכת לקבוע את טווח הגרירה.

    Kotlin

    val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
    
    if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
        splitAttributesBuilder.setDividerAttributes(
            DividerAttributes.DraggableDividerAttributes.Builder()
                .setColor(getColor(R.color.divider_color))
                .setWidthDp(4)
                .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
                .build()
        )
    }
    val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

    Java

    SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);
    
    if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
        splitAttributesBuilder.setDividerAttributes(
          new DividerAttributes.DraggableDividerAttributes.Builder()
            .setColor(ContextCompat.getColor(this, R.color.divider_color))
            .setWidthDp(4)
            .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
            .build()
        );
    }
    SplitAttributes _splitAttributes = splitAttributesBuilder.build();

הצמדה של רשימת פעילויות

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

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

  1. מוסיפים לחצן לקובץ הפריסה של הפעילות שרוצים להצמיד. לדוגמה, פעילות הפרטים של פריסת רשימה-פרטים:

    <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/detailActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/white"
     tools:context=".DetailActivity">
    
    <TextView
       android:id="@+id/textViewItemDetail"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="36sp"
       android:textColor="@color/obsidian"
       app:layout_constraintBottom_toTopOf="@id/pinButton"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
    
    <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/pinButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/pin_this_activity"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. ב-method onCreate() של הפעילות, מגדירים onclick listener על הכפתור:

    Kotlin

    val pinButton: Button = findViewById(R.id.pinButton)
    pinButton.setOnClickListener {
        val splitAttributes: SplitAttributes = SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build()
    
        val pinSplitRule = SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build()
    
        SplitController.getInstance(applicationContext)
            .pinTopActivityStack(taskId, pinSplitRule)
    }

    Java

    Button pinButton = findViewById(R.id.pinButton);
    pinButton.setOnClickListener( (view) -> {
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();
    
        SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();
    
        SplitController.getInstance(getApplicationContext())
            .pinTopActivityStack(getTaskId(), pinSplitRule);
    });

העמעום של תיבת דו-שיח במסך מלא

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

ב-WindowManager מגרסה 1.4 ואילך, חלון האפליקציה כולו מוצלל כברירת מחדל כשנפתח דו-שיח (ראו EmbeddingConfiguration.DimAreaBehavior.ON_TASK).

כדי להחליש רק את הרקע של הפעילות שפתחה את תיבת הדו-שיח, משתמשים ב-EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK.

העברה של פעילות מחלון מפוצל לחלון מלא

יוצרים הגדרה חדשה שמציגה את הפעילות הצדדית בחלון מלא, ואז מפעילים מחדש את הפעילות באמצעות Intent שמוביל לאותו מופע.

בדיקה אם יש תמיכה בפיצול בזמן ריצה

הטמעת פעילות נתמכת ב-Android 12L‏ (API ברמה 32) ומעלה, אבל היא זמינה גם במכשירים מסוימים שמותקנות בהם גרסאות קודמות של הפלטפורמה. כדי לבדוק בזמן הריצה אם התכונה זמינה, משתמשים במאפיין SplitController.splitSupportStatus או בשיטה SplitController.getSplitSupportStatus():

Kotlin

if (SplitController.getInstance(this).splitSupportStatus ==
    SplitController.SplitSupportStatus.SPLIT_AVAILABLE
) {
    // Device supports split activity features.
}

Java

if (SplitController.getInstance(this).getSplitSupportStatus() ==
    SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
    // Device supports split activity features.
}

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

מניעת שינוי מברירת המחדל של המערכת

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

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

האפליקציה יכולה למנוע או לאפשר הטמעה של פעילות המערכת על ידי הגדרת PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE בקובץ המניפסט של האפליקציה, למשל:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
            android:value="true|false" />
    </application>
</manifest>

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

בלי קשר לרמת ה-API המטורגטת של האפליקציה.

מגבלות, הגבלות וסייגים

  • רק אפליקציית המארח של המשימה, שמזוהה כבעלים של פעילות הבסיס במשימה, יכולה לארגן ולהטמיע פעילויות אחרות במשימה. אם פעילויות שתומכות בהטמעה ובפיצול מופעלות במשימה ששייכת לאפליקציה אחרת, ההטמעה והפיצול לא יפעלו עבור הפעילויות האלה.
  • אפשר לארגן פעילויות רק בתוך משימה אחת. כשמפעילים פעילות במשימה חדשה, היא תמיד מופיעה בחלון חדש מורחב מחוץ לכל חלוקה קיימת.
  • אפשר לארגן ולפצל רק פעילויות שנמצאות באותו תהליך. הקריאה החוזרת SplitInfo מדווחת רק על פעילויות ששייכות לאותו תהליך, כי אין דרך לדעת על פעילויות בתהליכים שונים.
  • כל כלל של פעילות יחידה או זוג פעילויות חל רק על הפעלות של פעילויות שמתרחשות אחרי שהכלל נרשם. נכון לעכשיו, אין אפשרות לעדכן פיצולים קיימים או את המאפיינים החזותיים שלהם.
  • ההגדרה של המסנן לפי זוגות מפוצלים צריכה להיות זהה לכוונות שמשמשות להפעלת פעילויות באופן מלא. ההתאמה מתרחשת ברגע שמתחילה פעילות חדשה מתהליך האפליקציה, ולכן יכול להיות שהיא לא תדע על שמות רכיבים שנפתרים מאוחר יותר בתהליך המערכת כשמשתמשים ב-intent משתמע. אם שם הרכיב לא ידוע בזמן ההפעלה, אפשר להשתמש בתו כללי במקום זאת (‎*/*‎) ולסנן על סמך פעולת הכוונה.
  • בשלב הזה, אין אפשרות להעביר פעילויות בין מאגרי תגים או אל תוך פיצולים ומחוצה להם אחרי שהם נוצרו. הפיצולים נוצרים רק על ידי ספריית WindowManager כשמופעלות פעילויות חדשות עם כללים תואמים, והם נהרסים כשהפעילות האחרונה במאגר הפיצולים מסתיימת.
  • אפשר להפעיל מחדש פעילויות כשמבצעים שינויים בהגדרות. לכן, כשיוצרים או מסירים פיצול ומשנים את גבולות הפעילות, הפעילות יכולה לעבור השמדה מלאה של המופע הקודם ויצירה של מופע חדש. לכן, מפתחי אפליקציות צריכים להיזהר כשמבצעים פעולות כמו הפעלת פעילויות חדשות מתוך קריאות חוזרות (callbacks) של מחזור החיים.
  • המכשירים צריכים לכלול את ממשק התוספים של החלונות כדי לתמוך בהטמעת פעילות. כמעט כל המכשירים עם מסך גדול שפועלת בהם מערכת Android 12L (רמת API‏ 32) ומעלה כוללים את הממשק. עם זאת, חלק מהמכשירים עם מסך גדול שלא יכולים להריץ כמה פעילויות לא כוללים את ממשק התוספים של החלון. אם מכשיר עם מסך גדול לא תומך במצב ריבוי חלונות, יכול להיות שהוא לא תומך בהטמעת פעילות.

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