التحكّم في لوحة مفاتيح البرنامج وتحريكها

تجربة طريقة Compose
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدم المقترَحة لنظام التشغيل Android. تعرَّف على كيفية استخدام لوحة المفاتيح في Compose.

باستخدام WindowInsetsCompat، يمكن لتطبيقك الاستعلام عن لوحة مفاتيح على الشاشة والتحكّم فيها (تُعرف أيضًا باسم أداة IME) بطريقة مشابهة لطريقة تفاعله مع أشرطة النظام. يمكن لتطبيقك أيضًا استخدام WindowInsetsAnimationCompat لإنشاء انتقالات سلسة عند فتح لوحة المفاتيح الافتراضية أو إغلاقها.

الشكل 1: مثالان على انتقال لوحة المفاتيح الافتراضية من الحالة المفتوحة إلى المغلقة

المتطلبات الأساسية

قبل إعداد عناصر التحكّم والحركة في لوحة المفاتيح الافتراضية، اضبط تطبيقك على العرض على كامل الشاشة. يتيح ذلك له التعامل مع مساحات العرض المتاحة في نافذة النظام، مثل أشرطة النظام ولوحة المفاتيح على الشاشة.

صورة متحركة تلقائية متزامنة مع العناصر المضمّنة

بدءًا من المستوى 37.2 من واجهة برمجة التطبيقات والإصدارات الأحدث، يمكن للتطبيقات التي تستخدم تعديلات تنسيق النافذة التلقائية (مثل SOFT_INPUT_ADJUST_RESIZE أو SOFT_INPUT_ADJUST_PAN عند عرض "محرِّر أسلوب الإدخال" باستخدام WindowInsetsController.show(WindowInsets.Type.ime())) بدون تنفيذ WindowInsetsAnimationCompat.Callback مخصّص الاستفادة من الصور المتحركة المتزامنة على مستوى النظام لكل لقطة. عند تفعيل هذه الميزة، يتم تطبيق الإطارات الداخلية تلقائيًا على كل إطار ويتم تفعيل عملية تخطيط، ما يؤدي إلى إنشاء انتقال سلس أثناء تحرّك لوحة المفاتيح.

لتفعيل هذا السلوك، أضِف علامة <property> التالية إلى نشاطك أو تطبيقك في ملف AndroidManifest.xml:

<property
    android:name="android.window.PROPERTY_COMPAT_ALLOW_SYNCHRONIZED_INSETS_ANIMATION"
    android:value="true" />

تنفيذ صور متحركة للإطارات خارج سلسلة التعليمات الرئيسية

بشكل تلقائي، يؤدي تسجيل WindowInsetsAnimationCompat.Callback إلى فرض تشغيل الرسوم المتحركة لإدراج النافذة في سلسلة التعليمات الرئيسية للتطبيق. هذا الإجراء ضروري إذا كان تطبيقك يحدّث باستمرار تنسيق العرض الخاص به ويُزامنه أثناء عرض الصور المتحركة (كما هو الحال في طريقة onProgress). ومع ذلك، إذا كان تطبيقك يحتاج فقط إلى الاستماع إلى انتقالات مراحل النشاط (مثل عندما يبدأ محرِّر أسلوب الإدخال أو ينهي الحركة) ولا يحتاج إلى تعديل إطارات العرض، فإنّ التشغيل على سلسلة التعليمات الرئيسية يمكن أن يتسبب في زيادة غير ضرورية في الموارد وتلعثم في الحركة، خاصةً إذا كانت سلسلة التعليمات الرئيسية مشغولة بخلاف ذلك.

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

Kotlin

val listener = object : ViewTreeObserver.WindowInsetsAnimationListener {
  override fun onPrepare(animation: WindowInsetsAnimation) {
    // Handle preparation before animation starts
  }

  override fun onEnd(animation: WindowInsetsAnimation) {
    // Clean up temporary changes
  }
}

