입력 이벤트 개요

Compose 방법 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 터치 및 입력을 사용하는 방법을 알아보세요.

Android에는 사용자와 애플리케이션의 상호작용으로부터 이벤트를 가로채는 방법이 여러 가지 있습니다. 사용자 인터페이스 내의 이벤트를 고려할 경우, 그 접근 방법은 해당 이벤트를 사용자가 상호작용하는 특정 View 객체로부터 캡처하는 것입니다. 이에 필요한 수단은 View 클래스가 제공합니다.

레이아웃을 작성하는 데 사용하는 여러 가지 View 클래스 내에는 UI 이벤트에 유용해 보이는 공개 콜백 메서드가 여러 개 있습니다. 이러한 메서드는 해당 객체에서 각각의 작업이 발생할 때 Android 프레임워크에 의해 호출됩니다. 예를 들어, View(예: 버튼)를 하나 터치하면 해당 객체에서 onTouchEvent() 메서드가 호출됩니다. 그러나 이것을 가로채려면 클래스를 확장하고 메서드를 재정의해야 합니다. 다만 그런 이벤트를 처리하기 위해 모든 View 객체를 확장하는 것은 유용하지 않습니다. 이 때문에 View 클래스에도 일련의 중첩된 인터페이스가 있고 거기에 훨씬 쉽게 정의할 수 있는 콜백이 있습니다. 이와 같은 인터페이스를 이벤트 리스너라고 하며, UI와의 사용자 상호작용을 캡처하는 데 적합합니다.

사용자 상호작용을 수신 대기하는 데는 이벤트 리스너를 사용하는 것이 좀 더 보편적이지만, 맞춤 구성요소를 빌드하기 위해 View 클래스를 확장하고자 하는 상황이 올 수도 있습니다. 어쩌면 Button 클래스를 확장하여 무언가 더 복잡한 것을 만들고자 할 수도 있습니다. 이런 경우, 이벤트 핸들러 클래스를 사용하여 클래스의 기본 이벤트 동작을 정의할 수 있습니다.

이벤트 리스너

이벤트 리스너란 View 클래스 내에 있는 일종의 인터페이스로, 이 안에 하나의 콜백 메서드가 들어 있습니다. 이러한 메서드는 리스너가 등록된 뷰가 사용자 상호작용으로 인해 UI 내 항목과 함께 트리거되었을 때 Android 프레임워크에 의해 호출됩니다.

이벤트 리스너 인터페이스에 포함된 콜백 메서드는 다음과 같습니다.

onClick()
View.OnClickListener에서. 이 메서드는 사용자가 (터치 모드에 있을 때) 항목을 터치하거나, 탐색 키 또는 트랙볼을 사용하여 해당 항목에 포커스를 맞추고 적절한 'Enter' 키를 누르거나, 트랙볼을 누르면 호출됩니다.
onLongClick()
View.OnLongClickListener에서. 이 메서드는 사용자가 (터치 모드에 있을 때) 항목을 길게 터치하거나, 탐색 키 또는 트랙볼을 사용하여 해당 항목에 포커스를 맞추고 적절한 'Enter' 키를 길게 누르거나 트랙볼을 길게 누르면(1초간) 호출됩니다.
onFocusChange()
View.OnFocusChangeListener에서. 이 메서드는 사용자가 탐색 키 또는 트랙볼을 사용하여 항목 쪽으로 이동하거나 항목에서 멀어지면 호출됩니다.
onKey()
View.OnKeyListener에서. 이 메서드는 사용자가 항목에 포커스를 맞추고 있으면서 기기에 있는 하드웨어 키를 누르거나 키에서 손을 떼면 호출됩니다.
onTouch()
View.OnTouchListener에서. 이 메서드는 사용자가 터치 이벤트로서의 자격을 만족하는 작업을 수행하는 경우에 호출되며, 여기에는 누르기, 손 떼기와 화면에서 이루어지는 모든 움직임 동작(항목의 경계 내에서)이 포함됩니다.
onCreateContextMenu()
View.OnCreateContextMenuListener에서. 이 메서드는 컨텍스트 메뉴가 구축되는 중일 때 ('길게 클릭'의 지속 결과로) 호출됩니다. 메뉴 개발자 가이드에 있는 컨텍스트 메뉴 관련 논의를 참고하세요.

