입력 변경사항 처리

Chromebook은 사용자에게 키보드, 마우스, 트랙패드, 터치 스크린, 스타일러스, MIDI, 게임패드/블루 컨트롤러 등 다양한 입력 옵션을 제공합니다. 즉, 동일한 기기가 DJ의 방송국, 아티스트의 캔버스, AAA 스트리밍 게임을 위한 게이머의 선호 플랫폼이 될 수 있습니다.

개발자는 이를 통해 부착된 키보드부터 스타일러스, Stadia 게임 컨트롤러에 이르기까지 사용자가 이미 가지고 있는 입력 기기를 활용하여 사용자에게 다재다능하고 흥미로운 앱 환경을 제공할 수 있습니다. 하지만 이러한 모든 가능성을 고려할 때 앱 환경을 원활하고 논리적으로 만들기 위해 UI를 고려해야 합니다. 앱이나 게임이 휴대전화를 염두에 두고 설계된 경우 특히 그렇습니다. 예를 들어 게임에 휴대전화용 터치 컨트롤 온스크린 조이스틱이 있는 경우 사용자가 키보드로 플레이할 때는 이를 숨기는 것이 좋습니다.

이 페이지에서는 여러 입력 소스를 고려할 때 유의해야 할 주요 문제와 이를 해결하기 위한 전략을 확인할 수 있습니다.

지원되는 입력 방법의 사용자 검색

앱은 사용자가 선택한 입력에 원활하게 응답해야 합니다. 이 과정은 간단하며 사용자에게 추가 정보를 제공할 필요가 없는 경우가 많습니다. 예를 들어 사용자가 마우스, 트랙패드, 터치 스크린, 스타일러스 등으로 버튼을 클릭하면 버튼이 작동해야 하며, 사용자가 이러한 다양한 기기를 사용하여 버튼을 활성화할 수 있다고 알려줄 필요가 없습니다.

하지만 입력 방식이 사용자 환경을 개선할 수 있는 상황이 있으며 앱에서 이를 지원한다고 알리는 것이 타당할 수 있습니다. 예를 들면 다음과 같습니다.

  • 미디어 재생 앱은 사용자가 쉽게 추측할 수 없는 유용한 키보드 단축키를 많이 지원할 수 있습니다.
  • DJ 앱을 만든 경우 사용자는 처음에는 터치 스크린을 사용할 수 있으며 개발자가 키보드/트랙패드를 사용하여 일부 기능에 촉각 액세스를 제공하도록 허용했다는 사실을 알지 못할 수 있습니다. 마찬가지로 사용자가 다양한 MIDI DJ 컨트롤러를 지원한다는 사실을 모를 수도 있습니다. 지원되는 하드웨어를 확인하도록 유도하면 더욱 실감 나는 DJing 환경을 제공할 수 있습니다.
  • 게임이 터치 스크린과 키보드/마우스에 최적화되어 있어도 블루투스 게임 컨트롤러를 지원한다는 사실을 사용자가 모를 수 있습니다. 이러한 기기 중 하나를 연결하면 사용자 만족도와 참여도를 높일 수 있습니다.

적절한 시점에 메시지를 표시하여 사용자가 입력 옵션을 찾을 수 있도록 지원할 수 있습니다. 구현은 앱마다 다르게 표시됩니다. 몇 가지 예는 다음과 같습니다.

  • 첫 실행 또는 오늘의 팁 팝업
  • 설정 패널의 구성 옵션은 지원이 있음을 사용자에게 수동적으로 나타낼 수 있습니다. 예를 들어 게임의 설정 패널에 '게임 컨트롤러' 탭이 있으면 컨트롤러가 지원됨을 나타냅니다.
  • 상황별 메시지 예를 들어 실제 키보드를 감지했는데 사용자가 마우스나 터치 스크린을 사용하여 작업을 클릭하는 경우 키보드 단축키를 제안하는 유용한 힌트를 표시할 수 있습니다.
  • 단축키 목록 물리적 키보드가 감지되면 사용 가능한 단축키 목록을 표시하는 방법을 UI에 표시하면 키보드 지원이 제공된다는 점을 광고하는 동시에 사용자가 지원되는 단축키를 쉽게 보고 기억할 수 있는 방법이 제공됩니다.

