รองรับการจัดหน้าต่างเดสก์ท็อป

การจัดหน้าต่างเดสก์ท็อปช่วยให้ผู้ใช้เรียกใช้แอปหลายแอปพร้อมกันในหน้าต่างแอปที่ปรับขนาดได้ เพื่อให้ได้รับประสบการณ์การใช้งานที่หลากหลายและคล้ายกับบนเดสก์ท็อป

ในรูปที่ 1 คุณจะเห็นการจัดระเบียบหน้าจอเมื่อเปิดใช้การจัดหน้าต่างเดสก์ท็อป สิ่งที่ควรทราบ

  • ผู้ใช้สามารถเรียกใช้แอปหลายแอปพร้อมกันแบบเคียงข้างกันได้
  • แถบงานจะอยู่ในตำแหน่งคงที่ที่ด้านล่างของจอแสดงผล ซึ่งจะแสดงแอปที่กำลังทำงานอยู่ ผู้ใช้สามารถปักหมุดแอปเพื่อเข้าถึงได้อย่างรวดเร็ว
  • แถบส่วนหัวที่ปรับแต่งได้ใหม่จะตกแต่งด้านบนของแต่ละหน้าต่างด้วยส่วนควบคุมต่างๆ เช่น ย่อเล็กสุดและขยายใหญ่สุด
รูปที่ 1 การจัดหน้าต่างเดสก์ท็อปบนแท็บเล็ต

โดยค่าเริ่มต้นแล้ว แอปจะเปิดแบบเต็มหน้าจอบนแท็บเล็ต Android หากต้องการเปิดแอปในโหมดการจัดหน้าต่างเดสก์ท็อป ให้กดแฮนเดิลของหน้าต่างที่ด้านบนของหน้าจอค้างไว้ แล้วลากแฮนเดิลภายใน UI ดังที่แสดงในรูปที่ 2

เมื่อแอปเปิดอยู่ในโหมดการจัดหน้าต่างเดสก์ท็อป แอปอื่นๆ ก็จะเปิดในหน้าต่างเดสก์ท็อปด้วย

รูปที่ 2 กดแฮนเดิลของหน้าต่างแอปค้างไว้แล้วลากเพื่อเข้าสู่โหมดการจัดหน้าต่างเดสก์ท็อป

ผู้ใช้ยังเรียกใช้โหมดการจัดหน้าต่างเดสก์ท็อปได้จากเมนูที่ปรากฏขึ้นใต้ แฮนเดิลของหน้าต่างเมื่อคุณแตะหรือคลิกแฮนเดิล หรือใช้แป้นพิมพ์ลัด แป้น Meta (Windows, Command หรือ Search) + Ctrl + ลูกศรลง

ผู้ใช้จะออกจากโหมดการจัดหน้าต่างเดสก์ท็อปได้โดยการปิดหน้าต่างที่ใช้งานอยู่ทั้งหมด หรือโดยการจับแฮนเดิลของหน้าต่างที่ด้านบนของหน้าต่างเดสก์ท็อปแล้วลากแอปไปที่ด้านบนของหน้าจอ แป้นพิมพ์ลัด Meta + H จะออกจากโหมดการจัดหน้าต่างเดสก์ท็อป และเรียกใช้แอปแบบเต็มหน้าจออีกครั้งด้วย

หากต้องการกลับไปที่โหมดการจัดหน้าต่างเดสก์ท็อป ให้แตะหรือคลิกไทล์พื้นที่เดสก์ท็อปในหน้าจอ "ล่าสุด"

เพิ่มประสิทธิภาพเลย์เอาต์ของแอปให้เหมาะกับสภาพแวดล้อมที่คล้ายกับบนเดสก์ท็อป

การออกแบบสำหรับประสบการณ์การใช้งานบนเดสก์ท็อปอาจแตกต่างจากการออกแบบสำหรับอุปกรณ์เคลื่อนที่อย่างมาก เนื่องจากมีพื้นที่หน้าจอมากขึ้น การป้อนข้อมูลด้วยเมาส์และแป้นพิมพ์มีความแม่นยำสูง และผู้ใช้คาดหวังว่าจะได้รับประสิทธิภาพการทำงานสูง

Jetpack WindowManager มี API ที่กำหนดไว้เพื่อช่วยให้นักพัฒนาแอปตัดสินใจ ว่าจะแสดง UI บนเดสก์ท็อปเมื่อใด ซึ่งโดยทั่วไปแล้วจะมีข้อมูลหนาแน่นกว่า รูปแบบการนำทางที่แตกต่างกัน และการโต้ตอบด้วยเมาส์ที่เพิ่มประสิทธิภาพแล้ว

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) จะมีการปรับขนาด UI โดยรักษาสัดส่วนภาพเดิมไว้

