הטמעת פעילויות מאפשרת לכם לשפר את הביצועים של אפליקציות במכשירים עם מסך גדול על ידי חלוקת חלון המשימות של האפליקציה בין שתי פעילויות או בין שני מופעים של אותה פעילות.
אם האפליקציה שלכם מורכבת מכמה פעילויות, הטמעת הפעילויות מאפשרת לכם לספק חוויית משתמש משופרת בטאבלטים, במכשירים מתקפלים ובמכשירי ChromeOS.
אין צורך לבצע רפאקציה של הקוד כדי להטמיע פעילויות. אתם קובעים איך האפליקציה תציג את הפעילויות שלה – זה לצד זה או מוערמות – על ידי יצירת קובץ תצורה מסוג XML או על ידי שליחת קריאות ל-API של Jetpack WindowManager.
התמיכה במסכים קטנים מתבצעת באופן אוטומטי. כשהאפליקציה מותקנת במכשיר עם מסך קטן, הפעילויות נערמות אחת על גבי השנייה. במסכים גדולים, הפעילויות מוצגות זו לצד זו. המערכת קובעת את הצגת הנתונים על סמך ההגדרה שיצרתם – לא נדרשת לוגיקה של הסתעפות.
הטמעת הפעילות מתאימה לשינויים בכיוון המכשיר ופועלת בצורה חלקה במכשירים מתקפלים, כך שאפשר לערום פעילויות ולפרוס אותן כשהמכשיר מתקפל ונפתח.
אפשר להטמיע פעילויות ברוב המכשירים עם מסך גדול שפועלים עם Android 12L (רמת API 32) ואילך.
פיצול חלון המשימה
הטמעת הפעילות מפצלת את חלון המשימות של האפליקציה לשני קונטיינרים: ראשי ומשני. הקונטיינרים מכילים פעילויות שהופעלו מהפעילות הראשית או מפעילויות אחרות שכבר נמצאות בקונטיינרים.
הפעילויות נערמות בקונטיינר המשני כשהן מופעלות, והקונטיינר המשני נערם מעל הקונטיינר הראשי במסכים קטנים, כך שהערמה של פעילויות וניווט חזרה תואמים לסדר הפעילויות שכבר מובנות באפליקציה.
הטמעת פעילויות מאפשרת לכם להציג פעילויות במגוון דרכים. האפליקציה יכולה לפצל את חלון המשימות על ידי הפעלה של שתי פעילויות בו-זמנית:
לחלופין, אפשר ליצור חלוקה של פעילות שממלאת את כל חלון המשימות על ידי הפעלת פעילות חדשה לצד:
פעילויות שכבר מחולקות ומשתתפות בחלון משימות משותף יכולות להפעיל פעילויות אחרות בדרכים הבאות:
בצד, מעל פעילות אחרת:
לצד, ולהזיז את החלוקה לצדדים כדי להסתיר את הפעילות הראשית הקודמת:
להפעיל פעילות במקום העליון, כלומר באותה רשימת פעילויות (activity stack):
הפעלת חלון מלא של פעילות באותה משימה:
ניווט חזרה
לסוגים שונים של אפליקציות יכולים להיות כללי ניווט אחורה שונים במצב של חלון משימות מפוצל, בהתאם ליחסי התלות בין הפעילויות או לאופן שבו המשתמשים מפעילים את אירוע החזרה אחורה. לדוגמה:
- פעילות משותפת: אם שתי פעילויות קשורות זו לזו ואחת לא אמורה להופיע בלי השנייה, אפשר להגדיר את הניווט לאחור כך שיסיים את שתיהן.
- פעילות עצמאית: אם הפעילויות עצמאיות לחלוטין, ניווט חזרה בפעילות לא משפיע על המצב של פעילות אחרת בחלון המשימות.
האירוע back נשלח לפעילות האחרונה שבה המשתמש התמקד כשמשתמשים בניווט באמצעות לחצנים.
לניווט מבוסס-תנועות:
Android 14 (רמת API 34) וגרסאות ישנות יותר – אירוע החזרה לאחור נשלח לפעילות שבה התרחשה התנועה. כשמשתמשים מחליקים מצד ימין של המסך, אירוע החזרה לאחור נשלח לפעילות בחלונית הימנית של החלון המפוצל. כשמשתמשים מחליקים מהצד הימני של המסך, אירוע החזרה נשלח לפעילות שבחלונית השמאלית.
Android מגרסה 15 (רמת API 35) ואילך
כשיש כמה פעילויות מאותה אפליקציה, התנועה מסתיימת בפעילות העליונה ללא קשר לכיוון ההחלקה, וכך מספקת חוויה אחידה יותר.
בתרחישים שכוללים שתי פעילויות מאפליקציות שונות (שכבת-על), אירוע החזרה לאחור מופנה לפעילות האחרונה שבה התמקדו, בהתאם להתנהגות של ניווט באמצעות לחצנים.
פריסת חלוניות מרובות
Jetpack WindowManager מאפשר ליצור פעילות שמוטמעת בתצוגה עם כמה חלונות במכשירים עם מסך גדול עם Android 12L (API ברמה 32) ואילך, ובמכשירים מסוימים עם גרסאות קודמות של הפלטפורמה. אפליקציות קיימות שמבוססות על כמה פעילויות במקום על קטעים או על פריסות שמבוססות על תצוגות, כמו SlidingPaneLayout
, יכולות לספק חוויית משתמש משופרת במסך גדול בלי לבצע רפאקציה של קוד המקור.
דוגמה נפוצה היא פיצול של רשימה לפרטים. כדי להבטיח איכות גבוהה של הצגה, המערכת מפעילה את הפעילות של הרשימה, ואז האפליקציה מפעילה מיד את הפעילות של הפרטים. מערכת המעבר ממתינה עד ששתי הפעילויות יוצגו, ואז מציגה אותן יחד. מבחינת המשתמש, שתי הפעילויות מופעלות כפעילות אחת.
פיצול מאפיינים
אתם יכולים לציין את היחס בין החלונות של המשימות לבין הקונטיינרים המפוצלים, ואת אופן הפריסה של הקונטיינרים ביחס זה לזה.
לכללים שמוגדרים בקובץ תצורה מסוג XML, מגדירים את המאפיינים הבאים:
splitRatio
: הגדרת הפרופורציות של הקונטיינר. הערך הוא מספר נקודה צפה (floating-point) בטווח הפתוח (0.0, 1.0).splitLayoutDirection
: מציין את אופן הפריסה של הקונטיינרים המפוצלים ביחס זה לזה. הערכים כוללים:ltr
: משמאל לימיןrtl
: מימין לשמאלlocale
: הערךltr
אוrtl
נקבע לפי הגדרת השפה והאזור
דוגמאות מפורטות מופיעות בקטע הגדרת XML.
לכללים שנוצרו באמצעות ממשקי ה-API של WindowManager, יוצרים אובייקט SplitAttributes
עם SplitAttributes.Builder
ומפעילים את שיטות ה-builder הבאות:
setSplitType()
: הגדרת הפרופורציות של הקונטיינרים המפוצלים. אפשר לעיין במאמרSplitAttributes.SplitType
כדי לראות אילו ארגומנטים חוקיים, כולל השיטהSplitAttributes.SplitType.ratio()
.setLayoutDirection()
: הגדרת הפריסה של הקונטיינרים. הערכים האפשריים מפורטים במאמרSplitAttributes.LayoutDirection
.
דוגמאות מפורטות בקטע WindowManager API.
placeholders
פעילויות placeholder הן פעילויות משניות ריקות שממלאות אזור של חלוקת פעילות. בסופו של דבר, הן אמורות להחליף בפעילות אחרת שמכילה תוכן. לדוגמה, פעילות placeholder יכולה לתפוס את הצד המשני של פעילות שמחולקת בתצוגת רשימה עם פרטים, עד שבוחרים פריט מהרשימה. בשלב הזה, פעילות שמכילה את פרטי הפריט שנבחר ברשימה מחליפה את ה-placeholder.
כברירת מחדל, המערכת מציגה placeholders רק כשיש מספיק מקום לחלוקת פעילות. ה-placeholders מסתיימים באופן אוטומטי כשגודל התצוגה משתנה לרוחב או לגובה קטנים מדי כדי להציג חלוקה. אם יש מספיק מקום, המערכת מפעילה מחדש את ה-placeholder במצב שמוגדר מחדש.
עם זאת, המאפיין stickyPlaceholder
של שיטת SplitPlaceholderRule
או setSticky()
של SplitPlaceholder.Builder
יכול לבטל את התנהגות ברירת המחדל. כשהערך של המאפיין או השיטה הוא true
, המערכת מציגה את placeholder כפעילות העליונה בחלון המשימות כשהגודל של המסך משתנה מחלון עם שתי חלוניות לחלון עם חלונית אחת (לדוגמה, ראו הגדרת פיצול).
שינויים בגודל החלון
כששינויים בהגדרות המכשיר מצמצמים את רוחב חלון המשימות כך שהוא לא גדול מספיק לפריסת חלונות מרובים (לדוגמה, כשמכשיר מתקפל עם מסך גדול מתקפל מטאבלט לגודל טלפון או כשמשנים את הגודל של חלון האפליקציה במצב חלונות מרובים), הפעילויות שאינן placeholders בחלונית המשנית של חלון המשימות נערמות מעל הפעילויות בחלונית הראשית.
פעילויות של placeholders מוצגות רק כשיש מספיק רוחב תצוגה לחלוקה. במסכים קטנים יותר, Placeholder נסגר באופן אוטומטי. כשאזור התצוגה שוב יהיה גדול מספיק, ה-placeholder ייווצר מחדש. (מידע נוסף זמין בקטע Placeholders).
אפשר להצמיד פעילויות כי WindowManager מסדר את הפעילויות בחלונית המשנית לפי סדר 'z' מעל הפעילויות בחלונית הראשית.
כמה פעילויות בחלונית המשנית
פעילות ב' מפעילה את פעילות ג' במקום ללא דגלים נוספים של כוונת השימוש:
וכתוצאה מכך, סדר הפעילויות באותה משימה יהיה:
לכן, בחלון משימות קטן יותר, האפליקציה מצטמצמת לפעילות אחת עם C בחלק העליון של הסטאק:
ניווט חזרה בחלון הקטן יותר מאפשר לנווט בין הפעילויות שממוזערות זו על גבי זו.
אם תגדירו מחדש את חלון המשימות לגודל גדול יותר שיכול להכיל כמה חלוניות, הפעילויות יוצגו שוב זו לצד זו.
פלחים מוערמים
פעילות ב' מתחילה את פעילות ג' בצד ומזיזה את החלוקה לצד:
התוצאה היא סדר z הבא של הפעילויות באותה משימה:
בחלון משימות קטן יותר, האפליקציה מצטמצמת לפעילות אחת עם C בחלק העליון:
כיוון לאורך קבוע
הגדרת המניפסט android:screenOrientation מאפשרת לאפליקציות להגביל את הפעילויות לכיוון לאורך או לרוחב. כדי לשפר את חוויית השימוש במכשירים עם מסך גדול, כמו טאבלטים ומכשירים מתקפלים, יצרני המכשירים (OEM) יכולים להתעלם מהבקשות לגבי כיוון המסך ולהציג את האפליקציה בפורמט letterbox בכיוון לאורך במסכים בכיוון לרוחב, או בכיוון לרוחב במסכים בכיוון לאורך.
באופן דומה, כשהתכונה 'הטמעת פעילות' מופעלת, יצרני ציוד מקורי יכולים להתאים אישית מכשירים כך שיציגו פעילויות בפורמט letterbox לאורך בפריסה לרוחב במסכים גדולים (רוחב של 600dp ומעלה). כשפעילות בפורמט ניצב קבוע מפעילה פעילות שנייה, המכשיר יכול להציג את שתי הפעילויות זו לצד זו במסך עם שני חלונות.
תמיד מוסיפים את המאפיין android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED
לקובץ המניפסט של האפליקציה כדי להודיע למכשירים שהאפליקציה תומכת בהטמעת פעילויות (ראו הקטע הגדרת חלוקה). לאחר מכן, במכשירים בהתאמה אישית של יצרן ציוד מקורי אפשר יהיה לקבוע אם להציג בפורמט letterbox פעילויות בפורמט לאורך קבוע.
הגדרת פיצול
כללי פיצול מגדירים את הפיצולים של הפעילות. מגדירים את כללי הפיצול בקובץ תצורה של XML או באמצעות קריאות ל-API של WindowManager ב-Jetpack.
בכל מקרה, האפליקציה צריכה לגשת לספרייה 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>
במהדורה 1.1.0-alpha06 ואילך של WindowManager, חלוקות של הטמעת פעילויות מושבתות, אלא אם המאפיין נוסף למניפסט והוגדר כ-true.
בנוסף, יצרני המכשירים משתמשים בהגדרה הזו כדי להפעיל יכולות בהתאמה אישית לאפליקציות שתומכות בהטמעת פעילות. לדוגמה, במכשירים עם מסך אופקי, פעילות בפורמט לאורך בלבד מוצגת בפורמט letterbox כדי להתאים את הפעילות למעבר לפריסה של שני חלונות כשפעילות שנייה מתחילה (ראו כיוון קבוע לרוחב).
הגדרות 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 הופך את כללי הפיצול לזמינים ל-
RuleController
על ידי העברת המזהה של קובץ התצורה של ה-XML (main_split_config.xml
) לשיטהRuleController.parseRules()
.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
final 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()
. ערך ברירת המחדל לפורמט 'לאורך' הוא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
שמזהה את הפעילות שאיתה המקום הזמין לשמירת תוכן משתף את החלוקה של חלון המשימות: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(context, 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()
. ערך ברירת המחדל לפריסה לרוחב הואALWAYS_ALLOW
.setFinishPrimaryWithPlaceholder()
: מגדיר את ההשפעה של סיום הפעילות של placeholder על הפעילויות במאגר הראשי. הערך ALWAYS מציין שהמערכת תמיד צריכה לסיים את הפעילויות במאגר הראשי כשה-placeholder מסתיים (ראו סיום פעילויות).setSticky()
: קובע אם פעילות 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:
מודל אמון
תהליכי המארח שמטמיעים פעילויות מאפליקציות אחרות יכולים להגדיר מחדש את הצגת הפעילויות המוטמעות, כולל הגודל, המיקום, החיתוך והשקיפות. מארחים זדוניים יכולים להשתמש ביכולת הזו כדי להטעות משתמשים וליצור הונאת clickjacking או התקפות אחרות שמתמקדות בממשק המשתמש.
כדי למנוע שימוש לרעה בהטמעת פעילויות בין אפליקציות, נדרש מפתחי אפליקציות ב-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 על ידי הרצת המשימה signingReport
ב-Gradle. תקציר האישור הוא טביעת האצבע מסוג 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(this).isActivityEmbedded(activity); }
הגבלת גודל מינימלי
מערכת Android מחילה על פעילויות מוטמעות את הגובה והרוחב המינימליים שצוינו באלמנט <layout>
במניפסט של האפליקציה. אם לא צוינו גובה ורוחב מינימלי באפליקציה, יילקחו ערכי ברירת המחדל של המערכת (sw220dp
).
אם המארח ינסה לשנות את הגודל של הקונטיינר המוטמע לגודל קטן מהגודל המינימלי, הקונטיינר המוטמע יתרחב כדי לתפוס את כל גבולות המשימה.
<activity-alias>
כדי שאפשר יהיה להטמיע פעילות מהימנה או לא מהימנה עם האלמנט <activity-alias>
, צריך להחיל את android:knownActivityEmbeddingCerts
או את android:allowUntrustedActivityEmbedding
על פעילות היעד ולא על הכינוי. המדיניות שמאמתת את האבטחה בשרת המערכת מבוססת על הדגלים שהוגדרו ביעד, ולא על הכינוי החלופי.
אפליקציית המארח
אפליקציות מארחות מטמיעות הטמעת פעילות בכמה אפליקציות באותו אופן שבו הן מטמיעות הטמעת פעילות באפליקציה אחת. אובייקטים מסוג SplitPairRule
ו-SplitPairFilter
או אובייקטים מסוג ActivityRule
ו-ActivityFilter
מציינים פעילויות מוטמעות ופיצולים של חלון המשימות. כללי הפיצול מוגדרים סטטית ב-XML או בזמן הריצה באמצעות קריאות ל-Jetpack WindowManager API.
אם אפליקציית מארח מנסה להטמיע פעילות שלא הביעה הסכמה להטמעה בין אפליקציות, הפעילות תופסת את כל גבולות המשימה. לכן, אפליקציות המארח צריכות לדעת אם פעילויות היעד מאפשרות הטמעה בכמה אפליקציות.
אם פעילות מוטמעת מתחילה פעילות חדשה באותה משימה, והפעילות החדשה לא הביעה הסכמה להטמעה בין אפליקציות, הפעילות תופסת את כל גבולות המשימה במקום להופיע כשכבת-על לפעילות במאגר המוטמע.
אפליקציית מארח יכולה להטמיע פעילויות משלה ללא הגבלה, כל עוד הפעילויות מופעלות באותה משימה.
דוגמאות לפיצול
פיצול ממסך מלא
אין צורך לבצע רפאקציה. אפשר להגדיר את ההגדרות של הפיצול באופן סטטי או בזמן הריצה, ואז להפעיל את 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?) { . . . RuleController.getInstance(this) .addRule(SplitPairRule.Builder(filterSet).build()) startActivity(Intent(this, DetailActivity::class.java)) }
Java
@Override protected void onCreate(@Nullable Bundle 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 { . . . fun onOpenSubDetail() { startActivity(Intent(this, SubDetailActivity::class.java)) } }
Java
public class DetailActivity { . . . 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
class MenuActivity { . . . fun onMenuItemSelected(selectedMenuItem: Int) { startActivity(Intent(this, classForItem(selectedMenuItem))) } }
Java
public class MenuActivity { . . . 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 { . . . fun onOpenC() { startActivity(Intent(this, C::class.java)) } }
Java
public class B { . . . 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 { lifecycle.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) { . . . 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()
.
חלון עזר במסך מלא
פעילויות מסוימות מונעות ממשתמשים לקיים אינטראקציה עם האפליקציה עד לביצוע פעולה מסוימת. לדוגמה, פעילות במסך ההתחברות, מסך אישור המדיניות או הודעת שגיאה. רצוי למנוע הצגה של פעילויות מסוג חלון דו-שיח בחלוקה.
אפשר לאלץ פעילות למלא תמיד את חלון המשימות באמצעות הגדרת ההרחבה:
<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(context, 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(context, 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>
בשיטה
onCreate()
של הפעילות, מגדירים למעקב אחר לחיצות על הלחצן:Kotlin
pinButton = 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
.
איך מעבירים פעילות מחלון מפוצל לחלון מלא
יוצרים הגדרה חדשה שבה מוצג חלון מלא של הפעילות הצדדית, ואז מפעילים מחדש את הפעילות עם כוונה שמפנה לאותה מכונה.
בדיקת התמיכה בחלוקה בזמן הריצה
יש תמיכה בהטמעת פעילות ב-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. }
אם אין תמיכה בחלוקות, הפעילויות מופעלות מעל ל-activity stack (לפי מודל הטמעה ללא פעילות).
מניעת שינוי מברירת המחדל של המערכת
יצרני מכשירי Android (יצרני ציוד מקורי, או OEM) יכולים להטמיע הטמעת פעילות כפונקציה של מערכת המכשיר. המערכת מציינת כללי פיצול לאפליקציות עם כמה פעילויות, ומבטלת את התנהגות החלונות של האפליקציות. שינוי ברירת המחדל של המערכת מאלץ אפליקציות עם כמה פעילויות לעבור למצב הטמעת פעילות שהוגדר על ידי המערכת.
הטמעת הפעילות של המערכת יכולה לשפר את הצגת האפליקציה באמצעות פריסות עם כמה חלונות, כמו list-detail, בלי לבצע שינויים באפליקציה. עם זאת, הטמעת הפעילות של המערכת עשויה גם לגרום לפריסות שגויות של האפליקציה, לבאגים או להתנגשויות עם הטמעת הפעילות שהאפליקציה מטמיעה.
האפליקציה יכולה למנוע הטמעה של פעילות מערכת או לאפשר אותה על ידי הגדרת מאפיין בקובץ המניפסט של האפליקציה. לדוגמה:
<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>
שם הנכס מוגדר באובייקט WindowProperties
של WindowManager ב-Jetpack. מגדירים את הערך כ-false
אם האפליקציה מטמיעה הטמעת פעילות, או אם רוצים למנוע בדרך אחרת את המערכת משימוש בכללי הטמעת הפעילות שלה באפליקציה. מגדירים את הערך כ-true
כדי לאפשר למערכת להחיל על האפליקציה הטמעת פעילות שהוגדרה על ידי המערכת.
מגבלות, הגבלות וסייגים
- רק אפליקציית המארח של המשימה, שזוהתה בתור הבעלים של פעילות הבסיס במשימה, יכולה לארגן ולטמיע פעילויות אחרות במשימה. אם פעילויות שתומכות בהטמעה ובחלוקות פועלות במשימה ששייכת לאפליקציה אחרת, ההטמעה והחלוקות לא יפעלו בפעילויות האלה.
- אפשר לארגן פעילויות רק בתוך משימה אחת. הפעלת פעילות במשימה חדשה תמיד תפתח אותה בחלון חדש מורחב, מחוץ לכל פיצולים קיימים.
- אפשר לארגן ולחלק רק פעילויות באותו תהליך. פונקציית ה-callback
SplitInfo
מדווחת רק על פעילויות ששייכות לאותו תהליך, כי אין דרך לדעת על פעילויות בתהליכים שונים. - כל כלל פעילות יחיד או כלל פעילות משולש חל רק על השקות של פעילויות שמתרחשות אחרי שהכלל נרשם. בשלב זה אין אפשרות לעדכן חלוקות קיימות או את המאפיינים החזותיים שלהן.
- הגדרת המסנן של הצמד המפוצל חייבת להתאים לכוונות שבהן השתמשתם כשהפעילתם את הפעילויות באופן מלא. ההתאמה מתרחשת בנקודה שבה מתחילה פעילות חדשה מתהליך האפליקציה, כך שיכול להיות שהיא לא תדע על שמות הרכיבים שמתקבלים מאוחר יותר בתהליך המערכת כשמשתמשים בכוונות משתמשים מרומזות. אם שם הרכיב לא ידוע בזמן ההשקה, אפשר להשתמש בתו אסימון ("*/*") ולבצע סינון על סמך פעולת הכוונה.
- בשלב זה אין אפשרות להעביר פעילויות בין קונטיינרים או להעביר אותן לחלוקות או מהן אחרי שהן נוצרו. ספריית WindowManager יוצרת חלוקות רק כשמפעילים פעילויות חדשות עם כללים תואמים, והן נמחקות כשהפעילות האחרונה בקונטיינר המפוצל מסתיימת.
- אפשר להפעיל מחדש פעילויות כשהתצורה משתנה. לכן, כשיוצרים או מסירים פיצול וערכי הגבול של הפעילות משתנים, הפעילות יכולה לעבור תהליך של השמדה מוחלטת של המכונה הקודמת ויצירה של מכונה חדשה. כתוצאה מכך, מפתחי אפליקציות צריכים להיזהר בדברים כמו הפעלת פעילויות חדשות מקריאות חזרה (callbacks) במחזור החיים.
- כדי לתמוך בהטמעת פעילות, המכשירים חייבים לכלול את ממשק התוספים של החלון. ממשק המשתמש נכלל כמעט בכל המכשירים עם מסך גדול שפועלים עם Android 12L (רמת API 32) ואילך. עם זאת, במכשירים מסוימים עם מסך גדול שלא יכולים להריץ כמה פעילויות, ממשק התוספים של החלונות לא מופיע. אם מכשיר עם מסך גדול לא תומך במצב של חלונות מרובים, יכול להיות שהוא לא תומך בהטמעת פעילויות.
מקורות מידע נוספים
- Codelabs:
- תוכנית לימודים – הטמעת פעילות
- אפליקציה לדוגמה – הטמעת פעילות