입력 변형의 UI 라벨 지정/레이아웃

이상적으로는 다른 입력 기기를 사용하는 경우 시각적 인터페이스를 많이 변경할 필요가 없으며 가능한 모든 입력이 '그냥 작동'해야 합니다. 하지만 중요한 예외가 있습니다. 주요한 두 가지는 터치 전용 UI와 화면 메시지입니다.

터치 전용 UI

앱에 터치 전용 UI 요소(예: 게임의 화면 조이스틱)가 있는 경우 터치를 사용하지 않을 때의 사용자 경험을 고려해야 합니다. 일부 모바일 게임에서는 터치 기반 플레이에 필요하지만 사용자가 게임패드나 키보드를 사용하여 플레이하는 경우에는 필요하지 않은 컨트롤이 화면의 상당 부분을 차지합니다. 앱이나 게임은 현재 사용 중인 입력 방법을 감지하고 그에 따라 UI를 조정하는 로직을 제공해야 합니다. 이를 실행하는 방법의 예는 아래 구현 섹션을 참고하세요.

자동차 경주 게임 UI - 화면 컨트롤이 있는 UI와 키보드가 있는 UI

화면에 표시되는 메시지

앱에서 사용자에게 유용한 화면 메시지를 제공할 수 있습니다. 입력 장치에 종속되지 않도록 주의하세요. 예를 들면 다음과 같습니다.

  • 다음으로 스와이프
  • 아무 곳이나 탭하여 닫습니다.
  • 손가락을 모으거나 펼쳐 확대/축소
  • 'X'를 누르면…
  • 길게 눌러 활성화

일부 앱은 입력에 구애받지 않도록 문구를 조정할 수 있습니다. 이 방법이 적합한 경우도 있지만, 많은 경우 구체성이 중요하며 사용 중인 입력 방법에 따라 다른 메시지를 표시해야 할 수도 있습니다. 특히 앱의 첫 실행과 같은 튜토리얼 유형 모드에서 그렇습니다.

다국어 고려사항

앱에서 여러 언어를 지원하는 경우 문자열 아키텍처를 고려해야 합니다. 예를 들어 3개의 입력 모드와 5개의 언어를 지원하는 경우 모든 UI 메시지의 15가지 버전이 있을 수 있습니다. 이렇게 하면 새로운 기능을 추가하는 데 필요한 작업량이 늘어나고 맞춤법 오류가 발생할 가능성이 높아집니다.

한 가지 방법은 인터페이스 작업을 별도의 문자열 집합으로 생각하는 것입니다. 예를 들어 '닫기' 작업을 '탭하여 닫기', 'Esc 키를 눌러 닫기', '클릭하여 닫기', '버튼을 눌러 닫기'와 같은 입력별 변형이 있는 자체 문자열 변수로 정의하면 사용자에게 닫는 방법을 알려야 하는 모든 UI 메시지에서 이 단일 '닫기' 문자열 변수를 사용할 수 있습니다. 입력 방법이 변경되면 이 변수 하나의 값만 변경하면 됩니다.

소프트 키보드 / IME 입력

사용자가 실제 키보드 없이 앱을 사용하는 경우 터치 키보드를 통해 텍스트를 입력할 수 있습니다. 터치 키보드가 표시될 때 필요한 UI 요소가 가려지지 않는지 테스트해야 합니다. 자세한 내용은 Android IME 표시 상태 문서를 참고하세요.

구현

대부분의 경우 앱이나 게임은 화면에 표시되는 내용과 관계없이 모든 유효한 입력에 올바르게 응답해야 합니다. 사용자가 10분 동안 터치 스크린을 사용하다가 갑자기 키보드 사용으로 전환하는 경우 화면의 시각적 프롬프트나 컨트롤과 관계없이 키보드 입력이 작동해야 합니다. 즉, 시각적 요소/텍스트보다 기능이 우선해야 합니다.이렇게 하면 입력 감지 로직에 오류가 있거나 예기치 않은 상황이 발생하더라도 앱/게임을 사용할 수 있습니다.

