แสดงเนื้อหาแบบไร้ขอบในแอปและจัดการส่วนหน้าต่างใน Compose

แพลตฟอร์ม Android มีหน้าที่วาด UI ของระบบ เช่น แถบสถานะและแถบนําทาง UI ระบบนี้จะแสดงขึ้นไม่ว่าผู้ใช้จะใช้แอปใดก็ตาม

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

วาดจากขอบหนึ่งไปอีกขอบหนึ่งเพื่อวาดหลังแถบระบบ
รูปที่ 1 วาดจากขอบถึงขอบเพื่อวาดหลังแถบระบบ

ใน Android 14 (API ระดับ 34) และต่ำกว่า UI ของแอปจะไม่วาดใต้แถบระบบและส่วนที่ถูกตัดออกของจอแสดงผลโดยค่าเริ่มต้น

ใน Android 15 (API ระดับ 35) ขึ้นไป แอปจะวาดใต้แถบระบบและแสดงส่วนที่ถูกตัดออกเมื่อแอปกำหนดเป้าหมายเป็น SDK 35 ซึ่งส่งผลให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ราบรื่นยิ่งขึ้น และแอปของคุณก็ใช้ประโยชน์จากพื้นที่ในหน้าต่างได้อย่างเต็มที่

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

ข้อมูลพื้นฐานเกี่ยวกับส่วนแทรก

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

ขนาดของ UI ของระบบและข้อมูลเกี่ยวกับตําแหน่งที่จะวาง UI นั้นระบุผ่านส่วนเกิน

ส่วนต่างๆ ของ UI ของระบบจะมีส่วนแทรกประเภทที่เกี่ยวข้องซึ่งอธิบายขนาดและตำแหน่งของส่วนนั้น ตัวอย่างเช่น ข้อมูลแทรกของแถบสถานะจะระบุขนาดและตําแหน่งของแถบสถานะ ส่วนข้อมูลแทรกของแถบนําทางจะระบุขนาดและตําแหน่งของแถบนําทาง ข้อมูลแทรกแต่ละประเภทประกอบด้วยมิติข้อมูลพิกเซล 4 รายการ ได้แก่ ด้านบน ซ้าย ขวา และด้านล่าง มิติข้อมูลเหล่านี้ระบุระยะที่ UI ของระบบยื่นออกมาจากด้านที่เกี่ยวข้องของหน้าต่างแอป ดังนั้น UI ของแอปจึงต้องฝังเข้าไปเป็นจำนวนดังกล่าวเพื่อไม่ให้ทับซ้อนกับ UI ของระบบประเภทนั้น

ประเภทอินเซ็ตในตัวของ Android ที่พร้อมใช้งานผ่าน WindowInsets มีดังนี้

WindowInsets.statusBars

ส่วนที่เป็นภาพประกอบที่อธิบายแถบสถานะ แถบเหล่านี้คือแถบ UI ของระบบด้านบนที่มีไอคอนการแจ้งเตือนและตัวบ่งชี้อื่นๆ

WindowInsets.statusBarsIgnoringVisibility

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

WindowInsets.navigationBars

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

WindowInsets.navigationBarsIgnoringVisibility

แถบนำทางจะฝังอยู่เมื่อมองเห็น หากขณะนี้แถบนำทางซ่อนอยู่ (เนื่องจากเข้าสู่โหมดเต็มหน้าจอที่สมจริง) ส่วนแทรกของแถบนำทางหลักจะว่างเปล่า แต่ส่วนแทรกเหล่านี้จะไม่ว่างเปล่า

WindowInsets.captionBar

ส่วนที่เป็นภาพแทรกที่อธิบายการตกแต่งหน้าต่าง UI ของระบบหากอยู่ในหน้าต่างรูปแบบอิสระ เช่น แถบชื่อด้านบน

WindowInsets.captionBarIgnoringVisibility

แถบคำบรรยายจะฝังอยู่เมื่อปรากฏ หากตอนนี้แถบคำบรรยายแทนเสียงซ่อนอยู่ ส่วนย่อยของแถบคำบรรยายแทนเสียงหลักจะว่างเปล่า แต่ส่วนย่อยเหล่านี้จะไม่ว่างเปล่า