รูปที่ 4 UI ของแอปที่ปรับขนาดไม่ได้จะปรับขนาดตามการปรับขนาดหน้าต่าง

แอปกล้องที่ล็อกการวางแนวหรือประกาศว่าปรับขนาดไม่ได้จะมีวิธีจัดการช่องมองภาพของกล้องเป็นพิเศษ โดยหน้าต่างจะปรับขนาดได้อย่างเต็มที่ แต่ช่องมองภาพจะรักษาสัดส่วนภาพเดิมไว้ การสันนิษฐานว่าแอปจะทำงานในแนวตั้งหรือแนวนอนเสมอจะทำให้แอปฮาร์ดโค้ดหรือทำการสันนิษฐานอื่นๆ ที่นำไปสู่การคำนวณการวางแนวหรือสัดส่วนภาพของภาพตัวอย่างหรือภาพที่ถ่ายได้ไม่ถูกต้อง ซึ่งส่งผลให้ภาพยืดออก ตะแคง หรือกลับหัว

จนกว่าแอปจะพร้อมใช้งานช่องมองภาพของกล้องที่ปรับเปลี่ยนตามอุปกรณ์ได้เต็มรูปแบบ วิธีจัดการพิเศษนี้จะมอบประสบการณ์การใช้งานพื้นฐานมากขึ้นซึ่งช่วยลดผลกระทบที่อาจเกิดขึ้นจากการสันนิษฐานที่ไม่ถูกต้อง

ดูข้อมูลเพิ่มเติมเกี่ยวกับโหมดความเข้ากันได้สำหรับแอปกล้องได้ที่โหมดความเข้ากันได้ ของอุปกรณ์

รูปที่ 5 ช่องมองภาพของกล้องจะรักษาสัดส่วนภาพไว้เมื่อมีการปรับขนาดหน้าต่าง

ระยะขอบส่วนหัวที่ปรับแต่งได้

แอปทั้งหมดที่ทำงานในโหมดการจัดหน้าต่างเดสก์ท็อปจะมีแถบส่วนหัว แม้ใน โหมดเต็มหน้าจอ คุณสามารถปรับแต่งแถบนี้เพื่อป้องกันไม่ให้เนื้อหาของแอปถูกบดบัง และเพื่อวาดองค์ประกอบ UI ที่กำหนดเองลงในพื้นที่ส่วนหัวโดยตรง

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): กำหนดค่าลักษณะภาพของแถบระบบ พารามิเตอร์แรกกำหนดแฟล็กลักษณะเป้าหมาย ส่วนพารามิเตอร์ที่ 2 ทำหน้าที่เป็นมาสก์เพื่อควบคุมว่าจะแก้ไขแฟล็กใดบ้าง

  • windowInsetsTopHeight(): กำหนดความสูงของ Composable โดยอัตโนมัติให้ตรงกับแถบส่วนหัวของระบบ ซึ่งจะช่วยให้พื้นหลังที่กำหนดเองเติมพื้นที่คำอธิบายได้โดยไม่ต้องฮาร์ดโค้ดค่าพิกเซล

  • WindowInsets.captionBar: ระบุขนาดสำหรับส่วนควบคุมการจัดหน้าต่างเดสก์ท็อป (ปิด, ขยายใหญ่สุด ฯลฯ) ซึ่งช่วยให้ UI ปรับขนาดหรือซ่อนโดยอัตโนมัติเมื่อเข้าสู่หรือออกจากโหมดการจัดหน้าต่างเดสก์ท็อป

ดูข้อมูลเพิ่มเติมได้ที่หัวข้อเกี่ยวกับระยะขอบของหน้าต่าง นอกจากชื่อแล้ว คุณยังแสดงองค์ประกอบ UI อื่นๆ ในแถบคำอธิบายได้ด้วย เช่น แท็บ (เหมือนใน Google Chrome) แถบค้นหา หรืออวาตาร์โปรไฟล์

อินเทอร์เฟซผู้ใช้

Android 15 มีเมธอด WindowInsets#getBoundingRects() เพื่อหลีกเลี่ยงไม่ให้ UI ทับซ้อนกับปุ่มระบบ เมธอดนี้จะแสดงผลรายการของ Rect ออบเจ็กต์ที่แสดงพื้นที่ที่องค์ประกอบระบบครอบครอง พื้นที่ที่เหลือในแถบคำอธิบายคือ โซนปลอดภัย ซึ่งคุณสามารถวางเนื้อหาที่กำหนดเองได้อย่างปลอดภัย

