소프트웨어 키보드 제어 및 애니메이션 처리

Compose 방식으로 시도
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 키보드를 사용하는 방법을 알아보세요.

WindowInsetsCompat를 사용하면 앱이 시스템 표시줄과 상호작용하는 방식과 유사하게 터치 키보드 (IME라고도 함)를 쿼리하고 제어할 수 있습니다. 앱은 WindowInsetsAnimationCompat을 사용하여 소프트 키보드가 열리거나 닫힐 때 원활한 전환을 만들 수도 있습니다.

그림 1. 소프트웨어 키보드 열기-닫기 전환의 두 가지 예

기본 요건

소프트웨어 키보드의 제어 및 애니메이션을 설정하기 전에 앱이 더 넓은 화면을 표시하도록 구성하세요. 이렇게 하면 시스템 바 및 온스크린 키보드와 같은 시스템 창 인셋을 처리할 수 있습니다.

키보드 소프트웨어 공개 상태 확인

WindowInsets을 사용하여 소프트웨어 키보드 공개 상태를 확인합니다.

Kotlin

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

자바

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
}

자바

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.
  }
)

자바

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()
}

자바

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
}

자바

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
}

자바

@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를 재정의할 수 있습니다. 이 메서드는 애니메이션이 끝난 후 호출됩니다. 이때 임시 변경사항을 정리하는 것이 좋습니다.

추가 리소스