WindowInsets.systemBars

การรวมส่วนแทรกของแถบระบบ ซึ่งรวมถึงแถบสถานะ แถบนําทาง และแถบคำบรรยายแทนเสียง

WindowInsets.systemBarsIgnoringVisibility

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

WindowInsets.ime

ส่วนแทรกที่อธิบายพื้นที่ด้านล่างที่แป้นพิมพ์ซอฟต์แวร์ใช้

WindowInsets.imeAnimationSource

ส่วนที่เป็นภาพตัดแสดงพื้นที่ที่แป้นพิมพ์ซอฟต์แวร์ใช้ก่อนภาพเคลื่อนไหวของแป้นพิมพ์ปัจจุบัน

WindowInsets.imeAnimationTarget

ส่วนที่เป็นภาพตัดแสดงพื้นที่ที่แป้นพิมพ์ซอฟต์แวร์จะครอบครองหลังจากภาพเคลื่อนไหวของแป้นพิมพ์ปัจจุบัน

WindowInsets.tappableElement

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

WindowInsets.tappableElementIgnoringVisibility

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

WindowInsets.systemGestures

ส่วนที่ยื่นออกมาแสดงจำนวนส่วนที่ยื่นออกมาที่ระบบจะขัดจังหวะท่าทางสัมผัสสำหรับการไปยังส่วนต่างๆ แอปสามารถระบุการจัดการท่าทางสัมผัสเหล่านี้ในจำนวนที่จำกัดด้วยตนเองผ่าน Modifier.systemGestureExclusion

WindowInsets.mandatorySystemGestures

ท่าทางสัมผัสของระบบชุดย่อยที่ระบบจะจัดการเสมอ และเลือกไม่ใช้ผ่าน Modifier.systemGestureExclusion ไม่ได้

WindowInsets.displayCutout

ส่วนที่เป็นขอบแสดงถึงระยะห่างที่จําเป็นเพื่อหลีกเลี่ยงการซ้อนทับกับรอยบากของจอแสดงผล (รอยบากหรือรูเล็ก ๆ)

WindowInsets.waterfall

ส่วนที่เป็นภาพตัดแสดงพื้นที่โค้งของการแสดงผล Waterfall การแสดงผล Waterfall มีบริเวณโค้งตามขอบของหน้าจอที่หน้าจอเริ่มโค้งไปตามด้านข้างของอุปกรณ์

ประเภทเหล่านี้จะสรุปเป็นประเภทโฆษณาซ้อนใน "ปลอดภัย" 3 ประเภทต่อไปนี้เพื่อให้มั่นใจว่าเนื้อหาจะไม่ถูกบดบัง

ประเภทเนื้อหาที่ฝัง "ปลอดภัย" เหล่านี้จะปกป้องเนื้อหาด้วยวิธีต่างๆ โดยอิงตามเนื้อหาที่ฝังของแพลตฟอร์มพื้นฐาน ดังนี้

  • ใช้ WindowInsets.safeDrawing เพื่อปกป้องเนื้อหาที่ไม่ควรวาดไว้ใต้ UI ของระบบ การใช้งานที่พบบ่อยที่สุดของส่วนตัดคือเพื่อป้องกันไม่ให้วาดเนื้อหาที่ถูกบดบังโดย UI ของระบบ (บางส่วนหรือทั้งหมด)
  • ใช้ WindowInsets.safeGestures เพื่อปกป้องเนื้อหาด้วยท่าทางสัมผัส วิธีนี้จะช่วยป้องกันไม่ให้ท่าทางสัมผัสของระบบขัดแย้งกับท่าทางสัมผัสของแอป (เช่น ท่าทางสัมผัสสำหรับชีตด้านล่าง ภาพสไลด์ หรือในเกม)
  • ใช้ WindowInsets.safeContent ร่วมกับ WindowInsets.safeDrawing และ WindowInsets.safeGestures เพื่อให้เนื้อหาไม่มีภาพซ้อนทับและท่าทางสัมผัสซ้อนทับ