이러한 메서드는 각자의 인터페이스 안에 단독으로 존재합니다. 이러한 메서드 중 하나를 정의하고 이벤트를 처리하려면 Activity 내에 중첩된 인터페이스를 구현하거나 익명의 클래스로 정의하면 됩니다. 그런 다음 구현의 인스턴스 하나를 각각의 View.set...Listener() 메서드에 전달합니다. 예를 들어, setOnClickListener()를 호출한 다음 이를 OnClickListener의 구현에 전달합니다.

아래의 예시는 버튼에 대하여 온-클릭 리스너를 등록하는 방법을 보여줍니다.

Kotlin

protected void onCreate(savedValues: Bundle) {
    ...
    val button: Button = findViewById(R.id.corky)
    // Register the onClick listener with the implementation above
    button.setOnClickListener { view ->
        // do something when the button is clicked
    }
    ...
}

Java

// Create an anonymous implementation of OnClickListener
private OnClickListener corkyListener = new OnClickListener() {
    public void onClick(View v) {
      // do something when the button is clicked
    }
};

protected void onCreate(Bundle savedValues) {
    ...
    // Capture our button from layout
    Button button = (Button)findViewById(R.id.corky);
    // Register the onClick listener with the implementation above
    button.setOnClickListener(corkyListener);
    ...
}

OnClickListener를 Activity의 일부로 구현하는 것이 더 편리할 수도 있습니다. 이렇게 하면 추가적인 클래스 부하와 객체 할당을 피할 수 있습니다. 예:

Kotlin

class ExampleActivity : Activity(), OnClickListener {
  
    protected fun onCreate(savedValues: Bundle) {
        val button: Button = findViewById(R.id.corky)
        button.setOnClickListener(this)
    }

    // Implement the OnClickListener callback
    fun onClick(v: View) {
        // do something when the button is clicked
    }
}

Java

public class ExampleActivity extends Activity implements OnClickListener {
    protected void onCreate(Bundle savedValues) {
        ...
        Button button = (Button)findViewById(R.id.corky);
        button.setOnClickListener(this);
    }

    // Implement the OnClickListener callback
    public void onClick(View v) {
      // do something when the button is clicked
    }
    ...
}

위의 예에서 onClick() 콜백에는 반환 값이 없지만 다른 이벤트 리스너 메서드 중에는 부울 값을 반환해야만 하는 것도 있다는 점에 유의하세요. 그 이유는 이벤트에 따라 다릅니다. 이런 필수 사항이 적용되는 몇몇 메서드의 경우, 이유는 다음과 같습니다.

  • onLongClick() - 이 메서드는 이벤트가 완전히 사용되었기 때문에 더 이상 전달되지 않아야 하는지를 나타내는 부울을 반환합니다. 다시 말해, true를 반환하면 이벤트를 처리했으며 여기에서 중단해야 한다는 것을 의미하고, false를 반환하면 이벤트가 아직 미처리 상태이거나 이 이벤트를 다른 클릭 시 리스너로 계속 전달해야 함을 나타냅니다.
  • onKey() - 이 메서드는 이벤트가 완전히 사용되었기 때문에 더 이상 전달되지 않아야 하는지를 나타내는 부울을 반환합니다. 다시 말해, true를 반환하면 이벤트를 처리했으며 여기에서 중단해야 한다는 것을 의미하고, false를 반환하면 이벤트가 아직 미처리 상태이거나 이 이벤트를 다른 온-키 리스너로 계속 전달해야 함을 나타냅니다.
  • onTouch() - 이 메서드는 리스너가 이 이벤트를 사용하는지를 나타내는 부울을 반환합니다. 여기서 중요한 점은 이 이벤트에는 서로 연달아 발생하는 여러 개의 작업이 있을 수 있다는 것입니다. 그러므로 'down' 작업 이벤트를 수신했을 때 false를 반환하면, 해당 이벤트를 사용하지 않았으며 이 이벤트의 이후 작업에 관심이 없음을 나타내는 것입니다. 따라서 이 이벤트 내의 다른 모든 작업과 관련해 개발자가 호출되지 않습니다(예: 손가락 동작 또는 최종적인 'up' 작업 이벤트 등).

