فهم حواف النوافذ في WebView

يدير WebView محاذاة المحتوى باستخدام إطارَي عرض: إطار عرض التنسيق (حجم الصفحة) وإطار العرض المرئي (جزء الصفحة الذي يراه المستخدم فعليًا). مع أنّ إطار عرض التصميم ثابت بشكل عام، يتغيّر إطار العرض المرئي بشكل ديناميكي عندما يكبّر المستخدمون أو يصغّرون أو يتصفّحون أو عندما تظهر عناصر واجهة مستخدم النظام (مثل لوحة المفاتيح البرمجية).

توافق الميزات

تطوّر دعم WebView لعمليات إدراج النوافذ بمرور الوقت بهدف مواءمة سلوك محتوى الويب مع توقعات تطبيقات Android الأصلية:

الإجراء تمّت إضافة الميزة. النطاق
M136 displayCutout() وsystemBars() من خلال safe-area-insets في CSS تتوفّر فقط في WebViews بملء الشاشة.
M139 ime() (محرّر أسلوب الإدخال، وهو لوحة مفاتيح) من خلال تغيير حجم إطار العرض المرئي. جميع عناصر WebView
M144 displayCutout() وsystemBars(). جميع عناصر WebView (بغض النظر عن حالة ملء الشاشة)

لمزيد من المعلومات، يُرجى الاطّلاع على WindowInsetsCompat.

الميكانيكا الأساسية

تتعامل WebView مع الحواف الداخلية من خلال آليتَين أساسيتَين:

  • المساحات الآمنة (displayCutout وsystemBars): يعيد WebView توجيه هذه الأبعاد إلى محتوى الويب من خلال متغيرات CSS safe-area-inset-‎*. يتيح ذلك للمطوّرين منع حجب عناصرهم التفاعلية (مثل أشرطة التنقّل) بسبب النتوءات أو أشرطة الحالة.

  • تغيير حجم إطار العرض المرئي باستخدام محرّر أسلوب الإدخال (IME): اعتبارًا من الإصدار M139، يغيّر محرّر أسلوب الإدخال (IME) حجم إطار العرض المرئي مباشرةً. تستند آلية تغيير الحجم هذه أيضًا إلى تقاطع WebView-Window. على سبيل المثال، في وضع تعدد المهام على Android، إذا كان الجزء السفلي من WebView يمتد بمقدار 200 وحدة بكسل مستقلة عن الكثافة (dp) أسفل الجزء السفلي من النافذة، سيكون إطار العرض المرئي أصغر بمقدار 200 وحدة بكسل مستقلة عن الكثافة (dp) من حجم WebView. لا يتم تطبيق تغيير حجم إطار العرض المرئي هذا (لكلّ من IME وتقاطع WebView مع النافذة) إلا على أسفل WebView. لا تتيح هذه الآلية تغيير الحجم في حال التداخل من اليسار أو اليمين أو الأعلى. وهذا يعني أنّ لوحات المفاتيح المثبّتة التي تظهر على هذه الحواف لا تؤدي إلى تغيير حجم إطار العرض المرئي.

في السابق، كان إطار العرض المرئي يظل ثابتًا، ما كان يؤدي غالبًا إلى إخفاء حقول الإدخال خلف لوحة المفاتيح. من خلال تغيير حجم إطار العرض، يصبح الجزء المرئي من الصفحة قابلاً للتمرير تلقائيًا، ما يضمن وصول المستخدمين إلى المحتوى المخفي.

الحدود ومنطق التداخل

يجب أن تتلقّى WebView قيم إزاحة غير صفرية فقط عندما تتداخل عناصر واجهة مستخدم النظام (الأشرطة أو الفتحات في الشاشة أو لوحة المفاتيح) بشكل مباشر مع حدود شاشة WebView. إذا لم يتداخل WebView مع عناصر واجهة المستخدم هذه (على سبيل المثال، إذا كان WebView في منتصف الشاشة ولا يلامس أشرطة النظام)، يجب أن يتلقّى هذه الإزاحات كقيمة صفر.

لتجاوز هذه المنطق التلقائي وتزويد محتوى الويب بأبعاد النظام الكاملة بغض النظر عن التداخل، استخدِم طريقة setOnApplyWindowInsetsListener وأرجِع عنصر windowInsets الأصلي غير المعدَّل من أداة المعالجة. يمكن أن يساعد توفير أبعاد النظام الكاملة في ضمان اتساق التصميم من خلال السماح بمحاذاة محتوى الويب مع أجهزة الجهاز بغض النظر عن الموضع الحالي لمكوّن 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. يتم إخفاء لوحة المفاتيح لأنّ التركيز قد تم فقده.

للتخفيف من حدة هذا السلوك، راجِع أدوات الاستماع على الويب للتأكّد من أنّ تغييرات إطار العرض لا تؤدي عن غير قصد إلى تشغيل وظيفة blur() JavaScript أو سلوكيات محو التركيز.

تنفيذ معالجة الإزاحة