การตั้งค่าส่วนแทรก

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

  1. กําหนดเป้าหมายเป็น SDK 35 ขึ้นไปเพื่อบังคับใช้การแสดงผลแบบไร้ขอบใน Android 15 ขึ้นไป แอปจะแสดงอยู่หลัง UI ของระบบ คุณสามารถปรับ UI ของแอปได้โดยจัดการส่วนเยื้อง
  2. (ไม่บังคับ) เรียกใช้ enableEdgeToEdge() ใน Activity.onCreate() ซึ่งจะช่วยให้แอปเป็นแบบไร้ขอบใน Android เวอร์ชันเก่าได้
  3. ตั้งค่า android:windowSoftInputMode="adjustResize" ในรายการ AndroidManifest.xml ของกิจกรรม การตั้งค่านี้ช่วยให้แอปได้รับขนาดของ IME ซอฟต์แวร์เป็นอินเซ็ต ซึ่งคุณใช้เพื่อเพิ่มระยะห่างและวางเลย์เอาต์เนื้อหาได้อย่างเหมาะสมเมื่อ IME ปรากฏและหายไปในแอป

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

Compose APIs

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

ต่อไปนี้เป็นตัวอย่างวิธีพื้นฐานที่สุดในการใช้ส่วนแทรกกับเนื้อหาของแอปทั้งแอป

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

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

ข้อความที่แทรกประเภทเหล่านี้ทั้งหมดจะมีภาพเคลื่อนไหวโดยอัตโนมัติด้วยภาพเคลื่อนไหว IME ที่พอร์ตกลับมายัง API 21 นอกจากนี้ เลย์เอาต์ทั้งหมดที่ใช้ส่วนแทรกเหล่านี้จะเคลื่อนไหวโดยอัตโนมัติเมื่อค่าส่วนแทรกเปลี่ยนแปลง

การใช้ประเภทการฝังเหล่านี้เพื่อปรับเลย์เอาต์ Composable มี 2 วิธีหลักๆ ได้แก่ ตัวแก้ไขระยะห่างจากขอบและตัวแก้ไขขนาดการฝัง

ตัวปรับระยะห่างจากขอบ

Modifier.windowInsetsPadding(windowInsets: WindowInsets) ใช้ระยะขอบหน้าต่างที่ระบุเป็นระยะห่างจากขอบ โดยทํางานเหมือนกับที่ Modifier.padding จะทำ เช่น Modifier.windowInsetsPadding(WindowInsets.safeDrawing) ใช้ส่วนแทรกของภาพวาดที่ปลอดภัยเป็นระยะห่างจากขอบทั้ง 4 ด้าน

นอกจากนี้ยังมีเมธอดยูทิลิตีในตัวหลายรายการสําหรับประเภทการฝังที่พบบ่อยที่สุด Modifier.safeDrawingPadding() เป็นวิธีหนึ่งซึ่งเทียบเท่ากับ Modifier.windowInsetsPadding(WindowInsets.safeDrawing) มีตัวหรี่ที่คล้ายกันสำหรับประเภทการฝังอื่นๆ

ตัวแก้ไขขนาดของส่วนแทรก

ตัวแก้ไขต่อไปนี้จะใช้จำนวนส่วนโค้งมนของหน้าต่างโดยกำหนดขนาดของคอมโพเนนต์ให้เท่ากับขนาดของส่วนโค้งมน

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

ใช้ด้านเริ่มต้นของ windowInsets เป็นความกว้าง (เช่น Modifier.width)

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

ใช้ด้านท้ายของ windowInsets เป็นความกว้าง (เช่น Modifier.width)

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

ใช้ด้านข้างบนของ windowInsets เป็นค่าความสูง (เช่น Modifier.height)

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

ใช้ด้านล่างของ windowInsets เป็นค่าความสูง (เช่น Modifier.height)

ตัวแก้ไขเหล่านี้มีประโยชน์อย่างยิ่งสำหรับการปรับขนาด Spacer ที่กินพื้นที่ของส่วนแทรก

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

การใช้งานที่ฝัง

