תמיכה בחלונות במחשב

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

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

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

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

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

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

משתמשים יכולים גם להפעיל את חלונות שולחן העבודה מהתפריט שמופיע מתחת לידית החלון כשמקישים או לוחצים על הידית או משתמשים במקש הקיצור מקש Meta (Windows,‏ Command או חיפוש) + Ctrl + חץ למטה.

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

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

אופטימיזציה של פריסת האפליקציה לסביבה שדומה למחשב

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

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

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowInfoTracker.windowEngagementInfo(this@DesktopWindowingActivity)
            .collect { windowEngagementInfo ->
                if(windowEngagementInfo.hasEngagementMode(WindowEngagementInfo.EngagementMode.PRECISE_POINTER)){
                    showDesktopOptimizedUI()
                }else {
                    showTouchOptimizedUI()
                }
        }
    }
}

מידע נוסף זמין במאמר עיצוב לאפליקציות למחשב.

שינוי גודל ומצב תאימות

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

איור 3. שינוי הגודל של חלון של אפליקציה שמוגבלת לפורטרט לרוחב.

ממשק המשתמש של אפליקציות שהוגדרו כלא ניתנות לשינוי גודל (כלומר, resizeableActivity = false) עובר שינוי גודל תוך שמירה על יחס הגובה-רוחב.

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

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

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

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

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

הגדרת שוליים פנימיים בכותרת בהתאמה אישית

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

‫Chrome לפני ואחרי הטמעה של כותרות בהתאמה אישית.
איור 6. ‫Chrome לפני ואחרי הטמעה של כותרות בהתאמה אישית.

הטמעה

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

window.insetsController?.setSystemBarsAppearance(
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND,
    WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
)

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

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CaptionBar() {
    if (WindowInsets.isCaptionBarVisible) {
        Row(
            modifier = Modifier
                .windowInsetsTopHeight(WindowInsets.captionBar)
                .fillMaxWidth()
                .background(if (isSystemInDarkTheme()) Color.White else Color.Black),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = "Caption Bar Title",
                style = MaterialTheme.typography.titleMedium,
                modifier = Modifier.padding(4.dp)
            )
        }
    }
}

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

  • windowInsetsTopHeight(): מגדיר אוטומטית את הגובה של רכיב ה-Composable כך שיתאים לסרגל הכותרת של המערכת, ועוזר לרקע המותאם אישית למלא את אזור הכיתוב בלי לקודד ערכי פיקסלים.

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

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

ממשק משתמש

כדי להימנע מחפיפה בין ממשק המשתמש לבין כפתורי המערכת, ב-Android 15 יש את השיטה WindowInsets#getBoundingRects(). השיטה מחזירה רשימה של אובייקטים מסוג Rect שמייצגים אזורים שמוקצים לרכיבי מערכת. כל השטח שנותר בסרגל הכיתוב הוא אזור גלוי בוודאות שבו אפשר להציב בבטחה תוכן מותאם אישית.

אפשר להחליף בין עיצוב בהיר לכהה של רכיבי הכתוביות במערכת באמצעות APPEARANCE_LIGHT_CAPTION_BARS. כדי לגשת לתוספים בתוך הודעות, משתמשים ב-WindowInsets.Companion.captionBar() במצב כתיבה או ב-WindowInsets.Type.captionBar() ב-Views.

מידע נוסף זמין במאמר בנושא שוליים פנימיים של חלונות.

תמיכה במולטיטסקינג ובכמה מופעים במקביל

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

החל מ-Android 15, אפשר להשתמש ב-PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI. כשמגדירים את המאפיין הזה ב-AndroidManifest.xml, מציינים שממשק המשתמש של המערכת צריך לספק אפשרויות (כמו לחצן 'חלון חדש') להפעלת האפליקציה בכמה מופעים.

<application>
    <property
        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
        android:value="true" />
</application>

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

ניהול מופעים של אפליקציות באמצעות תנועות גרירה

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

איור 7. מתחילים מופע חדש של Chrome על ידי גרירת כרטיסייה מחוץ לחלון של שולחן העבודה.

העברת נתונים באמצעות גרירה ושחרור

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

ב-Android 15 יש שני דגלים מרכזיים להצגת חלונות בסגנון מחשב ולפעולות עם כמה מופעים במקביל:

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

Modifier.dragAndDropSource { _ ->
    DragAndDropTransferData(
        clipData = ClipData.newPlainText("label", "Your data"),
        flags = View.DRAG_FLAG_GLOBAL_SAME_APPLICATION
    )
}

  • DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG: מאפשר למשתמשים להתחיל מופע חדש של האפליקציה על ידי גרירת התוכן והשלכתו לאזור ריק במסך, אם אף חלון אחר לא מטפל בהשלכה.
    • כשמשתמשים בדגל הזה, צריך לספק IntentSender באמצעות ClipData.Item.Builder#setIntentSender(). המערכת משתמשת בערך הזה כדי להפעיל את הפעילות החדשה אם מתרחשת השמטה שלא טופלה.

Modifier.dragAndDropSource { _ ->
    val intent = Intent.makeMainActivity(activity.componentName).apply {
        putExtra("EXTRA_ITEM_ID", itemId)
        flags = Intent.FLAG_ACTIVITY_NEW_TASK or
                Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
                Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT
    }

    val pendingIntent = PendingIntent.getActivity(
        activity, 0, intent, PendingIntent.FLAG_IMMUTABLE
    )

    val data = ClipData(
        "Item $itemId",
        arrayOf(ClipDescription.MIMETYPE_TEXT_INTENT),
        ClipData.Item.Builder().setIntentSender(pendingIntent.intentSender).build()
    )

    DragAndDropTransferData(
        clipData = data,
        flags = View.DRAG_FLAG_GLOBAL_SAME_APPLICATION or
                View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG,
    )
}

קבלת נתונים שהועברו

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

Modifier.dragAndDropTarget(
    shouldStartDragAndDrop = { event ->
        event.toAndroidDragEvent().clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)
    },
    target = object : DragAndDropTarget {
        override fun onDrop(event: DragAndDropEvent): Boolean {
            requestDragAndDropPermissions(activity, event.toAndroidDragEvent())
            val clipData = event.toAndroidDragEvent().clipData
            val item = clipData?.getItemAt(0)?.text
            if (item != null) {
                // Process the dropped text item here
            }
            return item != null
        }
    }
)

השלבים העיקריים:

  • מסנן: משתמשים ב-shouldStartDragAndDrop כדי לבדוק אם יש תמיכה בנתונים הנכנסים (סוג MIME).
  • הרשאות: התקשר אל requestDragAndDropPermissions(event) כדי לגשת לנתונים.
  • הטיפול: חילוץ הנתונים בקריאה החוזרת onDrop.

אופטימיזציות נוספות

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

הגדרת גודל ומיקום כברירת מחדל

לא כל האפליקציות, גם אם אפשר לשנות את הגודל שלהן, צריכות חלון גדול כדי לספק ערך למשתמשים. אפשר להשתמש ב-method‏ ActivityOptions#setLaunchBounds() כדי לציין גודל ומיקום כברירת מחדל כשמפעילים פעילות.

כניסה למסך מלא מהמרחב במחשב

אפליקציות יכולות לעבור למסך מלא באמצעות קריאה ל-Activity#requestFullScreenMode(). השיטה מציגה את האפליקציה במסך מלא ישירות מממשק מחשב.