// Add the listener
view.viewTreeObserver.addWindowInsetsAnimationListener(listener)

// Remove the listener when no longer needed
view.viewTreeObserver.removeWindowInsetsAnimationListener(listener)

Java

ViewTreeObserver.WindowInsetsAnimationListener listener =
    new ViewTreeObserver.WindowInsetsAnimationListener() {
      @Override
      public void onPrepare(@NonNull WindowInsetsAnimation animation) {
        // Handle preparation before animation starts
      }

      @Override
      public void onEnd(@NonNull WindowInsetsAnimation animation) {
        // Clean up temporary changes
      }
    };

// Add the listener
view.getViewTreeObserver().addWindowInsetsAnimationListener(listener);

// Remove the listener when no longer needed
view.getViewTreeObserver().removeWindowInsetsAnimationListener(listener);

التحقّق من ظهور برنامج لوحة المفاتيح

استخدِم WindowInsets للتحقّق من ظهور لوحة المفاتيح الافتراضية.

Kotlin

val insets = ViewCompat.getRootWindowInsets(view) ?: return
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom

Java

WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;

يمكنك بدلاً من ذلك استخدام ViewCompat.setOnApplyWindowInsetsListener لمراقبة التغييرات في مستوى ظهور لوحة المفاتيح الافتراضية.

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
  val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
  val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
  insets
}

Java

ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
  boolean imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
  int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
  return insets;
});

مزامنة الصور المتحركة مع لوحة المفاتيح الافتراضية

عندما ينقر المستخدم على حقل إدخال نص، تنزلق لوحة المفاتيح إلى مكانها من أسفل الشاشة، كما هو موضّح في المثال التالي:

الشكل 2. صورة متحركة للوحة مفاتيح متزامنة
  • يعرض المثال الذي يحمل التصنيف "غير متزامن" في الشكل 2 الإعداد التلقائي في Android 10 (المستوى 29 لواجهة برمجة التطبيقات)، حيث يتم تثبيت حقل النص ومحتوى التطبيق في مكانهما بدلاً من المزامنة مع الرسوم المتحركة للوحة المفاتيح، وهو سلوك قد يكون مزعجًا من الناحية المرئية.

  • في الإصدار 11 من نظام التشغيل Android (المستوى 30 لواجهة برمجة التطبيقات) والإصدارات الأحدث، يمكنك استخدام WindowInsetsAnimationCompat لمزامنة انتقال التطبيق مع لوحة المفاتيح التي تنزلق للأعلى وللأسفل من أسفل الشاشة. يبدو هذا أكثر سلاسة، كما هو موضح في المثال المسمى "متزامن" في الشكل 2.

اضبط WindowInsetsAnimationCompat.Callback مع العرض لتتم مزامنته مع حركة لوحة المفاتيح.

Kotlin

ViewCompat.setWindowInsetsAnimationCallback(
  view,
  object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
    // Override methods.
  }
)

Java

ViewCompat.setWindowInsetsAnimationCallback(
    view,
    new WindowInsetsAnimationCompat.Callback(
        WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
    ) {
      // Override methods.
    });

هناك عدة طرق للتجاوز في WindowInsetsAnimationCompat.Callback، وهي onPrepare() وonStart() وonProgress() وonEnd(). ابدأ باستدعاء onPrepare() قبل أي تغييرات في التنسيق.

يتم استدعاء onPrepare عند بدء حركة العناصر الداخلية وقبل إعادة تخطيط طرق العرض بسبب الحركة. يمكنك استخدامه لحفظ حالة البدء، والتي في هذه الحالة هي الإحداثي السفلي للعرض.

صورة تعرض إحداثيات الجزء السفلي من الحالة الأولية للعرض الجذري
الشكل 3 يتم استخدام onPrepare() لتسجيل حالة البدء.

يعرض المقتطف التالي نموذجًا لطلب onPrepare:

Kotlin