ตัวแก้ไขระยะห่างจากขอบใน (windowInsetsPadding และตัวช่วย เช่น safeDrawingPadding) จะใช้ส่วนของส่วนที่เยื้องเข้ามาซึ่งใช้เป็นระยะห่างจากขอบโดยอัตโนมัติ เมื่อเข้าไปในลําดับชั้นองค์ประกอบที่ลึกขึ้น ตัวแก้ไขระยะห่างจากขอบด้านในแบบฝังและตัวแก้ไขขนาดระยะห่างจากขอบด้านในจะทราบว่ามีการใช้ระยะห่างจากขอบด้านในบางส่วนโดยตัวแก้ไขระยะห่างจากขอบด้านในแบบด้านนอกแล้ว และหลีกเลี่ยงการใช้ระยะห่างจากขอบด้านในบางส่วนเดียวกันมากกว่า 1 ครั้ง ซึ่งจะทำให้มีระยะห่างเพิ่มเติมมากเกินไป

นอกจากนี้ ตัวแก้ไขขนาดของส่วนตัดยังหลีกเลี่ยงการใช้ส่วนตัดเดียวกันมากกว่า 1 ครั้งด้วยหากมีการใช้ส่วนตัดไปแล้ว อย่างไรก็ตาม เนื่องจากชิ้นงานเหล่านี้จะเปลี่ยนขนาดโดยตรง จึงไม่ได้กินพื้นที่ส่วนเกินเอง

ด้วยเหตุนี้ ตัวแก้ไขการเว้นวรรคที่ฝังไว้จึงเปลี่ยนจำนวนการเว้นวรรคที่ใช้กับคอมโพสิเบิลแต่ละรายการโดยอัตโนมัติ

เมื่อดูตัวอย่าง LazyColumn เดียวกันกับก่อนหน้านี้ LazyColumn จะได้รับการปรับขนาดโดยตัวแก้ไข imePadding ภายใน LazyColumn รายการสุดท้ายจะปรับขนาดให้เท่ากับความสูงของด้านล่างของแถบระบบ

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

เมื่อ IME ปิดอยู่ ตัวแก้ไข imePadding() จะไม่ใช้ระยะห่างจากขอบ เนื่องจาก IME ไม่มีความสูง เนื่องจากตัวแก้ไข imePadding() ไม่ได้ใช้ระยะห่างจากขอบ ระบบจะไม่ใช้ส่วนเว้า และส่วนสูงของ Spacer จะเท่ากับขนาดของด้านด้านล่างของแถบระบบ

เมื่อ IME เปิดขึ้น ส่วนที่แทรกของ IME จะเคลื่อนไหวเพื่อให้ตรงกับขนาดของ IME และตัวแก้ไข imePadding() จะเริ่มใช้ระยะห่างจากด้านล่างเพื่อปรับขนาด LazyColumn เมื่อ IME เปิดขึ้น เมื่อตัวแก้ไข imePadding() เริ่มใช้ระยะห่างจากขอบด้านล่าง ก็จะเริ่มใช้ระยะห่างจากขอบในจำนวนนั้นด้วย ดังนั้น ความสูงของ Spacer จึงเริ่มลดลง เนื่องจากตัวดัดแปลง imePadding() ได้ใช้การเว้นวรรคสำหรับแถบของระบบแล้ว เมื่อตัวแก้ไข imePadding() ใช้ระยะห่างจากขอบด้านล่างที่มากกว่าแถบระบบ ความสูงของ Spacer จะเท่ากับ 0

เมื่อ IME ปิด การเปลี่ยนแปลงจะเกิดขึ้นในลักษณะย้อนกลับ โดย Spacer จะเริ่มขยายจากระดับความสูง 0 เมื่อ imePadding() มีผลน้อยกว่าด้านด้านล่างของแถบระบบ จนในที่สุด Spacer จะเท่ากับความสูงของด้านด้านล่างของแถบระบบเมื่อ IME แสดงภาพเคลื่อนไหวจนหมด

รูปที่ 2 คอลัมน์แบบ Lazy จากขอบถึงขอบที่มี TextField

ลักษณะการทำงานนี้เกิดขึ้นจากการสื่อสารระหว่างตัวปรับเปลี่ยน windowInsetsPadding ทั้งหมด และอาจได้รับอิทธิพลจากวิธีอื่นๆ อีก 2 วิธี

