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

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

ความเข้ากันได้ของฟีเจอร์

การรองรับส่วนแทรกหน้าต่างของ WebView มีการพัฒนามาเรื่อยๆ เพื่อให้ลักษณะการทำงานของเนื้อหาเว็บสอดคล้องกับความคาดหวังของแอป Android เนทีฟ ดังนี้

Milestone เพิ่มฟีเจอร์แล้ว ขอบเขต
M136 displayCutout() และรองรับ systemBars() ผ่าน CSS safe-area-insets เฉพาะ WebView แบบเต็มหน้าจอ
M139 ime() (ตัวแก้ไขวิธีการป้อนข้อมูล ซึ่งเป็นแป้นพิมพ์) ผ่านการปรับขนาดวิวพอร์ตด้วยภาพ WebView ทั้งหมด
M144 displayCutout() และsystemBars() WebView ทั้งหมด (ไม่ว่าจะอยู่ในสถานะเต็มหน้าจอหรือไม่ก็ตาม)

ดูข้อมูลเพิ่มเติมได้ที่ WindowInsetsCompat

กลไกหลัก

WebView จัดการ Inset ผ่านกลไกหลัก 2 อย่าง ดังนี้

  • พื้นที่ปลอดภัย (displayCutout, systemBars): WebView จะส่งต่อมิติข้อมูลเหล่านี้ไปยังเนื้อหาเว็บผ่านตัวแปร CSS safe-area-inset-* ซึ่งจะช่วยให้นักพัฒนาแอปป้องกันไม่ให้รอยบากหรือแถบสถานะบดบังองค์ประกอบแบบอินเทอร์แอกทีฟของตนเอง (เช่น แถบนำทาง)

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

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

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

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

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

ใช้การจัดการระยะขอบ

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

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

แนวทางการตั้งค่าเป็น 0

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

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

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

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

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

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

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

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

  1. Intercept insets: ใช้ setOnApplyWindowInsetsListener หรือลบล้าง onApplyWindowInsets ในคลาสย่อย WebView

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