שליטה במקלדת התוכנה והנפשה

איך עושים את זה ב-Compose
‫Jetpack Compose היא ערכת הכלים המומלצת לפיתוח ממשק משתמש ב-Android. איך עובדים עם המקלדת ב-Compose.

באמצעות WindowInsetsCompat, האפליקציה יכולה לשלוח שאילתות למקלדת הווירטואלית (שנקראת גם IME) ולשלוט בה, בדומה לאופן שבו היא יוצרת אינטראקציה עם סרגלי המידע. האפליקציה יכולה גם להשתמש ב-WindowInsetsAnimationCompat כדי ליצור מעברים חלקים כשהמקלדת הווירטואלית נפתחת או נסגרת.

איור 1. שתי דוגמאות למעבר בין פתיחה לסגירה של המקלדת הווירטואלית.

דרישות מוקדמות

לפני שמגדירים את השליטה והאנימציה של המקלדת הווירטואלית, צריך להגדיר את האפליקציה כך שתוצג מקצה לקצה. כך הוא יכול לטפל בחלונות פנימיים של המערכת, כמו סרגלי המידע והמקלדת הווירטואלית.

אנימציית ברירת מחדל של הוספת פריטים מסונכרנים

החל מרמת API‏ 37.2, אפליקציות שמשתמשות בהתאמות של פריסת החלון שמוגדרות כברירת מחדל (כמו SOFT_INPUT_ADJUST_RESIZE או SOFT_INPUT_ADJUST_PAN כשמציגים את ה-IME באמצעות WindowInsetsController.show(WindowInsets.Type.ime())) בלי להטמיע WindowInsetsAnimationCompat.Callback בהתאמה אישית, יכולות ליהנות מאנימציות מסונכרנות ברמת המערכת, פריים אחר פריים. כשהתכונה הזו מופעלת, היא מחילה באופן אוטומטי את ה-insets על כל פריים ומפעילה מעבר פריסה, וכך יוצרת מעבר חלק כשהמקלדת זזה.

כדי להפעיל את ההתנהגות הזו, מוסיפים את התג <property> לפעילות או לאפליקציה בקובץ AndroidManifest.xml:

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

הפעלת אנימציות של הוספות מחוץ ל-thread הראשי

כברירת מחדל, רישום של WindowInsetsAnimationCompat.Callback אוכף את ההפעלה של האנימציה של ה-window inset ב-thread הראשי של האפליקציה. זה נחוץ אם האפליקציה מעדכנת ומסנכרנת כל הזמן את פריסת התצוגה שלה במהלך האנימציה (למשל בשיטה onProgress). עם זאת, אם האפליקציה צריכה רק להאזין למעברים במחזור החיים (למשל, כשה-IME מתחיל או מסיים את האנימציה) ולא צריכה לעדכן את התצוגות פריים אחר פריים, הפעלה ב-thread הראשי עלולה לגרום לתקורה מיותרת ולגמגום באנימציה – במיוחד אם ה-thread הראשי עסוק במשימות אחרות.

החל מרמת API‏ 37.2, אפשר להשתמש ב-ViewTreeObserver.WindowInsetsAnimationListener כדי לצפות במעברים האלה. מכיוון שהמאזין הזה לא מקבל עדכוני התקדמות של פריים אחר פריים (אין קריאה חוזרת (callback) של onProgress), המערכת יכולה להריץ את האנימציה של ה-inset ב-thread ייעודי של אנימציה במקום ב-thread הראשי, וכך המעבר חלק יותר.

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 (רמת API‏ 29), שבה שדה הטקסט ותוכן האפליקציה נצמדים למקום במקום להסתנכרן עם האנימציה של המקלדת – פעולה שיכולה להיות צורמת מבחינה חזותית.

  • ב-Android מגרסה 11 (רמת API‏ 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 נקראת כשאנימציית ה-inset מתחילה ולפני שהתצוגות מסודרות מחדש בגלל אנימציה. אפשר להשתמש בו כדי לשמור את מצב ההתחלה, שבמקרה הזה הוא הקואורדינטה התחתונה של התצוגה.

תמונה שבה מוצגת הקואורדינטה התחתונה של המצב ההתחלתי בתצוגת הבסיס.
איור 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 נקראת כשאנימציית ה-inset מתחילה. אפשר להשתמש בו כדי להגדיר את כל מאפייני התצוגה למצב הסופי של השינויים בפריסה. אם הגדרתם קריאה חוזרת (callback) מסוג 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 נקראת כשההגדרות של ה-inset משתנות כחלק מהפעלת אנימציה, כך שאפשר לשנות אותה ולקבל התראה על כל פריים במהלך האנימציה של המקלדת. מעדכנים את מאפייני התצוגה כדי שהתצוגה תונפש בסנכרון עם המקלדת.

בשלב הזה, כל השינויים בפריסה הושלמו. לדוגמה, אם משתמשים ב-View.translationY כדי להזיז את התצוגה, הערך יורד בהדרגה בכל קריאה ל-method הזה, ובסופו של דבר מגיע ל-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. המערכת קוראת ל-method הזו אחרי שהאנימציה מסתיימת. זה זמן טוב לנקות את כל השינויים הזמניים.

מקורות מידע נוספים