Modifier.consumeWindowInsets(insets: WindowInsets) ยังใช้ส่วนเกินในลักษณะเดียวกับ Modifier.windowInsetsPadding แต่ไม่ได้ใช้ส่วนเกินที่ใช้เป็นระยะห่างจากขอบ ซึ่งมีประโยชน์เมื่อใช้ร่วมกับตัวแก้ไขขนาดของส่วนตัดเพื่อบ่งบอกให้องค์ประกอบพี่น้องทราบว่ามีการใช้ส่วนตัดจำนวนหนึ่งแล้ว

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues) ทำงานคล้ายกับเวอร์ชันที่มีอาร์กิวเมนต์ WindowInsets มาก แต่จะใช้ PaddingValues ที่กำหนดเอง ซึ่งมีประโยชน์ในการแจ้งให้องค์ประกอบย่อยทราบเมื่อมีการใส่การเว้นวรรคหรือระยะห่างโดยกลไกอื่นนอกเหนือจากตัวแก้ไขการเว้นวรรคที่ฝัง เช่น Modifier.padding ธรรมดาหรือตัวเว้นวรรคความสูงคงที่

Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

ในกรณีที่ต้องใช้ข้อมูลอินเซ็ตของกรอบเวลาดิบแบบไม่รวมการบริโภค ให้ใช้ค่า WindowInsets โดยตรง หรือใช้ WindowInsets.asPaddingValues() เพื่อแสดงผล PaddingValues ของอินเซ็ตที่ไม่ได้รับผลกระทบจากการบริโภค อย่างไรก็ตาม เนื่องจากข้อควรระวังด้านล่าง เราขอแนะนำให้ใช้ตัวแก้ไขระยะห่างจากขอบของส่วนแทรกของหน้าต่างและตัวแก้ไขขนาดของส่วนแทรกของหน้าต่างเมื่อเป็นไปได้

ระยะขอบและระยะของ Jetpack Compose

Compose ใช้ API หลักของ AndroidX ที่อยู่เบื้องหลังเพื่ออัปเดตและแสดงภาพเคลื่อนไหวของส่วนตัด ซึ่งใช้ API ของแพลตฟอร์มที่อยู่เบื้องหลังซึ่งจัดการส่วนตัด ลักษณะการทํางานของแพลตฟอร์มนี้ทําให้ส่วนตัดสัมพันธ์กับระยะของ Jetpack Compose เป็นพิเศษ

ระบบจะอัปเดตค่าของส่วนตัด หลังระยะการคอมโพส แต่ก่อนระยะเลย์เอาต์ ซึ่งหมายความว่าการอ่านค่าของส่วนตัดในองค์ประกอบโดยทั่วไปจะใช้ค่าของส่วนตัดที่ล่าช้า 1 เฟรม ตัวปรับเปลี่ยนในตัวที่อธิบายไว้ในหน้านี้สร้างขึ้นเพื่อเลื่อนเวลาการใช้ค่าของส่วนตัดจนกว่าจะถึงระยะการวางเลย์เอาต์ ซึ่งช่วยให้มั่นใจได้ว่าระบบจะใช้ค่าของส่วนตัดในเฟรมเดียวกันกับที่อัปเดต

ภาพเคลื่อนไหวของ IME บนแป้นพิมพ์ที่มี WindowInsets

คุณสามารถใช้ Modifier.imeNestedScroll() กับคอนเทนเนอร์ที่เลื่อนได้เพื่อเปิดและปิด IME โดยอัตโนมัติเมื่อเลื่อนไปที่ด้านล่างของคอนเทนเนอร์

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

ภาพเคลื่อนไหวแสดงองค์ประกอบ UI ที่เลื่อนขึ้นและลงเพื่อให้แป้นพิมพ์แสดง
รูปที่ 3 ภาพเคลื่อนไหวของ IME

การรองรับการฝังสำหรับคอมโพเนนต์ Material 3

คอมโพสิชัน Material 3 ในตัวจำนวนมาก (androidx.compose.material3) จะจัดการส่วนเกินด้วยตัวเองเพื่อความสะดวกในการใช้งาน โดยอิงตามตำแหน่งที่วางคอมโพสิชันในแอปตามข้อกำหนดของ Material

