본 글에서는 EditText 입력 시 포커스와 키보드 표시를 더 자연스럽게 다루는 방법을 소개합니다.
🚧🚧🚧 포스팅에 사용된 내용은 해당 동작만 체크했습니다. 이외의 내용은 다루지 않습니다. 🚧🚧🚧
포스팅에 사용한 샘플 코드 : https://github.com/Pluu/EditTextWithContainerSample
앱 입력 화면에서는 EditText뿐만 아니라 관련 설명과 입력 길이 및 버튼 등 다른 View와 함께 안내하는 입력 폼 형태의 UI가 자주 사용됩니다. 그리고 이런 입력들은 EditText에만 맞춰서 노출되기보다는 그룹 형태로 노출되기를 원합니다.

처음 보여준 입력 폼처럼 ViewGroup 내부에 EditText와 다른 View를 위치한 경우, EditText 클릭 시에는 EditText에 딱 맞춰서 화면 이동과 키보드가 노출됩니다. 입력한 문자열 길이를 노출하는 경우에 사용자가 몇 글자 입력되었는지 알려주지 않는다면 좋은 UX가 아닐 것입니다.
<LinearLayout ...>
  <androidx.constraintlayout.widget.ConstraintLayout ...>
    <TextView .../>
    <EditText .../>
    <TextView .../>
  </androidx.constraintlayout.widget.ConstraintLayout>
  <LinearLayout ...>
    <EditText ... />
    <Button .../>
  </LinearLayout>
</LinearLayout>
입력 노출을 해결 방법으로 비슷한 UX가 Material Design에서 볼 수 있습니다. TextInputLayout 내부에 TextInputEditText를 배치하면, TextInputEditText 클릭 시 TextInputLayout 영역에 맞춰서 입력이 제어되고 있습니다.
Material Design ~ Text fields : https://m2.material.io/components/text-fields/android#using-text-fields
<LinearLayout ...>
  <com.google.android.material.textfield.TextInputLayout ...>
    <com.google.android.material.textfield.TextInputEditText .../>
  </com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
TextInputLayout에 OutlinedBox와 counter 적용한 후, TextInputEditText 선택 시 기대되는 영역(=TextInputLayout)만큼 이동/키보드 노출되고 있습니다.
추가로 TextInputLayout에 하단 여백으로 100dp를 준 경우에도 여백만큼 이동/키보드 노출하고 있습니다.
<LinearLayout ...>
  <!-- 하단 100dp 여백 준 경우 -->
  <com.google.android.material.textfield.TextInputLayout
    android:paddingBottom="100dp"
    ...>
    <com.google.android.material.textfield.TextInputEditText .../>
  </com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
이번 글에서는 TextInputLayout의 동작보다는 TextInputEditText가 핵심입니다. TextInputEditText 내부 코드를 살펴보면 일부 동작에서 TextInputLayout를 가져온 후 별도 처리를 하고 있습니다.
public class TextInputEditText extends AppCompatEditText {
  @Nullable
  private TextInputLayout getTextInputLayout() {
    // 부모 뷰로 이동하면서 TextInputLayout를 찾음 
    ViewParent parent = getParent();
    while (parent instanceof View) {
      if (parent instanceof TextInputLayout) {
        return (TextInputLayout) parent;
      }
      parent = parent.getParent();
    }
    return null;
  }
  
