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

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

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

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

기본 요건

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

기본 동기화된 인셋 애니메이션

API 수준 37.2 이상부터는 맞춤 WindowInsetsAnimationCompat.Callback를 구현하지 않고 기본 창 레이아웃 조정 (예: WindowInsetsController.show(WindowInsets.Type.ime())를 사용하여 IME를 표시할 때 SOFT_INPUT_ADJUST_RESIZE 또는 SOFT_INPUT_ADJUST_PAN)을 사용하는 애플리케이션은 시스템 수준의 동기화된 프레임별 애니메이션을 활용할 수 있습니다. 이 기능을 사용 설정하면 모든 프레임에 인셋이 자동으로 적용되고 레이아웃 패스가 트리거되어 키보드가 움직일 때 부드러운 전환이 생성됩니다.

이 동작을 사용 설정하려면 AndroidManifest.xml 파일의 활동 또는 애플리케이션에 다음 <property> 태그를 추가하세요.

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

기본 스레드 외부에서 인셋 애니메이션 실행

기본적으로 WindowInsetsAnimationCompat.Callback를 등록하면 창 인셋 애니메이션이 앱의 기본 스레드에서 실행됩니다. 이는 앱이 애니메이션 중에 뷰 레이아웃을 지속적으로 업데이트하고 동기화하는 경우 (예: onProgress 메서드)에 필요합니다. 하지만 앱이 수명 주기 전환 (예: IME가 애니메이션을 시작하거나 종료하는 경우)만 수신 대기하면 되고 프레임별로 뷰를 업데이트할 필요가 없는 경우 기본 스레드에서 실행하면 불필요한 오버헤드와 애니메이션 끊김이 발생할 수 있습니다. 특히 기본 스레드가 다른 작업으로 바쁜 경우에 그렇습니다.

API 수준 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)

자바

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

자바

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

추가 리소스