การจัดการคอมโพเนนต์ที่ประกอบกันได้ในอินเซ็ต

ด้านล่างนี้คือรายการคอมโพเนนต์ Material ที่จัดการส่วนเกินโดยอัตโนมัติ

แถบแอป

  • TopAppBar / SmallTopAppBar / CenterAlignedTopAppBar / MediumTopAppBar / LargeTopAppBar: ใช้ด้านด้านบนและแนวนอนของแถบระบบเป็นระยะห่างจากขอบ เนื่องจากแถบดังกล่าวใช้ที่ด้านบนของหน้าต่าง
  • BottomAppBar: ใช้ด้านด้านล่างและแนวนอนของแถบระบบเป็นระยะห่างจากขอบ

คอนเทนเนอร์เนื้อหา

  • ModalDrawerSheet / DismissibleDrawerSheet / PermanentDrawerSheet (เนื้อหาภายในลิ้นชักการนำทางแบบโมดัล): ใช้ส่วนแทรกแนวตั้งและเริ่มต้นกับเนื้อหา
  • ModalBottomSheet: ใช้ระยะล่าง
  • NavigationBar : ใช้การฝังด้านล่างและแนวนอน
  • NavigationRail: ใช้การเยื้องแนวตั้งและเริ่มต้น

Scaffold

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

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

ลบล้างระยะขอบเริ่มต้น

คุณสามารถเปลี่ยนพารามิเตอร์ windowInsets ที่ส่งไปยังคอมโพสิเบิลเพื่อกําหนดค่าลักษณะการทํางานของคอมโพสิเบิลได้ พารามิเตอร์นี้อาจเป็นส่วนโค้งมนของหน้าต่างประเภทอื่นที่จะนําไปใช้แทน หรือปิดใช้โดยการส่งอินสแตนซ์ว่าง WindowInsets(0, 0, 0, 0)

ตัวอย่างเช่น หากต้องการปิดใช้การจัดการส่วนเกินใน LargeTopAppBar ให้ตั้งค่าพารามิเตอร์ windowInsets เป็นอินสแตนซ์ว่าง ดังนี้

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

การทํางานร่วมกันกับส่วนแทรกของระบบ View

คุณอาจต้องลบล้างส่วนเกินเริ่มต้นเมื่อหน้าจอมีทั้งมุมมองและโค้ดการคอมโพสิทในลําดับชั้นเดียวกัน ในกรณีนี้ คุณต้องระบุอย่างชัดเจนว่าชิ้นงานใดควรใช้ส่วนแทรกและชิ้นงานใดควรละเว้น

เช่น หากเลย์เอาต์ด้านนอกสุดคือเลย์เอาต์ View ของ Android คุณควรใช้ส่วนตัดในระบบ View และละเว้นส่วนตัดใน Compose หรือหากเลย์เอาต์ด้านนอกสุดเป็นคอมโพสิเบิล คุณควรใช้ส่วนเยื้องใน Compose และเพิ่มระยะห่างให้กับคอมโพสิเบิล AndroidView ตามนั้น

โดยค่าเริ่มต้น ComposeView แต่ละรายการจะใช้อินเซ็ตทั้งหมดในระดับการบริโภค WindowInsetsCompat หากต้องการเปลี่ยนลักษณะการทำงานเริ่มต้นนี้ ให้ตั้งค่า ComposeView.consumeWindowInsets เป็น false

การปกป้องแถบระบบ

เมื่อแอปกำหนดเป้าหมายเป็น SDK 35 ขึ้นไป ระบบจะบังคับใช้การแสดงผลแบบไร้ขอบ แถบสถานะของระบบและแถบการนำทางด้วยท่าทางสัมผัสจะโปร่งใส แต่แถบนำทางแบบ 3 ปุ่มจะโปร่งแสง

หากต้องการนำการป้องกันพื้นหลังการนำทางแบบ 3 ปุ่มโปร่งแสงเริ่มต้นออก ให้ตั้งค่า Window.setNavigationBarContrastEnforced เป็น false

แหล่งข้อมูล