ทำความเข้าใจส่วนที่เว้นไว้ในหน้าต่างใน WebView

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 กับหน้าต่างด้วย ตัวอย่างเช่น ในโหมดมัลติทาสก์ของ Android หากด้านล่างของ WebView ยื่นออกไป 200dp ใต้ด้านล่างของหน้าต่าง วิวพอร์ตภาพจะมีขนาดเล็กกว่า WebView 200dp การปรับขนาดวิวพอร์ตภาพนี้ (ทั้งสำหรับ IME และการตัดกันของ WebView กับหน้าต่าง) จะมีผลกับด้านล่างของ 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;
});

จัดการเหตุการณ์การปรับขนาด

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

  1. ผู้ใช้โฟกัสที่องค์ประกอบอินพุต
  2. แป้นพิมพ์ปรากฏขึ้น ซึ่งจะทริกเกอร์เหตุการณ์การปรับขนาด
  3. โค้ดของเว็บไซต์จะล้างโฟกัสเพื่อตอบสนองต่อการปรับขนาด
  4. แป้นพิมพ์ซ่อนอยู่เนื่องจากสูญเสียโฟกัส

หากต้องการลดลักษณะการทำงานนี้ ให้ตรวจสอบ Listener ฝั่งเว็บเพื่อให้แน่ใจว่าการเปลี่ยนแปลงวิวพอร์ตไม่ได้ทริกเกอร์ฟังก์ชัน JavaScript blur() หรือลักษณะการทำงานในการล้างโฟกัสโดยไม่ตั้งใจ

ใช้การจัดการส่วนแทรก

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

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

วิธีการ การตั้งค่าเป็น 0

หากต้องการป้องกันการเพิ่มระยะห่างจากขอบ 2 เท่า คุณต้องตรวจสอบว่าหลังจากมุมมองที่มาพร้อมเครื่องใช้ขนาดส่วนที่เว้นไว้สำหรับระยะห่างจากขอบแล้ว ให้รีเซ็ตขนาดนั้นเป็น 0 โดยใช้ Insets.NONE ในออบเจ็กต์ WindowInsets ใหม่ก่อนที่จะส่งออบเจ็กต์ที่แก้ไขแล้วลงไปตามลำดับชั้นการแสดงผลไปยัง WebView

เมื่อเพิ่มระยะห่างให้กับมุมมองระดับบน คุณควรใช้วิธีการตั้งค่าเป็น 0 โดยตั้งค่า Insets.NONE แทน WindowInsetsCompat.CONSUMED โดยทั่วไป การส่งคืน WindowInsetsCompat.CONSUMED อาจใช้ได้ในบางสถานการณ์ อย่างไรก็ตาม อาจเกิดปัญหาขึ้นหากตัวแฮนเดิลของแอปเปลี่ยนส่วนที่เว้นไว้หรือเพิ่มระยะห่างจากขอบของตัวเอง วิธีการตั้งค่าเป็น 0 ไม่มีข้อจำกัดเหล่านี้

หลีกเลี่ยงระยะห่างที่มองไม่เห็นโดยการตั้งค่าส่วนแทรกเป็น 0

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

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

  1. สถานะเริ่มต้น: แอปจะส่งส่วนแทรกที่ไม่ได้ใช้ (เช่น displayCutout() หรือ systemBars()) ไปยัง WebView ในตอนแรก ซึ่งจะเพิ่มระยะห่างให้กับเนื้อหาเว็บภายใน
  2. การเปลี่ยนแปลงสถานะและข้อผิดพลาด: หากแอปเปลี่ยนสถานะ (เช่น แป้นพิมพ์ซ่อนอยู่) และแอปเลือกที่จะจัดการส่วนแทรกที่ได้โดยการส่งคืน WindowInsetsCompat.CONSUMED
  3. การแจ้งเตือนถูกบล็อก: การใช้ส่วนที่เว้นไว้จะป้องกันไม่ให้ระบบ Android ส่งการแจ้งเตือนการอัปเดตที่จำเป็นลงไปตามลำดับชั้นการแสดงผลไปยัง WebView
  4. ระยะห่างที่มองไม่เห็น: เนื่องจาก 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();
});

วิธีการเลือกไม่ใช้

หากต้องการปิดใช้ลักษณะการทำงานที่ทันสมัยเหล่านี้และกลับไปใช้การจัดการวิวพอร์ตแบบเดิม ให้ทำดังนี้

  1. ดักจับส่วนแทรก: ใช้ setOnApplyWindowInsetsListener หรือลบล้าง onApplyWindowInsets ในคลาสย่อย WebView

  2. ล้างส่วนแทรก: ส่งคืนชุดส่วนแทรกที่ใช้แล้ว (เช่น WindowInsetsCompat.CONSUMED) ตั้งแต่เริ่มต้น การดำเนินการนี้จะป้องกันไม่ให้การแจ้งเตือนส่วนแทรกแพร่ไปยัง WebView ทั้งหมด ซึ่งจะปิดใช้การปรับขนาดวิวพอร์ตที่ทันสมัยอย่างมีประสิทธิภาพและบังคับให้ WebView เก็บขนาดวิวพอร์ตภาพเริ่มต้นไว้