var startBottom = 0f

override fun onPrepare(
  animation: WindowInsetsAnimationCompat
) {
  startBottom = view.bottom.toFloat()
}

Java

float startBottom;

@Override
public void onPrepare(
    @NonNull WindowInsetsAnimationCompat animation
) {
  startBottom = view.getBottom();
}

يتم استدعاء onStart عند بدء حركة الهوامش. يمكنك استخدامها لضبط جميع خصائص العرض على الحالة النهائية لتغييرات التخطيط. إذا كان لديك استدعاء OnApplyWindowInsetsListener تم ضبطه على أي من طرق العرض، فسيتم استدعاؤه بالفعل في هذه المرحلة. هذا هو الوقت المناسب لحفظ الحالة النهائية لخصائص العرض.

صورة تعرض إحداثيات الجزء السفلي من الحالة النهائية للعرض
الشكل 4. استخدام onStart() لتسجيل الحالة النهائية

يعرض المقتطف التالي نموذجًا لطلب onStart:

Kotlin

var endBottom = 0f

override fun onStart(
  animation: WindowInsetsAnimationCompat,
  bounds: WindowInsetsAnimationCompat.BoundsCompat
): WindowInsetsAnimationCompat.BoundsCompat {
  // Record the position of the view after the IME transition.
  endBottom = view.bottom.toFloat()

  return bounds
}

Java

float endBottom;

@NonNull
@Override
public WindowInsetsAnimationCompat.BoundsCompat onStart(
    @NonNull WindowInsetsAnimationCompat animation,
    @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds
) {
  endBottom = view.getBottom();
  return bounds;
}

يتم استدعاء onProgress عند تغيير الإطارات الداخلية كجزء من تشغيل صورة متحركة، لذا يمكنك إلغاؤها وسيتم إعلامك في كل إطار أثناء الصورة المتحركة للوحة المفاتيح. عدِّل خصائص العرض بحيث يتم عرض الصورة المتحركة بالتزامن مع لوحة المفاتيح.

تم الانتهاء من جميع تغييرات التخطيط في هذه المرحلة. على سبيل المثال، إذا كنت تستخدم View.translationY لتغيير العرض، ستنخفض القيمة تدريجيًا لكل استدعاء لهذه الطريقة وتصل في النهاية إلى 0 إلى موضع التنسيق الأصلي.

الشكل 5. باستخدام onProgress() لمزامنة الصور المتحركة

يعرض المقتطف التالي نموذجًا لطلب onProgress:

Kotlin

override fun onProgress(
  insets: WindowInsetsCompat,
  runningAnimations: MutableList<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
  // Find an IME animation.
  val imeAnimation = runningAnimations.find {
    it.typeMask and WindowInsetsCompat.Type.ime() != 0
  } ?: return insets

  // Offset the view based on the interpolated fraction of the IME animation.
  view.translationY =
    (startBottom - endBottom) * (1 - imeAnimation.interpolatedFraction)

  return insets
}

Java

@NonNull
@Override
public WindowInsetsCompat onProgress(
    @NonNull WindowInsetsCompat insets,
    @NonNull List<WindowInsetsAnimationCompat> runningAnimations
) {
  // Find an IME animation.
  WindowInsetsAnimationCompat imeAnimation = null;
  for (WindowInsetsAnimationCompat animation : runningAnimations) {
    if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {
      imeAnimation = animation;
      break;
    }
  }
  if (imeAnimation != null) {
    // Offset the view based on the interpolated fraction of the IME animation.
    view.setTranslationY((startBottom - endBottom)

        *   (1 - imeAnimation.getInterpolatedFraction()));
  }
  return insets;
}

يمكنك اختياريًا إلغاء onEnd. يتم استدعاء هذه الطريقة بعد انتهاء الرسوم المتحركة. هذا هو الوقت المناسب لإزالة أي تغييرات مؤقتة.

مراجع إضافية