다음 단계는 현재 사용 중인 입력 방식에 맞는 UI를 표시하는 것입니다. 이 문제를 정확하게 감지하려면 어떻게 해야 하나요? 사용자가 앱을 사용하는 동안 여러 입력 방식으로 전환하면 어떻게 되나요? 여러 방법을 동시에 사용하는 경우는 어떻게 되나요?

수신된 이벤트를 기반으로 우선순위가 지정된 상태 머신

한 가지 접근 방식은 앱에서 수신하는 실제 입력 이벤트를 추적하고 우선순위가 지정된 논리를 사용하여 상태 간에 전환하여 현재 '활성 입력 상태'(현재 화면에 사용자에게 표시되는 입력 프롬프트 나타냄)를 추적하는 것입니다.

우선순위 지정

입력 상태에 우선순위를 지정해야 하는 이유 사용자는 다양한 방식으로 기기와 상호작용하며 앱은 사용자의 선택을 지원해야 합니다. 예를 들어 사용자가 터치 스크린과 블루투스 마우스를 동시에 사용하기로 선택한 경우 지원되어야 합니다. 하지만 어떤 화면 입력 프롬프트와 컨트롤을 표시해야 할까요? 마우스 또는 터치

각 프롬프트 세트를 '입력 상태'로 정의한 다음 우선순위를 지정하면 일관된 방식으로 이를 결정하는 데 도움이 됩니다.

수신된 입력 이벤트가 상태를 결정함

수신된 입력 이벤트에만 작동하는 이유는 무엇인가요? 블루투스 연결을 추적하여 블루투스 컨트롤러가 연결되었는지 표시하거나 USB 기기의 USB 연결을 감시할 수 있다고 생각할 수 있습니다. 이 방법은 두 가지 주요 이유로 권장되지 않습니다.

무엇보다 API 변수를 기반으로 연결된 기기에 관해 추측할 수 있는 정보가 일관되지 않으며 블루투스/하드웨어/스타일러스 기기의 수가 계속 증가하고 있습니다.

연결 상태 대신 수신된 이벤트를 사용해야 하는 두 번째 이유는 사용자가 마우스, 블루투스 컨트롤러, MIDI 컨트롤러 등을 연결했지만 앱이나 게임과 상호작용하는 데 적극적으로 사용하지 않을 수 있기 때문입니다.

앱에서 적극적으로 수신한 입력 이벤트에 응답하면 불완전한 정보로 사용자의 의도를 추측하지 않고 사용자의 실제 행동에 실시간으로 응답할 수 있습니다.

예: 터치, 키보드/마우스, 컨트롤러 지원이 있는 게임

터치 기반 휴대폰용 자동차 경주 게임을 개발했다고 가정해 보겠습니다. 플레이어는 가속, 감속, 오른쪽 회전, 왼쪽 회전, 니트로를 사용하여 속도를 높일 수 있습니다.

현재 터치 스크린 인터페이스는 속도 및 방향 제어를 위한 화면의 왼쪽 하단에 있는 화면 조이스틱과 니트로를 위한 오른쪽 하단의 버튼으로 구성되어 있습니다. 사용자는 트랙에서 Nitro 캔을 수집할 수 있으며 화면 하단의 Nitro 표시줄이 가득 차면 버튼 위에 'Nitro를 사용하려면 누르세요'라는 메시지가 표시됩니다. 사용자의 첫 번째 게임이거나 한동안 입력이 수신되지 않으면 조이스틱 위에 자동차를 움직이는 방법을 보여주는 '튜토리얼' 텍스트가 표시됩니다.

키보드 및 블루투스 게임 컨트롤러 지원을 추가하고 싶습니다. 어디서부터 시작해야 할까요?

터치 컨트롤이 있는 자동차 레이싱 게임

입력 상태

게임이 실행될 수 있는 모든 입력 상태를 식별한 다음 각 상태에서 변경할 모든 매개변수를 나열합니다.

                                                                                                                                                                        
터치키보드/마우스게임 컨트롤러
       

반응

     
       

모든 입력

     
       