하드웨어 키 이벤트는 항상 현재 포커스를 가진 뷰로 전달된다는 점을 명심하세요. 하드웨어 키 이벤트는 적절한 목적지에 도달할 때까지 뷰 계층 구조의 맨 위에서부터 아래 방향으로 보내집니다. 뷰(또는 뷰의 하위 요소)에 현재 초점이 맞춰져 있으면, 이벤트가 dispatchKeyEvent() 메서드를 통과하여 이동하는 것을 확인할 수 있습니다. 뷰를 통해 키 이벤트를 캡처하는 대신, Activity 내부의 모든 이벤트를 onKeyDown()onKeyUp()을 사용하여 수신할 수도 있습니다.

또한 애플리케이션의 텍스트 입력의 경우 대다수 기기에는 소프트웨어 입력 메서드만 있다는 사실을 명심하세요. 그러한 메서드는 반드시 키 기반이 아니어도 되며, 음성 입력, 손글씨 등을 사용할 수도 있습니다. 입력 메서드가 키보드와 같은 인터페이스를 표시하더라도 일반적으로 onKeyDown() 이벤트군을 트리거하지는 않습니다. 애플리케이션을 하드웨어 키보드가 있는 기기로 제한하지 않는 한, 특정 키 누름을 제어해야 하는 UI를 빌드해서는 안 됩니다. 특히, 사용자가 Return 키를 누를 때 입력의 유효성을 검사하기 위해 이와 같은 메서드를 사용해서는 안 됩니다. 대신 IME_ACTION_DONE과 같은 작업을 사용하여, 필요한 애플리케이션의 반응을 입력 메서드에 표시해야 합니다. 그러면 UI가 의미 있게 변경됩니다. 소프트웨어 입력 메서드가 어떻게 작동할지 임의로 추정하지 말고, 이미 형식화된 텍스트를 애플리케이션에 제공해줄 것이라 믿으면 됩니다.

참고: Android는 맨 처음에는 이벤트 핸들러를 호출하고, 뒤 이어 클래스 정의에서 적절한 기본 핸들러를 호출합니다. 따라서 이벤트 리스너에서 true를 반환하면 이벤트가 다른 이벤트 리스너로 전파되는 것이 중지될 뿐만 아니라 뷰에 있는 기본 이벤트 핸들러로의 콜백도 차단됩니다. true를 반환할 때는 해당 이벤트를 확실히 종료하려는 생각이어야 합니다.

이벤트 핸들러

뷰에서 맞춤 구성요소를 빌드하는 경우, 기본 이벤트 핸들러로 사용할 콜백 메서드를 여러 개 정의할 수 있습니다. 맞춤 뷰 구성요소에 관한 문서를 보면 이벤트 처리에 사용되는 몇 가지 보편적인 콜백을 확인할 수 있습니다.

개발자가 알아두어야 하는 다른 메서드가 몇 가지 더 있습니다. 이러한 메서드는 View 클래스의 일부는 아니지만 이벤트를 처리할 수 있는 방식에 직접적으로 영향을 미칠 수 있습니다. 그러므로 레이아웃 안에서 좀 더 복잡한 이벤트를 관리하는 경우 이러한 다른 메서드도 고려하세요. 예를 들면 다음과 같습니다.

터치 모드

사용자가 방향 키 또는 트랙볼을 사용하여 사용자 인터페이스를 탐색하고 있는 경우, 실행 가능한 항목(예: 버튼)에 포커스를 맞추어야 합니다. 그래야 입력이 허용되는 항목을 사용자가 알 수 있습니다. 하지만 기기에 터치 기능이 있고 사용자가 인터페이스를 터치하여 인터페이스와의 상호작용을 시작하는 경우라면 더 이상 항목을 강조표시하거나 특정 뷰에 포커스를 맞추지 않아도 됩니다. 그 이유에서 '터치 모드'라는 상호작용 모드가 있습니다.

