הטמעת פעילות מאפשרת לבצע אופטימיזציה של אפליקציות במכשירים עם מסכים גדולים. האופטימיזציה מתבצעת על ידי פיצול חלון המשימות של האפליקציה בין שתי פעילויות או בין שני מופעים של אותה פעילות.
אם האפליקציה שלכם מורכבת מכמה פעילויות, הטמעת פעילויות מאפשרת לכם לספק חוויית משתמש משופרת בטאבלטים, במכשירים מתקפלים ובמכשירי ChromeOS.
הטמעה של פעילות לא דורשת שינוי של קוד. כדי לקבוע איך הפעילויות באפליקציה יוצגו – זו לצד זו או אחת מעל השנייה – צריך ליצור קובץ תצורת XML או לבצע קריאות ל-API של Jetpack WindowManager.
התמיכה במסכים קטנים מתבצעת באופן אוטומטי. כשהאפליקציה פועלת במכשיר עם מסך קטן, הפעילויות מוצגות אחת מעל השנייה. במסכים גדולים, הפעילויות מוצגות זו לצד זו. המערכת קובעת את המצגת על סמך ההגדרה שיצרתם – לא נדרשת לוגיקה של הסתעפות.
הטמעת פעילות מתאימה לשינויים בהתמצאות המכשיר, והיא פועלת בצורה חלקה במכשירים מתקפלים. הפעילויות נערמות ומוסרות מהערמה כשהמכשיר מתקפל ונפרש.
הטמעת פעילות נתמכת ברוב המכשירים עם מסך גדול שפועלת בהם מערכת Android 12L (רמת API 32) ומעלה.
פיצול חלון המשימה
הטמעת פעילות מפצלת את חלון המשימות של האפליקציה לשני מאגרים: ראשי ומשני. הקונטיינרים מכילים פעילויות שהופעלו מהפעילות הראשית או מפעילויות אחרות שכבר נמצאות בקונטיינרים.
הפעילויות מוערמות במאגר המשני כשהן מופעלות, והמאגר המשני מוערם מעל המאגר הראשי במסכים קטנים, כך שהערמת הפעילויות והניווט אחורה עקביים עם סדר הפעילויות שכבר מוטמע באפליקציה.
הטמעת פעילויות מאפשרת לכם להציג פעילויות במגוון דרכים. האפליקציה יכולה לפצל את חלון המשימות על ידי הפעלת שתי פעילויות בו-זמנית זו לצד זו או אחת מעל השנייה:
פעילות שתופסת את כל חלון המשימות יכולה ליצור פיצול על ידי הפעלת פעילות חדשה לצדה:
פעילויות שכבר נמצאות במסך מפוצל ומשתפות חלון משימות יכולות להפעיל פעילויות אחרות בדרכים הבאות:
בצד, מעל פעילות אחרת:
איור 4. פעילות א' מתחילה פעילות ג' בצד מעל פעילות ב'. לצד, והזזת המסך המפוצל לצדדים, כך שהפעילות הראשית הקודמת מוסתרת:
איור 5. פעילות ב' מתחילה פעילות ג' בצד ומזיזה את המסך המפוצל הצידה. הפעלת פעילות במקום, כלומר באותה רשימת פעילויות:
איור 6. פעילות ב' מתחילה את פעילות ג' ללא דגלים נוספים של כוונות. הפעלת פעילות בחלון מלא באותה משימה:
איור 7. פעילות א' או פעילות ב' מתחילות פעילות ג' שממלאת את חלון המשימות.
ניווט אחורה
לסוגים שונים של אפליקציות יכולים להיות כללים שונים לניווט אחורה במצב של חלון מפוצל, בהתאם לתלות בין הפעילויות או לאופן שבו המשתמשים מפעילים את אירוע החזרה, למשל:
- פעולות שקשורות זו לזו: אם הפעילויות קשורות זו לזו, ולא כדאי להציג אחת בלי השנייה, אפשר להגדיר את הניווט אחורה כך שיסיים את שתיהן.
- פעילות עצמאית: אם הפעילויות עצמאיות לחלוטין, ניווט אחורה בפעילות לא משפיע על המצב של פעילות אחרת בחלון המשימה.
כשמשתמשים בניווט באמצעות לחצנים, האירוע back נשלח לפעילות האחרונה שהייתה בפוקוס.
לניווט באמצעות תנועות:
Android 14 (רמת API 34) ומטה – אירוע החזרה נשלח לפעילות שבה התרחשה התנועה. כשמשתמשים מחליקים מהצד הימני של המסך, אירוע החזרה נשלח לפעילות בחלונית הימנית של המסך המפוצל. כשמשתמשים מחליקים מהצד הימני של המסך, אירוע החזרה נשלח לפעילות בחלונית הימנית.
Android מגרסה 15 (רמת API 35) ואילך
כשמבצעים כמה פעולות באותה אפליקציה, תנועת האצבע מסיימת את הפעולה העליונה בלי קשר לכיוון ההחלקה, וכך מספקת חוויה אחידה יותר.
בתרחישים שכוללים שתי פעילויות מאפליקציות שונות (שכבת-על), אירוע החזרה מופנה לפעילות האחרונה שבמוקד, בהתאם להתנהגות של ניווט באמצעות לחצנים.
פריסה עם חלוניות מרובות
Jetpack WindowManager מאפשר לכם ליצור פריסת חלוניות מרובות להטמעת פעילות במכשירים עם מסך גדול שמותקנת בהם גרסה Android 12L (API ברמה 32) ומעלה, ובחלק מהמכשירים עם גרסאות קודמות של הפלטפורמה. אפליקציות קיימות שמבוססות על
פעילויות מרובות ולא על פרגמנטים או פריסות מבוססות-תצוגה כמו SlidingPaneLayout יכולות לספק חוויית משתמש משופרת במסכים גדולים
בלי לשכתב את קוד המקור.
דוגמה נפוצה היא פיצול של רשימה ופרטים. כדי להבטיח הצגה באיכות גבוהה, המערכת מתחילה את פעילות הרשימה, ואז האפליקציה מתחילה מיד את פעילות הפרטים. מערכת המעבר ממתינה עד ששתי הפעילויות מצוירות, ואז מציגה אותן יחד. המשתמש רואה את שתי הפעילויות כפעולה אחת.
פיצול מאפיינים
אתם יכולים לציין את היחס בין החלונות המפוצלים של המשימות ואת הפריסה של החלונות ביחס זה לזה.
לכללים שמוגדרים בקובץ תצורה מסוג 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.
כיוון הפיצול
המידות ויחס הגובה-רוחב של המסך קובעים את המיקום של הפעילויות בחלוקות של הטמעת פעילויות. במסכים גדולים לרוחב, הפעילויות מוצגות זו לצד זו. במסכים גבוהים לאורך או במצב שולחן במכשירים מתקפלים, הפעילויות מוצגות זו מעל זו.
אפשר לציין את כיוון החלוקה באמצעות מחשבון 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 עם מצב אתחול מחדש.
עם זאת, המאפיין stickyPlaceholder של המתודה SplitPlaceholderRule או setSticky() של SplitPlaceholder.Builder יכול לבטל את התנהגות ברירת המחדל. אם המאפיין או השיטה מציינים ערך של true, המערכת מציגה את ה-placeholder כפעילות העליונה ביותר בחלון המשימה כשהתצוגה משתנה מתצוגה של שני חלונות לתצוגה של חלון אחד (בהגדרת הפיצול יש דוגמה).
שינויים בגודל החלון
כששינויים בתצורת המכשיר מצמצמים את הרוחב של חלון המשימה כך שהוא לא גדול מספיק לפריסה מרובת חלוניות (לדוגמה, כשמכשיר מתקפל עם מסך גדול מתקפל מגודל של טאבלט לגודל של טלפון, או כשמשנים את הגודל של חלון האפליקציה במצב ריבוי חלונות), הפעילויות שאינן placeholder בחלונית המשנית של חלון המשימה מוערמות מעל הפעילויות בחלונית הראשית.
פעילויות של placeholder מוצגות רק כשיש מספיק רוחב מסך לפיצול. במסכים קטנים יותר, ה-placeholder נסגר אוטומטית. כשאזור התצוגה גדל מספיק, הפלייסהולדר נוצר מחדש. (פרטים נוספים זמינים בקטע Placeholders).
אפשר להציג פעילויות זו על גבי זו כי WindowManager מסדר את הפעילויות לפי ציר Z בחלונית המשנית מעל הפעילויות בחלונית הראשית.
פעילויות מרובות בחלונית המשנית
פעילות ב' מתחילה את פעילות ג' במקום בלי דגלים נוספים של כוונות:

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

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

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

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

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