모든 입력

     
       

모든 입력

     
       

화면 컨트롤

     
       

- 화면 조이스틱
- Nitro 버튼

     
       

- 조이스틱 없음
- 니트로 버튼 없음

     
       

- 조이스틱 없음
- 니트로 버튼 없음

     
       

텍스트

     
       

탭하여 Nitro를 확인하세요.

     
       

Nitro를 사용하려면 'N'을 누르세요.

     
       

Nitro를 사용하려면 'A'를 누르세요.

     
       

튜토리얼

     
       

속도/방향 조이스틱 이미지

     
       

속도/방향을 나타내는 화살표 키와 WASD 이미지

     
       

속도/방향을 위한 게임패드 이미지

     

'활성 입력' 상태를 추적한 다음 이 상태에 따라 필요에 따라 UI를 업데이트합니다.

참고: 게임/앱은 상태와 관계없이 모든 입력 방식에 응답해야 합니다. 예를 들어 사용자가 터치 스크린으로 자동차를 운전하고 있지만 키보드에서 N을 누르면 니트로 액션이 트리거되어야 합니다.

우선순위가 지정된 상태 변경

일부 사용자는 터치 스크린과 키보드 입력을 동시에 사용할 수 있습니다. 일부 사용자는 태블릿 모드로 소파에서 게임/앱을 사용하다가 테이블에서 키보드를 사용하는 것으로 전환할 수 있습니다. 게임 도중에 게임 컨트롤러를 연결하거나 연결 해제하는 사용자도 있습니다.

터치 스크린을 사용하는 사용자에게 n 키를 누르라고 하는 등 잘못된 UI 요소가 없어야 합니다. 동시에 터치 스크린과 키보드와 같은 여러 입력 기기를 사용하는 사용자의 경우 UI가 두 상태 사이에서 계속 전환되지 않도록 해야 합니다.

이 문제를 처리하는 한 가지 방법은 입력 유형 우선순위를 설정하고 상태 변경 사이에 지연을 빌드하는 것입니다. 위의 자동차 게임의 경우 화면 터치 이벤트가 수신될 때마다 화면의 조이스틱이 표시되도록 항상 해야 합니다. 그렇지 않으면 사용자에게 게임이 사용할 수 없는 것처럼 보일 수 있습니다. 이렇게 하면 터치 스크린이 가장 높은 우선순위 기기가 됩니다.

키보드와 터치 스크린 이벤트가 동시에 수신되는 경우 게임은 터치 스크린 모드로 유지되어야 합니다. 하지만 키보드 입력에는 계속 반응합니다. 5초 후에 터치 스크린 입력이 수신되지 않고 키보드 이벤트가 계속 수신되면 화면 컨트롤이 흐려지고 게임이 키보드 상태로 이동할 수 있습니다.

게임 컨트롤러 입력도 비슷한 패턴을 따릅니다. 컨트롤러 UI 상태는 키보드/마우스 및 터치보다 우선순위가 낮으며 게임 컨트롤러 입력이 수신되고 다른 형태의 입력이 수신되지 않는 경우에만 표시됩니다. 게임은 항상 컨트롤러 입력에 응답합니다.

아래는 예의 상태 다이어그램과 전환 표입니다. 아이디어를 앱 또는 게임에 맞게 조정합니다.

우선순위가 지정된 상태 머신 - 터치 스크린, 키보드/마우스, 게임 컨트롤러

                                                                                                                                        
#1 터치 스크린#2 키보드#3 게임패드
       

1번으로 이동

     
       

해당 사항 없음

     
       

- 터치 입력 수신
- 즉시 터치 입력 상태로 이동

     
       

- 터치 입력 수신
- 즉시 터치 입력 상태로 이동

     
       

2번으로 이동

     
       

- 5초 동안 터치 없음
- 키보드 입력 수신됨
- 키보드 입력 상태로 이동

     
       

해당 사항 없음

     
       

- 키보드 입력 수신
(즉시 키보드 입력 상태로 이동)

     
       

3번으로 이동

     
       