สลับลักษณะที่ปรากฏขององค์ประกอบคำอธิบายระบบสำหรับธีมสว่างและธีมมืดโดยใช้ APPEARANCE_LIGHT_CAPTION_BARS เข้าถึงระยะขอบโดยใช้ WindowInsets.Companion.captionBar() ใน Compose หรือ WindowInsets.Type.captionBar() ใน View

ดูข้อมูลเพิ่มเติมได้ที่หัวข้อเกี่ยวกับระยะขอบของหน้าต่าง

การทำงานหลายอย่างพร้อมกันและการรองรับอินสแตนซ์หลายรายการ

การทำงานหลายอย่างพร้อมกันเป็นหัวใจสำคัญของโหมดการจัดหน้าต่างเดสก์ท็อป และการอนุญาตให้แอปมีหลายอินสแตนซ์จะช่วยเพิ่มประสิทธิภาพการทำงานของผู้ใช้ได้อย่างมาก

ตั้งแต่ Android 15 เป็นต้นไป คุณสามารถใช้ PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI ได้ การตั้งค่าพร็อพเพอร์ตี้นี้ใน AndroidManifest.xml จะระบุว่า UI ของระบบควรมีตัวเลือก (เช่น ปุ่ม "หน้าต่างใหม่") เพื่อให้แอปเปิดใช้ในหลายอินสแตนซ์ได้

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

หมายเหตุ: ในโหมดการจัดหน้าต่างเดสก์ท็อปและสภาพแวดล้อมแบบหลายหน้าต่างอื่นๆ งานใหม่จะเปิดในหน้าต่างใหม่ ดังนั้นโปรดตรวจสอบเส้นทางของผู้ใช้ทุกครั้งที่แอปเริ่มงานหลายงาน

จัดการอินสแตนซ์ของแอปด้วยท่าทางสัมผัสแบบลาก

ในโหมดหลายหน้าต่าง ผู้ใช้สามารถเริ่มอินสแตนซ์ใหม่ของแอปได้โดยการลากองค์ประกอบ UI (เช่น แท็บหรือเอกสาร) ออกจากหน้าต่างของแอป นอกจากนี้ ผู้ใช้ยังย้ายองค์ประกอบระหว่างอินสแตนซ์ต่างๆ ของแอปเดียวกันได้ด้วย

รูปที่ 7 เริ่มอินสแตนซ์ใหม่ของ Chrome โดยการลากแท็บออกจากหน้าต่างเดสก์ท็อป

โอนข้อมูลด้วยการลากและวาง

หากต้องการกำหนดค่า Composable เป็นแหล่งที่มาของการลากและวางแบบอินสแตนซ์หลายรายการ เพื่อให้ผู้ใช้ลากเนื้อหาไปยังอินสแตนซ์อื่นของแอป หรือสร้าง ใหม่ อินสแตนซ์โดยการวางเนื้อหาลงในพื้นที่ว่างของหน้าจอ ให้ใช้ dragAndDropSource ตัวปรับเปลี่ยน ในแลมบ์ดา ให้แสดงผล DragAndDropTransferData โดยส่ง ClipData ที่มีข้อมูลที่จะ โอน และแฟล็กเพื่อกำหนดค่าลักษณะการทำงานแบบอินสแตนซ์หลายรายการ

Android 15 ขอแนะนำแฟล็กหลัก 2 รายการสำหรับการจัดหน้าต่างสไตล์เดสก์ท็อปและการโต้ตอบแบบอินสแตนซ์หลายรายการ ดังนี้

  • 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

การเพิ่มประสิทธิภาพเพิ่มเติม

ปรับแต่งการเปิดแอปและเปลี่ยนแอปจากโหมดการจัดหน้าต่างเดสก์ท็อปเป็นแบบเต็มหน้าจอ

ระบุขนาดและตำแหน่งเริ่มต้น

แอปบางแอปไม่จำเป็นต้องมีหน้าต่างขนาดใหญ่เพื่อมอบคุณค่าแก่ผู้ใช้ แม้ว่าจะปรับขนาดได้ก็ตาม คุณ สามารถใช้เมธอด ActivityOptions#setLaunchBounds() เพื่อระบุขนาดและตำแหน่งเริ่มต้น เมื่อเปิดใช้งานกิจกรรม

เข้าสู่โหมดเต็มหน้าจอจากพื้นที่เดสก์ท็อป

แอปสามารถเข้าสู่โหมดเต็มหน้าจอได้โดยการเรียกใช้ Activity#requestFullScreenMode() เมธอดนี้จะแสดงแอปแบบเต็มหน้าจอโดยตรงจากโหมดการจัดหน้าต่างเดสก์ท็อป