터치 기능이 있는 기기의 경우, 사용자가 화면을 터치하면 기기가 터치 모드에 진입합니다. 이 시점부터는 isFocusableInTouchMode()가 true인 뷰만 포커스가 가능합니다(예: 텍스트 편집 위젯). 버튼과 같이 터치가 가능한 다른 뷰는 터치했을 때 포커스를 가지지 않고, 눌렸을 때 클릭 시 리스너가 실행되기만 합니다.

사용자가 방향 키를 누르거나 트랙볼로 스크롤 동작을 할 때마다 기기가 터치 모드를 종료하고 포커스를 맞출 뷰를 찾습니다. 이제 사용자는 화면을 터치하지 않아도 사용자 인터페이스와 상호작용을 다시 시작할 수 있습니다.

터치 모드 상태는 시스템 전체에서 유지됩니다(모든 창과 활동 포함). 현재 상태를 쿼리하려면 isInTouchMode()를 호출하여 기기가 현재 터치 모드에 있는지 확인하면 됩니다.

포커스 처리

프레임워크는 사용자 입력에 응답하여 일상적인 포커스 동작을 처리합니다. 뷰가 삭제되거나 숨겨질 때 또는 새 뷰가 사용 가능한 상태가 되었을 때 포커스를 변경하는 경우 등이 이에 포함됩니다. 뷰는 포커스를 받고자 하는 의향을 isFocusable() 메서드를 통해 나타냅니다. 뷰가 포커스를 받을 수 있는지를 변경하려면 setFocusable()을 호출합니다. 터치 모드에 있는 경우, isFocusableInTouchMode()를 사용하여 뷰가 포커스를 허용하는지 쿼리할 수 있습니다. 변경 시에는 setFocusableInTouchMode()를 사용하면 됩니다.

Android 9(API 수준 28) 이상이 실행되는 기기에서 활동은 초기 포커스를 할당하지 않습니다. 그 대신 개발자가 초기 포커스를 명시적으로 요청해야 합니다(원할 경우).

포커스 이동은 주어진 방향에서 가장 가까운 이웃을 찾아내는 알고리즘을 기반으로 합니다. 드문 일이지만 기본 알고리즘이 개발자가 의도한 동작과 일치하지 않는 경우도 있습니다. 이러한 상황이라면, 레이아웃 파일에서 nextFocusDown, nextFocusLeft, nextFocusRight, nextFocusUp과 같은 XML 속성을 사용하여 명시적 재정의를 제공하면 됩니다. 이러한 속성 중 하나를 포커스가 사라지는 뷰에 추가합니다. 그 속성 값을 포커스를 두어야 할 뷰의 ID가 되도록 정의합니다. 예:

<LinearLayout
    android:orientation="vertical"
    ... >
  <Button android:id="@+id/top"
          android:nextFocusUp="@+id/bottom"
          ... />
  <Button android:id="@+id/bottom"
          android:nextFocusDown="@+id/top"
          ... />
</LinearLayout>

보통은 이런 수직 레이아웃에서 첫 버튼부터 위로 이동하면 아무 데도 갈 수 없고 두 번째 버튼에서 아래로 이동해도 마찬가지입니다. 이제 맨 위 버튼이 맨 아래 버튼을 nextFocusUp으로 정의했습니다(반대의 경우도 마찬가지). 따라서 탐색 포커스가 위에서 아래로 갔다가 아래에서 위로 순환하게 됩니다.

특별히 뷰를 UI에서 초점을 맞출 수 있는 항목으로 선언하고자 한다면(일반적으로는 그렇지 않은 경우), 레이아웃 선언에서 뷰에 android:focusable XML 속성을 추가합니다. true 값을 설정합니다. 터치 모드에 있을 때에도 android:focusableInTouchMode를 사용하여 뷰를 포커스를 받을 수 있는 항목으로 선언할 수 있습니다.

특정 뷰에 포커스를 두도록 요청하려면, requestFocus()를 호출합니다.

포커스 이벤트를 수신 대기하려면(뷰가 포커스가 받거나 잃은 경우 이에 대한 알림 수신), 이벤트 리스너 섹션에서 설명한 바와 같이 onFocusChange()를 사용하면 됩니다.