- 5초 동안 터치 없음
- 5초 동안 키보드 없음
- 게임패드 입력 수신
- 게임패드 입력 상태로 이동

     
       

- 5초 동안 키보드 없음
- 게임패드 입력 수신
- 게임패드 입력 상태로 이동

     
       

해당 사항 없음

     

참고: 우선순위 지정은 어떤 유형의 입력이 지배적이어야 하는지 명확하게 하는 데 도움이 됩니다. 입력 상태가 우선순위에서 즉시 '위'로 이동합니다.

3. 게임패드 -> 2. 키보드 -> 1. 터치

우선순위가 높은 기기가 사용되는 즉시 우선순위가 천천히 '아래'로 이동합니다. 단, 지연 기간이 지난 후 우선순위가 낮은 기기가 활발하게 사용되는 경우에만 이동합니다.

입력 이벤트

다음은 표준 Android API를 사용하여 다양한 종류의 입력 장치에서 입력 이벤트를 감지하는 방법의 예시 코드입니다. 위와 같이 이러한 이벤트를 사용하여 상태 머신을 구동합니다. 일반적인 개념을 예상되는 입력 이벤트 유형과 앱 또는 게임에 맞게 조정해야 합니다.

키보드 및 컨트롤러 버튼

// Drive the state machine based on the received input type
// onKeyDown drives the state machine, but does not trigger game actions
// Both keyboard and game controller events come through as key events
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if (event != null) {
        // Check input source
        val outputMessage = when (event.source) {
            SOURCE_KEYBOARD -> {
                MyStateMachine.KeyboardEventReceived()
                "Keyboard event"
            }
            SOURCE_GAMEPAD -> {
                MyStateMachine.ControllerEventReceived()
                "Game controller event"
            }
            else -> "Other key event: ${event.source}"
        }
        // Do something based on source type
        findViewById(R.id.text_message).text = outputMessage
    }
    // Pass event up to system
    return super.onKeyDown(keyCode, event)
}
// Trigger game events based on key release
// Both keyboard and game controller events come through as key events
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
   when(keyCode) {
       KeyEvent.KEYCODE_N -> {
           MyStateMachine.keyboardEventReceived()
           engageNitro()
           return true // event handled here, return true
       }
   }
   // If event not handled, pass up to system
   return super.onKeyUp(keyCode, event)
}

참고: KeyEvents의 경우 onKeyDown() 또는 onKeyUp() 중 하나를 사용할 수 있습니다. 여기서 onKeyDown()는 상태 머신을 제어하는 데 사용되고 onKeyUp()는 게임 이벤트를 트리거하는 데 사용됩니다.

사용자가 버튼을 길게 누르면 onKeyUp()은 키 누름당 한 번만 트리거되는 반면 onKeyDown()은 여러 번 호출됩니다. 다운 프레스에 반응하려면 onKeyDown()에서 게임 이벤트를 처리하고 반복되는 이벤트를 처리하는 로직을 구현해야 합니다. 자세한 내용은 키보드 작업 처리 문서를 참고하세요.

터치 및 스타일러스

// Touch and stylus events come through as touch events
override fun onTouchEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Get tool type
       val pointerIndex = event.action and ACTION_POINTER_INDEX_MASK shr ACTION_POINTER_INDEX_SHIFT
       val toolType = event.getToolType(pointerIndex)

       // Check tool type
       val outputMessage = when (toolType) {
           TOOL_TYPE_FINGER -> {
               MyStateMachine.TouchEventReceived()
               "Touch event"
           }
           TOOL_TYPE_STYLUS -> {
                MyStateMachine.StylusEventReceived()
               "Stylus event"
           }
           else -> "Other touch event: ${toolType}"
       }

       // Do something based on tool type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}

마우스 및 조이스틱

// Mouse and joystick events come through as generic events
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Check input source
       val outputMessage = when (event.source) {
           SOURCE_JOYSTICK -> {
                MyStateMachine.ControllerEventReceived()
               "Controller event"
           }
           SOURCE_MOUSE -> {
                MyStateMachine.MouseEventReceived()
               "Mouse event"
           }
           else -> "Other generic event: ${event.source}"
       }
       // Do something based on source type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}