WebView จะจัดการการจัดแนวเนื้อหาโดยใช้วิวพอร์ต 2 แบบ ได้แก่ วิวพอร์ตเลย์เอาต์ (ขนาดหน้า) และวิวพอร์ตภาพ (ส่วนของหน้าที่ผู้ใช้เห็นจริง) แม้ว่าวิวพอร์ตของเลย์เอาต์โดยทั่วไปจะคงที่ แต่วิวพอร์ตแบบภาพจะเปลี่ยนแปลงแบบไดนามิกเมื่อผู้ใช้ซูม เลื่อน หรือเมื่อองค์ประกอบ UI ของระบบ (เช่น แป้นพิมพ์เสมือน) ปรากฏขึ้น
ความเข้ากันได้ของฟีเจอร์
การรองรับส่วนแทรกของหน้าต่างใน WebView มีการพัฒนาไปตามกาลเวลาเพื่อให้พฤติกรรมของเนื้อหาเว็บสอดคล้องกับความคาดหวังของแอป Android แบบเนทีฟ ดังนี้
| Milestone | เพิ่มฟีเจอร์แล้ว | ขอบเขต |
|---|---|---|
| M136 | displayCutout() และ systemBars() รองรับผ่าน CSS safe-area-insets |
WebView แบบเต็มหน้าจอเท่านั้น |
| M139 | การรองรับ ime() (ตัวแก้ไขวิธีการป้อนข้อมูล ซึ่งก็คือแป้นพิมพ์) ผ่านการปรับขนาดวิวพอร์ตแบบภาพ |
WebView ทั้งหมด |
| M144 | การสนับสนุน displayCutout() และ systemBars() |
WebView ทั้งหมด (ไม่ว่าจะอยู่ในสถานะเต็มหน้าจอหรือไม่) |
ดูข้อมูลเพิ่มเติมได้ที่ WindowInsetsCompat
กลไกหลัก
WebView จะจัดการส่วนแทรกผ่านกลไกหลัก 2 อย่าง ได้แก่
พื้นที่ปลอดภัย (
displayCutout,systemBars): WebView จะส่งต่อขนาดเหล่านี้ไปยังเนื้อหาเว็บผ่านตัวแปร CSS safe-area-inset-* ซึ่งช่วยให้นักพัฒนาแอปป้องกันไม่ให้องค์ประกอบแบบอินเทอร์แอกทีฟของตนเอง (เช่น แถบการนำทาง) ถูกบดบังด้วยรอยบากหรือแถบสถานะการปรับขนาดวิวพอร์ตภาพโดยใช้ตัวแก้ไขวิธีการป้อนข้อมูล (IME): ตั้งแต่เวอร์ชัน M139 เป็นต้นไป ตัวแก้ไขวิธีการป้อนข้อมูล (IME) จะปรับขนาดวิวพอร์ตภาพโดยตรง กลไกการปรับขนาดนี้ยังอิงตามจุดตัดของ WebView-Window ด้วย ตัวอย่างเช่น ในโหมดมัลติทาสก์ของ Android หากด้านล่างของ WebView ขยายออกไป 200dp ใต้ด้านล่างของหน้าต่าง วิวพอร์ตภาพจะมีขนาดเล็กกว่าขนาดของ WebView อยู่ 200dp การปรับขนาดวิวพอร์ตภาพนี้ (สำหรับทั้ง IME และจุดตัด WebView-Window) จะใช้กับด้านล่างของ WebView เท่านั้น กลไกนี้ไม่รองรับการปรับขนาดสำหรับการซ้อนทับด้านซ้าย ด้านขวา หรือด้านบน ซึ่งหมายความว่าแป้นพิมพ์ที่เชื่อมต่อซึ่งปรากฏที่ขอบเหล่านั้นจะไม่ทริกเกอร์การปรับขนาดวิวพอร์ตภาพ
ก่อนหน้านี้ วิวพอร์ตภาพจะยังคงคงที่ ซึ่งมักจะซ่อนช่องป้อนข้อมูลไว้หลังแป้นพิมพ์ การปรับขนาดวิวพอร์ตจะทำให้ส่วนที่มองเห็นได้ของหน้าเว็บเลื่อนได้โดยค่าเริ่มต้น ซึ่งช่วยให้ผู้ใช้เข้าถึงเนื้อหาที่ถูกบดบังได้
ขอบเขตและตรรกะการทับซ้อนกัน
WebView ควรได้รับค่าส่วนแทรกที่ไม่ใช่ 0 เฉพาะเมื่อองค์ประกอบ UI ของระบบ (แถบ หน้าจอรอยบาก หรือแป้นพิมพ์) ซ้อนทับกับขอบเขตหน้าจอของ WebView โดยตรง หาก WebView ไม่ซ้อนทับกับองค์ประกอบ UI เหล่านี้ (เช่น หาก WebView อยู่ตรงกลางหน้าจอและไม่แตะแถบระบบ) WebView ควรได้รับค่าส่วนแทรกเหล่านั้นเป็น 0
หากต้องการลบล้างตรรกะเริ่มต้นนี้และแสดงเนื้อหาเว็บที่มีขนาดของระบบทั้งหมดโดยไม่คำนึงถึงการทับซ้อน ให้ใช้เมธอด setOnApplyWindowInsetsListener และแสดงผลออบเจ็กต์ windowInsets เดิมที่ไม่มีการแก้ไขจาก Listener การระบุขนาดของระบบทั้งหมดจะช่วยให้การออกแบบมีความสอดคล้องกันโดยทำให้เนื้อหาเว็บสอดคล้องกับฮาร์ดแวร์ของอุปกรณ์ ไม่ว่า WebView จะอยู่ในตำแหน่งใดก็ตาม ซึ่งจะช่วยให้การเปลี่ยนผ่านเป็นไปอย่างราบรื่นเมื่อ WebView ย้ายหรือขยายเพื่อแตะขอบหน้าจอ
Kotlin
ViewCompat.setOnApplyWindowInsetsListener(myWebView) { _, windowInsets -> // By returning the original windowInsets object, we override the default // behavior that zeroes out system insets (like system bars or display // cutouts) when they don't directly overlap the WebView's screen bounds. windowInsets }
Java
ViewCompat.setOnApplyWindowInsetsListener(myWebView, (v, windowInsets) -> { // By returning the original windowInsets object, we override the default // behavior that zeroes out system insets (like system bars or display // cutouts) when they don't directly overlap the WebView's screen bounds. return windowInsets; });
จัดการเหตุการณ์การปรับขนาด
เนื่องจากระดับการมองเห็นแป้นพิมพ์จะทริกเกอร์การปรับขนาดวิวพอร์ตภาพ โค้ดเว็บจึงอาจเห็นเหตุการณ์การปรับขนาดบ่อยขึ้น นักพัฒนาแอปต้องตรวจสอบว่าโค้ดของตนไม่ตอบสนองต่อเหตุการณ์การปรับขนาดเหล่านี้ด้วยการล้างโฟกัสขององค์ประกอบ การทำเช่นนี้จะสร้างลูปของการสูญเสียโฟกัสและการปิดแป้นพิมพ์ซึ่งทำให้ผู้ใช้ป้อนข้อมูลไม่ได้ ดังนี้
- ผู้ใช้โฟกัสที่องค์ประกอบอินพุต
- แป้นพิมพ์จะปรากฏขึ้น ซึ่งจะทริกเกอร์เหตุการณ์การปรับขนาด
- โค้ดของเว็บไซต์จะล้างโฟกัสเพื่อตอบสนองต่อการปรับขนาด
- แป้นพิมพ์ซ่อนอยู่เนื่องจากสูญเสียโฟกัส
หากต้องการลดลักษณะการทำงานนี้ ให้ตรวจสอบ Listener ฝั่งเว็บเพื่อให้แน่ใจว่าการเปลี่ยนแปลงวิวพอร์ตไม่ได้ทริกเกอร์ฟังก์ชัน JavaScript blur() หรือลักษณะการทำงานที่ล้างโฟกัสโดยไม่ตั้งใจ
ใช้การจัดการส่วนแทรก
การตั้งค่าเริ่มต้นของ WebView จะทำงานโดยอัตโนมัติสำหรับแอปส่วนใหญ่ อย่างไรก็ตาม หากแอปใช้เลย์เอาต์ที่กำหนดเอง (เช่น หากคุณเพิ่มระยะห่างจากขอบของคุณเองเพื่อพิจารณาแถบสถานะหรือแป้นพิมพ์) คุณสามารถใช้วิธีการต่อไปนี้เพื่อปรับปรุงการทำงานร่วมกันของเนื้อหาเว็บและ UI แบบเนทีฟ หาก UI แบบเนทีฟใช้ระยะห่างจากขอบกับคอนเทนเนอร์ตาม WindowInsets คุณต้องจัดการส่วนแทรกเหล่านี้อย่างถูกต้องก่อนที่จะถึง WebView เพื่อหลีกเลี่ยงการใช้ระยะห่างจากขอบซ้ำซ้อน
การเว้นระยะห่างซ้ำซ้อนคือสถานการณ์ที่เลย์เอาต์เนทีฟและเนื้อหาเว็บใช้ขนาดส่วนแทรกเดียวกัน ซึ่งส่งผลให้เกิดระยะห่างที่ซ้ำซ้อน ตัวอย่างเช่น สมมติว่าโทรศัพท์มีแถบสถานะ 40 พิกเซล ทั้งมุมมองเนทีฟและ WebView จะเห็นส่วนแทรก 40 พิกเซล ทั้ง 2 องค์ประกอบเพิ่มระยะห่างจากขอบ 40 พิกเซล ส่งผลให้ผู้ใช้เห็นช่องว่าง 80 พิกเซลที่ด้านบน
แนวทาง Zeroing
เพื่อป้องกันไม่ให้เกิดการเว้นระยะห่างซ้ำซ้อน คุณต้องตรวจสอบว่าหลังจากที่มุมมองเนทีฟใช้มิติข้อมูลส่วนแทรกสำหรับการเว้นระยะห่างแล้ว คุณได้รีเซ็ตมิติข้อมูลนั้นเป็น 0 โดยใช้ Insets.NONE ในออบเจ็กต์ WindowInsets ใหม่ก่อนที่จะส่งออบเจ็กต์ที่แก้ไขแล้วลงในลำดับชั้นการแสดงผลไปยัง WebView
เมื่อใช้ระยะห่างจากขอบกับมุมมองหลัก โดยทั่วไปคุณควรใช้วิธีการตั้งค่าเป็นศูนย์โดยตั้งค่า Insets.NONE แทน WindowInsetsCompat.CONSUMED
การส่งคืน WindowInsetsCompat.CONSUMED อาจทำได้ในบางสถานการณ์
อย่างไรก็ตาม อาจเกิดปัญหาได้หากแฮนเดิลของแอปเปลี่ยนส่วนแทรกหรือเพิ่มระยะห่างจากขอบของตัวเอง แต่วิธีการปรับเป็น 0 ไม่มีข้อจำกัดเหล่านี้
หลีกเลี่ยงการเว้นระยะห่างที่มองไม่เห็นด้วยการตั้งค่าส่วนแทรกเป็น 0
หากคุณใช้ส่วนแทรกเมื่อแอปเคยส่งส่วนแทรกที่ไม่ได้ใช้มาก่อน หรือหากส่วนแทรกมีการเปลี่ยนแปลง (เช่น แป้นพิมพ์ซ่อนอยู่) การใช้ส่วนแทรกจะป้องกันไม่ให้ WebView ได้รับการแจ้งเตือนการอัปเดตที่จำเป็น ซึ่งอาจทำให้ WebView คงระยะห่างจากขอบที่มองไม่เห็นจากสถานะก่อนหน้าไว้ (เช่น คงระยะห่างจากขอบของแป้นพิมพ์ไว้หลังจากซ่อนแป้นพิมพ์)
ตัวอย่างต่อไปนี้แสดงการโต้ตอบที่ใช้งานไม่ได้ระหว่างแอปกับ WebView
- สถานะเริ่มต้น: ในตอนแรกแอปจะส่งส่วนแทรกที่ไม่ได้ใช้ (เช่น
displayCutout()หรือsystemBars()) ไปยัง WebView ซึ่งจะใช้ระยะห่างจากขอบกับเนื้อหาเว็บภายใน - การเปลี่ยนแปลงสถานะและข้อผิดพลาด: หากแอปเปลี่ยนสถานะ (เช่น แป้นพิมพ์ซ่อนอยู่) และแอปเลือกที่จะจัดการส่วนแทรกที่ได้โดยแสดงผลเป็น
WindowInsetsCompat.CONSUMED - การแจ้งเตือนถูกบล็อก: การใช้ส่วนแทรกจะป้องกันไม่ให้ระบบ Android ส่งการแจ้งเตือนการอัปเดตที่จำเป็นลงในลำดับชั้นการแสดงผลไปยัง WebView
- ระยะห่างจากขอบที่ผิดพลาด: เนื่องจาก WebView ไม่ได้รับการอัปเดต จึงยังคงใช้ระยะห่างจากขอบจากสถานะก่อนหน้า ซึ่งทำให้เกิดระยะห่างจากขอบที่ผิดพลาด (เช่น การคงระยะห่างจากขอบของแป้นพิมพ์ไว้หลังจากซ่อนแป้นพิมพ์แล้ว)
แต่ให้ใช้ WindowInsetsCompat.Builder เพื่อตั้งค่าประเภทที่จัดการเป็น 0 ก่อนส่งออบเจ็กต์ไปยังมุมมองย่อยแทน ซึ่งจะแจ้งให้ WebView ทราบว่าได้พิจารณาค่าส่วนแทรกที่เฉพาะเจาะจงเหล่านั้นแล้ว ขณะเดียวกันก็เปิดใช้การแจ้งเตือนเพื่อดำเนินการต่อในลำดับชั้นการแสดงผล
Kotlin
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets -> // 1. Identify the inset types you want to handle natively val types = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() // 2. Extract the dimensions and apply them as padding to the native container val insets = windowInsets.getInsets(types) view.setPadding(insets.left, insets.top, insets.right, insets.bottom) // 3. Return a new WindowInsets object with the handled types set to NONE (zeroed). // This informs the WebView that these areas are already padded, preventing // double-padding while still allowing the WebView to update its internal state. WindowInsetsCompat.Builder(windowInsets) .setInsets(types, Insets.NONE) .build() }
Java
ViewCompat.setOnApplyWindowInsetsListener(rootView, (view, windowInsets) -> { // 1. Identify the inset types you want to handle natively int types = WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(); // 2. Extract the dimensions and apply them as padding to the native container Insets insets = windowInsets.getInsets(types); rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom); // 3. Return a new Insets object with the handled types set to NONE (zeroed). // This informs the WebView that these areas are already padded, preventing // double-padding while still allowing the WebView to update its internal // state. return new WindowInsetsCompat.Builder(windowInsets) .setInsets(types, Insets.NONE) .build(); });
วิธีการเลือกไม่ใช้
หากต้องการปิดใช้ลักษณะการทำงานสมัยใหม่เหล่านี้และกลับไปใช้การจัดการวิวพอร์ตแบบเดิม ให้ทำดังนี้
Intercept insets: ใช้
setOnApplyWindowInsetsListenerหรือลบล้างonApplyWindowInsetsในคลาสย่อยWebViewล้างส่วนแทรก: แสดงผลชุดส่วนแทรกที่ใช้ไปแล้ว (เช่น
WindowInsetsCompat.CONSUMED) ตั้งแต่เริ่มต้น การดำเนินการนี้จะป้องกันไม่ให้การแจ้งเตือนส่วนแทรกเผยแพร่ไปยัง WebView โดยสิ้นเชิง ซึ่งจะปิดใช้การปรับขนาดวิวพอร์ตสมัยใหม่อย่างมีประสิทธิภาพ และบังคับให้ WebView คงขนาดวิวพอร์ตภาพเริ่มต้นไว้