כיוון קבוע לאורך
הגדרת המניפסט android:screenOrientation מאפשרת לאפליקציות להגביל את הפעילויות לכיוון לאורך או לרוחב. כדי לשפר את חוויית המשתמש במכשירים עם מסך גדול, כמו טאבלטים ומכשירים מתקפלים, יצרני מכשירים (OEM) יכולים להתעלם מבקשות של כיוון המסך ולהוסיף לאפליקציה מסגרת שחורה בצדדים (letterbox) בכיוון לאורך במסכים לרוחב או בכיוון לרוחב במסכים לאורך.
באופן דומה, כשמוטמעת פעילות, יצרני ציוד מקורי יכולים להתאים אישית מכשירים כדי להציג פעילויות בפורמט letterbox בפריסה לרוחב במסכים גדולים (רוחב של 600dp ומעלה). כשפעילות קבועה לאורך מופעלת ופותחת פעילות שנייה, המכשיר יכול להציג את שתי הפעילויות זו לצד זו בתצוגה עם שני חלונות.
תמיד צריך להוסיף את המאפיין android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED לקובץ המניפסט של האפליקציה כדי ליידע את המכשירים שהאפליקציה תומכת בהטמעת פעילויות (ראו את הקטע הגדרת פיצול). מכשירים בהתאמה אישית של OEM יכולים לקבוע אם להוסיף מסגרת שחורה לפעילויות במצב אנכי קבוע.
הגדרת פיצול
כללי פיצול מגדירים פיצולים של פעילויות. אפשר להגדיר כללי פיצול בקובץ תצורת XML או באמצעות קריאות ל-API של Jetpack WindowManager.
בכל מקרה, האפליקציה צריכה לגשת לספריית WindowManager ולעדכן את המערכת שהאפליקציה הטמיעה הטמעה של פעילות.
מבצעים את הפעולות הבאות:
מוסיפים את התלות בספריית WindowManager העדכנית לקובץ
build.gradleברמת המודול של האפליקציה, לדוגמה:implementation 'androidx.window:window:1.1.0-beta02'ספריית WindowManager מספקת את כל הרכיבים שנדרשים להטמעת פעילות.
מודיעים למערכת שהאפליקציה הטמיעה פעילות.
מוסיפים את המאפיין
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, מבצעים את השלבים הבאים:
יוצרים קובץ משאבי 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>יוצרים פונקציית אתחול.
רכיב WindowManager
RuleControllerמנתח את קובץ התצורה בפורמט XML ומאפשר למערכת להשתמש בכללים. ספריית Startup של JetpackInitializerמאפשרת לקובץ ה-XML להיות זמין ל-RuleControllerבזמן הפעלת האפליקציה, כך שהכללים יהיו בתוקף כשמתחילות פעילויות.כדי ליצור מאתחל:
מוסיפים את התלות של ספריית Jetpack Startup העדכנית לקובץ
build.gradleברמת המודול, לדוגמה:implementation 'androidx.startup:startup-runtime:1.1.1'יוצרים מחלקה שמטמיעה את הממשק
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(); } }
יוצרים ספק תוכן להגדרות הכללים.
מוסיפים את
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.
כדי ליצור פיצול פעילות באופן פרוגרמטי:
יוצרים כלל פיצול:
יוצרים
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 );
מוסיפים את המסנן לקבוצת מסננים:
```Kotlin
val filterSet = setOf(splitPairFilter)
Java
Set<SplitPairFilter> filterSet = new HashSet<>(); filterSet.add(splitPairFilter);
יוצרים מאפייני פריסה לפיצול:
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(): מציין את הפריסה של קונטיינרים של פעילות ביחס זה לזה, כאשר הקונטיינר הראשי מופיע ראשון.
-
איך יוצרים
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מציין שפעילויות חדשות מוערמות על פעילויות שכבר נמצאות במאגר המשני.
-
מקבלים את מופע הסינגלטון של WindowManager
RuleControllerומוסיפים את הכלל:Kotlin
val ruleController = RuleController.getInstance(this) ruleController.addRule(splitPairRule)
Java
RuleController ruleController = RuleController.getInstance(this); ruleController.addRule(splitPairRule);
יוצרים placeholder לקונטיינר המשני כשתוכן לא זמין:
יוצרים
ActivityFilterשמזהה את הפעילות שאיתה משתף ה-placeholder את החלון המפוצל של המשימה:Kotlin
val placeholderActivityFilter = ActivityFilter( ComponentName(this, ListActivity::class.java), null )
Java
ActivityFilter placeholderActivityFilter = new ActivityFilter( new ComponentName(this, ListActivity.class), null );
מוסיפים את המסנן לקבוצת מסננים:
Kotlin
val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
Java
Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>(); placeholderActivityFilterSet.add(placeholderActivityFilter);
יצירת
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 הופיע לראשונה בפיצול עם רוחב מינימלי מספיק.
-
מוסיפים את הכלל ל-WindowManager
RuleController:Kotlin
ruleController.addRule(splitPlaceholderRule)
Java
ruleController.addRule(splitPlaceholderRule);
ציון פעילויות שאסור לכלול בפיצול:
יוצרים תג
ActivityFilterשמזהה פעילות שתמיד צריכה לתפוס את כל האזור של תצוגת המשימות:Kotlin
val expandedActivityFilter = ActivityFilter( ComponentName(this, ExpandedActivity::class.java), null )
Java
ActivityFilter expandedActivityFilter = new ActivityFilter( new ComponentName(this, ExpandedActivity.class), null );
מוסיפים את המסנן לקבוצת מסננים:
Kotlin
val expandedActivityFilterSet = setOf(expandedActivityFilter)
Java
Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>(); expandedActivityFilterSet.add(expandedActivityFilter);
יצירת
ActivityRule:Kotlin
val activityRule = ActivityRule.Builder(expandedActivityFilterSet) .setAlwaysExpand(true) .build()
Java
ActivityRule activityRule = new ActivityRule.Builder( expandedActivityFilterSet ).setAlwaysExpand(true) .build();
ActivityRule.Builderיוצר ומגדיר את הכלל:-
expandedActivityFilterSet: מכיל מסנני פעילות שקובעים מתי להחיל את הכלל על ידי זיהוי פעילויות שרוצים להחריג מהפיצולים. -
setAlwaysExpand(): מציין אם הפעילות צריכה למלא את כל חלון המשימה.
-
מוסיפים את הכלל ל-WindowManager
RuleController:Kotlin
ruleController.addRule(activityRule)
Java
ruleController.addRule(activityRule);
הטמעה באפליקציות שונות
ב-Android 13 (רמת API 33) ומעלה, אפליקציות יכולות להטמיע פעילויות מאפליקציות אחרות. הטמעת פעילות בין אפליקציות, או בין מזהי UID, מאפשרת שילוב חזותי של פעילויות מכמה אפליקציות ל-Android. המערכת מציגה פעילות של אפליקציית המארח ופעילות מוטמעת מאפליקציה אחרת במסך זו לצד זו או זו מעל זו, בדיוק כמו בהטמעה של פעילות באפליקציה יחידה.
לדוגמה, אפליקציית ההגדרות יכולה להטמיע את פעילות בחירת הטפט מאפליקציית WallpaperPicker:
מודל אמון
תהליכי המארח שמטמיעים פעילויות מאפליקציות אחרות יכולים להגדיר מחדש את ההצגה של הפעילויות המוטמעות, כולל גודל, מיקום, חיתוך ושקיפות. מארחים זדוניים יכולים להשתמש ביכולת הזו כדי להטעות משתמשים וליצור הונאת קליקים או מתקפות אחרות של שינוי ממשק המשתמש.
כדי למנוע שימוש לרעה בהטמעה של פעילות בין אפליקציות, מערכת 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.
אם אפליקציית מארח מנסה להטמיע פעילות שלא הופעלה בה האפשרות להטמעה באפליקציות שונות, הפעילות תתפוס את כל גבולות המשימה. לכן, אפליקציות מארחות צריכות לדעת אם פעילויות היעד מאפשרות הטמעה בין אפליקציות.
אם פעילות מוטמעת מתחילה פעילות חדשה באותה משימה והפעילות החדשה לא הופעלה להטמעה חוצת-אפליקציות, הפעילות תופסת את כל גבולות המשימה במקום להופיע כשכבת-על בפעילות במאגר המוטמע.
אפליקציית מארח יכולה להטמיע פעילויות משלה ללא הגבלה, כל עוד הפעילויות מופעלות באותה משימה.
דוגמאות לפיצול
פיצול מהחלון המלא
אין צורך בשינוי מבנה הקוד. אפשר להגדיר את הפיצול באופן סטטי או בזמן ריצה, ואז להפעיל את Context#startActivity() ללא פרמטרים נוספים.
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
פיצול כברירת מחדל
כשדף הנחיתה של אפליקציה מתוכנן להתפצל לשני קונטיינרים במסכים גדולים, חוויית המשתמש תהיה הכי טובה אם שתי הפעילויות ייווצרו ויוצגו בו-זמנית. עם זאת, יכול להיות שהתוכן לא יהיה זמין במאגר המשני של המסך המפוצל עד שהמשתמש יצור אינטראקציה עם הפעילות במאגר הראשי (לדוגמה, המשתמש בוחר פריט מתפריט הניווט). פעילות placeholder יכולה למלא את החלל עד שניתן יהיה להציג תוכן במאגר המשני של המסך המפוצל (ראו את הקטע Placeholders).
כדי ליצור פיצול עם placeholder, יוצרים placeholder ומשייכים אותו לפעילות הראשית:
<SplitPlaceholderRule
window:placeholderActivityName=".PlaceholderActivity">
<ActivityFilter
window:activityName=".MainActivity"/>
</SplitPlaceholderRule>
פיצול של קישור עומק
כשמתקבלת כוונה באפליקציה, פעילות היעד יכולה להופיע כחלק המשני של פיצול הפעילות. לדוגמה, בקשה להצגת מסך פרטים עם מידע על פריט מרשימה. בתצוגות קטנות, הפרטים מוצגים בחלון המשימות המלא. במכשירים גדולים יותר, הפרטים מוצגים לצד הרשימה.
בקשת ההפעלה צריכה להיות מנותבת לפעילות הראשית, והפעילות של פרטי היעד צריכה להיות מופעלת במסך מפוצל. המערכת בוחרת אוטומטית את הפריסה הנכונה – זו לצד זו או זו מעל זו – על סמך הרוחב הזמין של המסך.
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>
אפשר לעיין בקטע מאפייני הגדרה.
כמה פעילויות במאגרי תגים מפוצלים
הצגת כמה פעילויות זו לצד זו במאגר מפוצל מאפשרת למשתמשים לגשת לתוכן מפורט. לדוגמה, אם התצוגה מחולקת לרשימה ולפרטים, יכול להיות שהמשתמש יצטרך להיכנס לקטע של פרטי משנה, אבל הפעילות העיקרית תישאר במקום:
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)); } }
פעילות המשנה ממוקמת מעל פעילות הפרטים ומסתירה אותה:

לאחר מכן המשתמש יכול לחזור לרמת הפירוט הקודמת על ידי חזרה אחורה במחסנית:
התנהגות ברירת המחדל היא שפעילויות מוצגות אחת מעל השנייה כשמפעילים אותן מתוך פעילות באותו מאגר משני. פעילויות שהופעלו מהמאגר הראשי בתוך פיצול פעיל מסתיימות גם במאגר המשני בראש רשימת הפעילויות.
פעילויות במשימה חדשה
כשפעילויות בחלון מפוצל של משימה מתחילות פעילויות במשימה חדשה, המשימה החדשה נפרדת מהמשימה שכוללת את הפיצול ומוצגת בחלון מלא. במסך 'פריטים אחרונים' מוצגות שתי משימות: המשימה בחלון המפוצל והמשימה החדשה.
החלפת פעילות
אפשר להחליף פעילויות במערך המשני של הקונטיינרים. לדוגמה, כשמשתמשים בפעילות הראשית לניווט ברמה העליונה והפעילות המשנית היא יעד נבחר. כל בחירה מתוך אמצעי הניווט ברמה העליונה צריכה להתחיל פעילות חדשה במאגר המשני ולהסיר את הפעילות או הפעילויות שהיו שם קודם.
אם האפליקציה לא מסיימת את הפעילות במאגר המשני כשמשנים את הבחירה בניווט, יכול להיות שהניווט אחורה יהיה מבלבל כשהמסך המפוצל מכווץ (כשהמכשיר מקופל). לדוגמה, אם יש תפריט בחלונית הראשית ומסכים א' ו-ב' מוערמים בחלונית המשנית, כשמשתמש מקפל את הטלפון, מסך ב' נמצא מעל מסך א' ומסך א' נמצא מעל התפריט. כשהמשתמש חוזר מדף 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))); } }
אפשר גם להשתמש באותה פעילות משנית, ומפעילות ראשית (תפריט) לשלוח כוונות חדשות שמובילות לאותו מופע, אבל מפעילות עדכון של מצב או של ממשק משתמש במאגר המשני.
פיצולים מרובים
אפליקציות יכולות לספק ניווט עמוק בכמה רמות על ידי הפעלת פעילויות נוספות בצד.
כשפעילות במאגר משני מפעילה פעילות חדשה בצד, נוצר פיצול חדש מעל הפיצול הקיים.
מחסנית החזרה מכילה את כל הפעילויות שנפתחו קודם, כך שהמשתמשים יכולים לנווט לפיצול A/B אחרי סיום הפעילות C.

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


איך מסיימים פעילויות ביחד
הפעילויות במאגר הראשי מסתיימות אוטומטית כשכל הפעילויות במאגר המשני מסתיימות:
<SplitPairRule
window:finishPrimaryWithSecondary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


השלמת הפעילויות במאגר המשני באופן אוטומטי כשכל הפעילויות במאגר הראשי מסתיימות:
<SplitPairRule
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


סיום הפעילויות יחד כשכל הפעילויות במיכל הראשי או המשני מסתיימות:
<SplitPairRule
window:finishPrimaryWithSecondary="always"
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>


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

ההגדרה של הפיצול מוגדרת על ידי ההגדרה של הפעילויות א' וב':
<SplitPairRule>
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>
אם מסיימים את הפעילות העליונה, המסך יישאר מפוצל.

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

כללים נוספים לסיום פעילויות משותפות, כמו סיום הפעילות המשנית עם הפעילות הראשית, מופעלים גם הם:
<SplitPairRule
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>

וכשהפיצול מוגדר כך שהפעולות הראשיות והמשניות יסתיימו יחד:
<SplitPairRule
window:finishPrimaryWithSecondary="always"
window:finishSecondaryWithPrimary="always">
<SplitPairFilter
window:primaryActivityName=".A"
window:secondaryActivityName=".B"/>
</SplitPairRule>



שינוי מאפייני הפיצול בזמן הריצה
אי אפשר לשנות את המאפיינים של פיצול פעיל וגלוי. שינוי כללי הפיצול משפיע על הפעלות נוספות של פעילויות ועל מאגרי תגים חדשים, אבל לא על פיצולים קיימים ופעילים.
כדי לשנות את המאפיינים של פעילויות מפוצלות, צריך לסיים את הפעילות או הפעילויות הצדדיות בפיצול ולהפעיל שוב את הפעילות הצדדית עם הגדרה חדשה.
מאפייני פיצול דינמי
Android 15 (רמת API 35) ומעלה נתמכות על ידי Jetpack WindowManager 1.4 ומעלה, ומציעות תכונות דינמיות שמאפשרות להגדיר את הפיצולים של הטמעת הפעילות, כולל:
- הרחבת החלונית: מחיצה אינטראקטיבית שאפשר לגרור מאפשרת למשתמשים לשנות את הגודל של החלוניות בתצוגה מפוצלת.
- הצמדת ערימת פעילויות: המשתמשים יכולים להצמיד את התוכן במאגר אחד ולבודד את הניווט במאגר מהניווט במאגר השני.
- החלשה של מסך מלא בתיבת דו-שיח: כשמוצגת תיבת דו-שיח, האפליקציות יכולות לציין אם להחליש את כל חלון המשימה או רק את המאגר שפתח את תיבת הדו-שיח.
הרחבת החלונית
הרחבת החלונית מאפשרת למשתמשים לשנות את כמות שטח המסך שמוקצה לשתי הפעילויות בפריסה של שתי חלוניות.
כדי להתאים אישית את המראה של הקו שמחלק את החלון ולהגדיר את הטווח שבו אפשר לגרור את הקו:
יצירת מופע של
DividerAttributesהתאמה אישית של מאפייני המחיצה:
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();
הצמדה של רשימת פעילויות
הצמדת ערימת פעילויות מאפשרת למשתמשים להצמיד אחד מהחלונות המפוצלים, כך שהפעילות נשארת כמו שהיא בזמן שהמשתמשים מנווטים בחלון השני. הצמדה של ערימת פעילויות משפרת את חוויית הריבוי משימות.
כדי להפעיל הצמדה של ערימת הפעילות באפליקציה:
מוסיפים לחצן לקובץ הפריסה של הפעילות שרוצים להצמיד. לדוגמה, פעילות הפרטים של פריסת רשימה-פרטים:
<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>ב-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 כדי לאפשר למערכת להחיל הטמעת פעילות שהוגדרה על ידי המערכת על האפליקציה.
מגבלות, הגבלות וסייגים
- רק אפליקציית המארח של המשימה, שמזוהה כבעלים של פעילות הבסיס במשימה, יכולה לארגן ולהטמיע פעילויות אחרות במשימה. אם פעילויות שתומכות בהטמעה ובפיצול מופעלות במשימה ששייכת לאפליקציה אחרת, ההטמעה והפיצול לא יפעלו עבור הפעילויות האלה.
- אפשר לארגן פעילויות רק בתוך משימה אחת. כשמפעילים פעילות במשימה חדשה, היא תמיד מופיעה בחלון חדש מורחב מחוץ לכל חלוקה קיימת.
- אפשר לארגן ולפצל רק פעילויות שנמצאות באותו תהליך. הקריאה החוזרת
SplitInfoמדווחת רק על פעילויות ששייכות לאותו תהליך, כי אין דרך לדעת על פעילויות בתהליכים שונים. - כל כלל של פעילות יחידה או זוג פעילויות חל רק על הפעלות של פעילויות שמתרחשות אחרי שהכלל נרשם. נכון לעכשיו, אין אפשרות לעדכן פיצולים קיימים או את המאפיינים החזותיים שלהם.
- ההגדרה של המסנן לפי זוגות מפוצלים צריכה להיות זהה לכוונות שמשמשות להפעלת פעילויות באופן מלא. ההתאמה מתרחשת ברגע שמתחילה פעילות חדשה מתהליך האפליקציה, ולכן יכול להיות שהיא לא תדע על שמות רכיבים שנפתרים מאוחר יותר בתהליך המערכת כשמשתמשים ב-intent משתמע. אם שם הרכיב לא ידוע בזמן ההפעלה, אפשר להשתמש בתו כללי במקום זאת (*/*) ולסנן על סמך פעולת הכוונה.
- בשלב הזה, אין אפשרות להעביר פעילויות בין מאגרי תגים או אל תוך פיצולים ומחוצה להם אחרי שהם נוצרו. הפיצולים נוצרים רק על ידי ספריית WindowManager כשמופעלות פעילויות חדשות עם כללים תואמים, והם נהרסים כשהפעילות האחרונה במאגר הפיצולים מסתיימת.
- אפשר להפעיל מחדש פעילויות כשמבצעים שינויים בהגדרות. לכן, כשיוצרים או מסירים פיצול ומשנים את גבולות הפעילות, הפעילות יכולה לעבור השמדה מלאה של המופע הקודם ויצירה של מופע חדש. לכן, מפתחי אפליקציות צריכים להיזהר כשמבצעים פעולות כמו הפעלת פעילויות חדשות מתוך קריאות חוזרות (callbacks) של מחזור החיים.
- המכשירים צריכים לכלול את ממשק התוספים של החלונות כדי לתמוך בהטמעת פעילות. כמעט כל המכשירים עם מסך גדול שפועלת בהם מערכת Android 12L (רמת API 32) ומעלה כוללים את הממשק. עם זאת, חלק מהמכשירים עם מסך גדול שלא יכולים להריץ כמה פעילויות לא כוללים את ממשק התוספים של החלון. אם מכשיר עם מסך גדול לא תומך במצב ריבוי חלונות, יכול להיות שהוא לא תומך בהטמעת פעילות.
מקורות מידע נוספים
- Codelabs:
- תוכנית לימודים – הטמעת פעילויות
- אפליקציה לדוגמה – activity-embedding