تعمل إعدادات WebView التلقائية تلقائيًا مع معظم التطبيقات. ومع ذلك، إذا كان تطبيقك يستخدم تنسيقات مخصّصة (على سبيل المثال، إذا أضفت مساحة متروكة خاصة بك لاحتساب شريط الحالة أو لوحة المفاتيح)، يمكنك استخدام الطرق التالية لتحسين طريقة عمل محتوى الويب وواجهة المستخدم الأصلية معًا. إذا كانت واجهة المستخدم الأصلية تضيف مساحة متروكة إلى حاوية استنادًا إلى WindowInsets، عليك إدارة هذه المساحات المتروكة بشكل صحيح قبل وصولها إلى WebView لتجنُّب إضافة مساحة متروكة مضاعفة.

يشير مصطلح "الحشو المزدوج" إلى الحالة التي يطبّق فيها كل من التنسيق الأصلي ومحتوى الويب أبعاد المسافة البادئة نفسها، ما يؤدي إلى تكرار المسافات. على سبيل المثال، تخيَّل هاتفًا بشريط حالة يبلغ عرضه 40 بكسل. يظهر كلّ من العرض الأصلي وWebView مع مساحة فارغة بحجم 40 بكسل. يضيف كل منهما مساحة متروكة بحجم 40 بكسل، ما يؤدي إلى ظهور فجوة بحجم 80 بكسل للمستخدم في أعلى الصفحة.

نهج التصفير

لمنع إضافة مساحة فارغة مضاعفة، عليك التأكّد من أنّه بعد استخدام عرض أصلي لبُعد إدراج من أجل إضافة مساحة فارغة، عليك إعادة ضبط هذا البُعد على صفر باستخدام Insets.NONE في عنصر WindowInsets جديد قبل تمرير العنصر المعدَّل إلى أسفل العرض الهرمي إلى WebView.

عند تطبيق مساحة متروكة على طريقة عرض رئيسية، يجب بشكل عام استخدام طريقة التصفير من خلال ضبط Insets.NONE بدلاً من WindowInsetsCompat.CONSUMED. قد يعمل الزر WindowInsetsCompat.CONSUMED في حالات معيّنة. ومع ذلك، يمكن أن تحدث مشاكل إذا غيّر معالج تطبيقك الحواف الداخلية أو أضاف مساحة متروكة خاصة به. لا يتضمّن نهج التصفير هذه القيود.

تجنُّب المساحة الفارغة غير المرئية من خلال ضبط قيمة الهوامش الداخلية على صفر

إذا كنت تستخدم الحواف الداخلية عندما يكون التطبيق قد مرّر سابقًا حواف داخلية غير مستخدَمة، أو إذا تغيرت الحواف الداخلية (مثل إخفاء لوحة المفاتيح)، سيؤدي استخدامها إلى منع WebView من تلقّي إشعار التحديث اللازم. ويمكن أن يؤدي ذلك إلى احتفاظ WebView بمساحة متروكة وهمية من حالة سابقة (على سبيل المثال، الاحتفاظ بمساحة متروكة للوحة المفاتيح بعد إخفائها).

يوضّح المثال التالي تفاعلاً غير سليم بين التطبيق وWebView:

  1. الحالة الأولية: يمرّر التطبيق في البداية هوامش داخلية غير مستهلكة (على سبيل المثال، displayCutout() أو systemBars()) إلى WebView، الذي يضيف داخليًا مسافة بادئة إلى محتوى الويب.
  2. تغيير الحالة والخطأ: إذا غيّر التطبيق الحالة (على سبيل المثال، تم إخفاء لوحة المفاتيح) واختار التطبيق معالجة الحواف الناتجة من خلال عرض WindowInsetsCompat.CONSUMED.
  3. تم حظر الإشعار: يؤدي استخدام الحواف إلى منع نظام التشغيل Android من إرسال إشعار التحديث اللازم إلى أسفل تسلسل عرض WebView.
  4. المساحة الفارغة: بما أنّ WebView لا يتلقّى التحديث، يحتفظ بالمساحة الفارغة من الحالة السابقة، ما يؤدي إلى ظهور مساحة فارغة (على سبيل المثال، الاحتفاظ بالمساحة الفارغة الخاصة بلوحة المفاتيح بعد إخفائها).

بدلاً من ذلك، استخدِم WindowInsetsCompat.Builder لضبط الأنواع التي يتم التعامل معها على صفر قبل تمرير العنصر إلى طرق العرض الفرعية. يُعلم ذلك 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. Clear insets: لعرض مجموعة مستهلكة من الإضافات (على سبيل المثال، WindowInsetsCompat.CONSUMED) من البداية. يمنع هذا الإجراء إرسال الإشعار المضمّن إلى WebView بشكل كامل، ما يؤدي إلى إيقاف ميزة تغيير حجم إطار العرض الحديثة وإجبار WebView على الاحتفاظ بحجم إطار العرض المرئي الأولي.