  @Override
  public void getFocusedRect(@Nullable Rect r) {
    super.getFocusedRect(r);
    TextInputLayout textInputLayout = getTextInputLayout();
    if (shouldUseTextInputLayoutFocusedRect(textInputLayout) && r != null) {
      textInputLayout.getFocusedRect(parentRect);
      r.bottom = parentRect.bottom;
    }
  }
  @Override
  public boolean getGlobalVisibleRect(@Nullable Rect r, @Nullable Point globalOffset) {
    TextInputLayout textInputLayout = getTextInputLayout();
    return shouldUseTextInputLayoutFocusedRect(textInputLayout)
        ? textInputLayout.getGlobalVisibleRect(r, globalOffset)
        : super.getGlobalVisibleRect(r, globalOffset);
  }
  @Override
  public boolean requestRectangleOnScreen(@Nullable Rect rectangle) {
    TextInputLayout textInputLayout = getTextInputLayout();
    if (shouldUseTextInputLayoutFocusedRect(textInputLayout) && rectangle != null) {
      int bottomOffset = textInputLayout.getHeight() - getHeight();
      parentRect.set(
          rectangle.left,
          rectangle.top,
          rectangle.right,
          rectangle.bottom + bottomOffset);
      return super.requestRectangleOnScreen(parentRect);
    } else {
      return super.requestRectangleOnScreen(rectangle);
    }
  }
}
소스 출처 : https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/textfield/TextInputEditText.java
대부분 흔하게 재정의하지 않지만, TextInputLayout와의 상호작용으로 3개의 함수를 재정하고 있습니다.
이 3개의 함수만 있으면 원하는 형태로 노출할 수 있습니다.
먼저 개선한 영상을 보겠습니다.
<LinearLayout ...>
  <com.pluu.view.edittext.EditTextWithContainer .../>
  <Button ... />
</LinearLayout>
TextInputEditText에서는 TextInputLayout가 필요했지만, 별도로 ViewGroup대신 기존 ViewGroup을 그대로 쓸 수 있도록 우회하는 방식을 채택했습니다. 그로 인해 포커스 영역을 지정할 별도 ViewGroup을 받을 수 있는 변수와 Setter를 추가했습니다.
class EditTextWithContainer @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = android.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {
    private val parentRect = Rect()
    // EditText 대신 별도로 포커스/키보드 노출에 사용할 Parent View
    private var parentView: WeakReference<View>? = null
    private fun getContainer(): View? = parentView?.get()
  
    ///////////////////////////////////////////////////////////////////////////
    // 아래 재정의한 함수는 TextInputEditText와 유사하게 코드 수정
    ///////////////////////////////////////////////////////////////////////////
    override fun getFocusedRect(r: Rect?) {
        super.getFocusedRect(r)
        val container = getContainer() ?: return
        if (r != null) {
            container.getFocusedRect(parentRect)
            r.bottom = parentRect.bottom
        }
    }
    override fun getGlobalVisibleRect(r: Rect?, globalOffset: Point?): Boolean {
        val container = getContainer()
        return container?.getGlobalVisibleRect(r, globalOffset)
            ?: super.getGlobalVisibleRect(r, globalOffset)
    }
    override fun requestRectangleOnScreen(rectangle: Rect?): Boolean {
        val container = getContainer()
        return if (container != null && rectangle != null) {
            val bottomOffset = container.height - height
            parentRect.set(
                rectangle.left,
                rectangle.top,
                rectangle.right,
                rectangle.bottom + bottomOffset
            )
            super.requestRectangleOnScreen(parentRect)
        } else {
            super.requestRectangleOnScreen(rectangle)
        }
    }
    fun setParentContainer(view: View) {
        // 외부로 주입된 View를 내부에 임시 저장하므로
        // 주입된 View가 변동으로 크래시 및 메모리 누수가 발생하지 않도록
        // 약한 참조로 View 처리
        this.parentView = WeakReference(view)
    }
}
EditText에 포커스 용도로 처리할 별도 뷰를 주입만 하면 할 일은 끝났습니다. 실행 후 EditText 선택 시 원하는 입력 폼이 유지되면서 이동/키보드 노출되는 것을 확인할 수 있습니다.
class MainActivity : AppCompatActivity() {
  private lateinit var binding: ActivityMainBinding
  override fun onCreate(savedInstanceState: Bundle?) {
    ...
    binding.editTextWithContainer.setParentContainer(binding.container)
  }
}
EditText를 자주 사용하지만, 입력 시 아쉬운 키보드와 UI 노출을 개선은 여전히 어려운 영역입니다. 본 글에서는 다루지 않은 접근성, 포커스 이동 등에 대해 테스트는 해보지 않았으므로 해당 부분에 대해서는 이해해주시길 바랍니다. 위 내용이 다른 분들에게도 도움이 되었으면 합니다.
Subscribe to this blog via RSS.
[발표자료] Google I/O Extended Incheon 2025 ~ What's new in Android development tools
Posted on 16 Aug 2025