앱 개발

다음은 앱 개발자를 위한 자료입니다.

앱에서 로터리를 지원하도록 하려면 다음을 실행해야 합니다.

  1. 각 활동 레이아웃에 FocusParkingView를 배치합니다.
  2. 포커스 가능 뷰인지 확인합니다.
  3. FocusArea를 사용하여 포커스 가능한 모든 뷰(FocusParkingView 제외)를 둘러쌉니다.

이러한 각 작업은 아래에 자세히 설명되어 있으며 그 전에 로터리 지원 앱 개발을 위한 환경을 설정합니다.

로터리 컨트롤러 설정

로터리 지원 앱 개발을 시작하려면 먼저 로터리 컨트롤러나 스탠드인이 필요합니다. 아래에 옵션이 설명되어 있습니다.

에뮬레이터

source build/envsetup.sh && lunch car_x86_64-userdebug
m -j
emulator -wipe-data -no-snapshot -writable-system

aosp_car_x86_64-userdebug도 사용할 수 있습니다.

에뮬레이션된 로터리 컨트롤러에 액세스하려면 다음 안내를 따르세요.

  1. 다음과 같이 툴바 하단의 점 3개를 탭합니다.

    에뮬레이션된 로터리 컨트롤러에 액세스
    그림 1. 에뮬레이션된 로터리 컨트롤러에 액세스
  2. 확장 컨트롤 창에서 자동차 로터리를 선택합니다.

    자동차 로터리 선택
    그림 2. 자동차 로터리 선택

USB 키보드

  • USB 키보드를 Android Automotive OS(AAOS)를 실행하는 기기에 연결합니다. 때에 따라 터치 키보드가 표시되지 않을 수 있습니다.
  • userdebug 또는 eng 빌드를 사용합니다.
  • 키 이벤트 필터링을 사용 설정합니다.
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • 아래 표를 참고하여 각 작업에 상응하는 키를 확인하세요.
    로터리 작업
    Q 시계 반대 방향으로 회전
    E 시계 방향으로 회전
    A 왼쪽으로 조금씩 이동
    D 오른쪽으로 조금씩 이동
    W 위쪽으로 조금씩 이동
    S 아래쪽으로 조금씩 이동
    F 또는 쉼표 가운데 버튼
    R 또는 Esc 뒤로 버튼

ADB 명령어

car_service 명령어를 사용하여 로터리 입력 이벤트를 삽입할 수 있습니다. 이러한 명령어는 Android Automotive OS(AAOS)를 실행하는 기기에서 또는 에뮬레이터에서 실행할 수 있습니다.

car_service 명령어 로터리 입력
adb shell cmd car_service inject-rotary 시계 반대 방향으로 회전
adb shell cmd car_service inject-rotary -c true 시계 방향으로 회전
adb shell cmd car_service inject-rotary -dt 100 50 시계 반대 방향으로 여러 번 회전(100ms 전, 50ms 전)
adb shell cmd car_service inject-key 282 왼쪽으로 조금씩 이동
adb shell cmd car_service inject-key 283 오른쪽으로 조금씩 이동
adb shell cmd car_service inject-key 280 위쪽으로 조금씩 이동
adb shell cmd car_service inject-key 281 아래쪽으로 조금씩 이동
adb shell cmd car_service inject-key 23 가운데 버튼 클릭
adb shell input keyevent inject-key 4 뒤로 버튼 클릭

OEM 로터리 컨트롤러

로터리 컨트롤러 하드웨어가 실행 중일 때 가장 현실적인 옵션입니다. 빠른 회전을 테스트하는 데 특히 유용합니다.

FocusParkingView

FocusParkingView자동차 UI 라이브러리(car-ui-library)의 투명 뷰입니다. RotaryService에서 사용하여 로터리 컨트롤러 탐색을 지원합니다. FocusParkingView는 레이아웃의 첫 번째 포커스 가능 뷰여야 합니다. 모든 FocusArea 외부에 배치해야 합니다. 각 창에 FocusParkingView가 하나씩 있어야 합니다. FocusParkingView가 포함된 car-ui-library 기본 레이아웃을 이미 사용하고 있다면 다른 FocusParkingView를 추가하지 않아도 됩니다. 다음은 RotaryPlaygroundFocusParkingView를 보여주는 예입니다.

<FrameLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">
   <com.android.car.ui.FocusParkingView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
   <FrameLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"/>
</FrameLayout>

FocusParkingView가 필요한 이유는 다음과 같습니다.

  1. Android에서는 포커스가 다른 창에 설정되어 있으면 자동으로 포커스를 지우지 않습니다. 이전 창에서 포커스를 지우려고 하면 Android는 이전 창의 뷰에 다시 포커스를 두기 때문에 동시에 두 창에 포커스가 있게 됩니다. 각 창에 FocusParkingView를 추가하면 이 문제를 해결할 수 있습니다. 이 뷰는 투명하고 기본 포커스 하이라이트가 사용 중지되어 있으므로 포커스가 있는지와 관계없이 사용자에게 표시되지 않습니다. RotaryService가 포커스를 배치하여 포커스 하이라이트를 삭제할 수 있도록 포커스를 받을 수 있습니다.
  2. 현재 창에 FocusArea가 하나만 있는 경우 FocusArea에서 컨트롤러를 회전하면 RotaryService가 오른쪽 뷰에서 왼쪽 뷰로(또는 그 반대로) 포커스를 이동합니다. 이 뷰를 각 창에 추가하면 문제를 해결할 수 있습니다. RotaryService에서 포커스 타겟이 FocusParkingView라고 판단하면 포커스를 이동하지 않음으로써 랩어라운드를 피하는 시점에 랩어라운드가 발생하려고 한다고 파악할 수 있습니다.
  3. 로터리 컨트롤이 앱을 실행하면 Android는 첫 번째 포커스 가능 뷰에 포커스를 두는데 이는 항상 FocusParkingView입니다. FocusParkingView는 포커스를 둘 최적의 뷰를 결정하여 포커스를 적용합니다.

포커스 가능 뷰

RotaryService는 휴대전화에 물리적 키보드와 D패드가 있던 때 Android 프레임워크의 기존 뷰 포커스 개념에 기반합니다. 기존 android:nextFocusForward 속성은 로터리 용도에 맞게 수정되었지만(FocusArea 맞춤설정 참고) android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp, android:nextFocusDown은 수정되지 않았습니다.

RotaryService는 포커스 가능한 뷰에만 포커스를 둡니다. Button과 같은 일부 뷰는 일반적으로 포커스 가능합니다. 그러나 TextViewViewGroup과 같은 다른 속성은 일반적으로 그렇지 않습니다. 클릭 가능한 뷰는 자동으로 포커스 가능하고 클릭 리스너가 있는 뷰는 자동으로 클릭 가능합니다. 이 자동 로직으로 인해 원하는 포커스 가능 여부가 발생하면 명시적으로 뷰의 포커스 가능 여부를 설정하지 않아도 됩니다. 자동 로직으로 인해 원하는 포커스 가능 여부가 발생하지 않으면 android:focusable 속성을 truefalse로 설정하거나 View.setFocusable(boolean)을 사용하여 뷰의 포커스 가능 여부를 프로그래매틱 방식으로 설정합니다. RotaryService가 포커스를 두려면 뷰가 다음 요구사항을 충족해야 합니다.

  • 포커스 가능해야 합니다.
  • 사용 설정되어 있어야 합니다.
  • 표시되어야 합니다.
  • 너비와 높이의 값이 0이 아니어야 합니다.

뷰가 이러한 요구사항을 모두 충족하지 않으면(예: 포커스 가능하지만 사용 중지된 버튼) 사용자는 로터리 컨트롤을 사용하여 뷰에 포커스를 둘 수 없습니다. 사용 중지된 뷰에 포커스를 두려면 android:state_enabled가 아닌 맞춤 상태를 사용하여 Android가 사용 중지로 간주해야 한다고 나타내지 않고 뷰가 표시되는 방식을 제어하는 것이 좋습니다. 앱은 탭할 때 뷰가 사용 중지된 이유를 사용자에게 알릴 수 있습니다. 다음 섹션에서는 이 작업을 실행하는 방법을 설명합니다.

맞춤 상태

맞춤 상태를 추가하려면 다음 안내를 따르세요.

  1. 맞춤 속성을 뷰에 추가합니다. 예를 들어 state_rotary_enabled 맞춤 상태를 CustomView 뷰 클래스에 추가하려면 다음을 사용합니다.
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. 이 상태를 추적하려면 인스턴스 변수를 접근자 메서드와 함께 뷰에 추가합니다.
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. 뷰를 만들 때 속성 값을 읽는 방법은 다음과 같습니다.
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. 뷰 클래스에서 onCreateDrawableState() 메서드를 재정의하고 적절한 경우 맞춤 상태를 추가합니다. 예를 들면 다음과 같습니다.
    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        if (mRotaryEnabled) extraSpace++;
        int[] drawableState = super.onCreateDrawableState(extraSpace);
        if (mRotaryEnabled) {
            mergeDrawableStates(drawableState, { R.attr.state_rotary_enabled });
        }
        return drawableState;
    }
    
  5. 뷰의 클릭 핸들러가 상태에 따라 다르게 실행되도록 합니다. 예를 들어 클릭 핸들러는 아무 작업도 하지 않거나 mRotaryEnabledfalse일 때 토스트 메시지를 표시할 수 있습니다.
  6. 버튼을 사용 중지된 상태로 표시하려면 뷰의 백그라운드 드로어블에서 android:state_enabled 대신 app:state_rotary_enabled를 사용합니다. 아직 없으면 다음을 추가해야 합니다.
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. 뷰가 모든 레이아웃에서 사용 중지된 경우 android:enabled="false"app:state_rotary_enabled="false"로 바꾸고 위와 같이 app 네임스페이스를 추가합니다.
  8. 뷰가 프로그래매틱 방식으로 사용 중지된 경우 setEnabled() 호출을 setRotaryEnabled() 호출로 바꿉니다.

FocusArea

FocusAreas를 사용하여 포커스 가능 뷰를 블록으로 나누어 탐색을 더 쉽게 하고 다른 앱과 일관되도록 합니다. 예를 들어 앱에 툴바가 있으면 툴바는 앱의 나머지 부분과 분리된 FocusArea에 있어야 합니다. 탭 바와 기타 탐색 요소도 앱의 나머지 부분과 분리되어야 합니다. 큰 목록에는 일반적으로 자체 FocusArea가 있어야 합니다. 없으면 사용자가 일부 뷰에 액세스하려고 전체 목록을 회전해야 합니다.

FocusArea는 car-ui-library의 LinearLayout 서브클래스입니다. 이 기능이 사용 설정되면 FocusArea는 하위 요소 중 하나에 포커스가 있을 때 하이라이트를 그립니다. 자세한 내용은 포커스 하이라이트 맞춤설정을 참고하세요.

레이아웃 파일에서 탐색 블록을 만들 때 LinearLayout을 블록의 컨테이너로 사용하려고 한다면 FocusArea를 대신 사용하세요. 그 외의 경우에는 블록을 FocusArea에서 래핑합니다.

다른 FocusAreaFocusArea를 중첩하지 마세요. 중첩하면 정의되지 않은 탐색 동작이 발생합니다. 모든 포커스 가능 뷰가 FocusArea 내에 중첩되어 있는지 확인합니다.

다음은 RotaryPlaygroundFocusArea를 보여주는 예입니다.

<com.android.car.ui.FocusArea
       android:layout_margin="16dp"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">
       <EditText
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:singleLine="true">
       </EditText>
   </com.android.car.ui.FocusArea>

FocusArea는 다음과 같이 작동합니다.

  1. 회전 및 조금씩 이동 작업을 처리할 때 RotaryService는 뷰 계층 구조에서 FocusArea 인스턴스를 찾습니다.
  2. 회전 이벤트를 수신하면 RotaryService는 같은 FocusArea에서 포커스를 받을 수 있는 다른 뷰로 포커스를 이동합니다.
  3. 조금씩 이동 이벤트를 수신하면 RotaryService는 일반적으로 인접한 다른 FocusArea에서 포커스를 받을 수 있는 다른 뷰로 포커스를 이동합니다.

레이아웃에 FocusAreas를 포함하지 않으면 루트 뷰는 암시적 포커스 영역으로 간주됩니다. 사용자는 앱에서 조금씩 이동하여 탐색할 수 없습니다. 대신 모든 포커스 가능 뷰를 순환하며 이는 대화상자에는 적절할 수 있습니다.

FocusArea 맞춤설정

두 가지 표준 뷰 속성을 사용하여 로터리 탐색을 맞춤설정할 수 있습니다.

  • android:nextFocusForward를 사용하면 앱 개발자가 포커스 영역에서 회전 순서를 지정할 수 있습니다. 이 속성은 키보드 탐색에서 탭 순서를 제어하는 데 사용되는 속성과 같습니다. 루프를 만드는 데 이 속성을 사용하지 마세요. 대신 app:wrapAround(아래 참고)를 사용하여 루프를 만드세요.
  • android:focusedByDefault를 사용하면 앱 개발자가 창에서 기본 포커스 뷰를 지정할 수 있습니다. 이 속성과 app:defaultFocus(아래 참고)를 같은 FocusArea에서 사용하지 마세요.

FocusArea는 로터리 탐색을 맞춤설정하는 속성도 정의합니다. 암시적 포커스 영역은 이러한 속성으로 맞춤설정할 수 없습니다.

  1. (Android 11 QPR3, Android 11 Car, Android 12)
    app:defaultFocus는 사용자가 이 FocusArea로 조금씩 이동할 때 포커스가 있어야 하는 포커스 가능 하위 뷰의 ID를 지정하는 데 사용할 수 있습니다.
  2. (Android 11 QPR3, Android 11 Car, Android 12)
    app:defaultFocusOverridesHistorytrue로 설정하여 이 FocusArea의 다른 뷰에 포커스가 있었다고 나타내는 기록이 있어도 위에서 지정된 뷰가 포커스를 받도록 할 수 있습니다.
  3. (Android 12)
    app:nudgeLeftShortcut, app:nudgeRightShortcut, app:nudgeUpShortcut, app:nudgeDownShortcut을 사용하여 사용자가 주어진 방향으로 조금씩 이동할 때 포커스가 있어야 하는 포커스 가능 하위 뷰의 ID를 지정합니다. 자세한 내용은 아래 조금씩 이동 바로가기 콘텐츠를 참고하세요.

    (Android 11 QPR3, Android 11 Car, Android 12에서는 지원 중단됨) app:nudgeShortcutapp:nudgeShortcutDirection은 조금씩 이동 바로가기 하나만 지원했습니다.

  4. (Android 11 QPR3, Android 11 Car, Android 12)
    FocusArea에서 둘러싸도록 회전을 사용 설정하려면 app:wrapAroundtrue로 설정하면 됩니다. 뷰가 원형이나 타원형으로 정렬될 때 가장 일반적으로 사용됩니다.
  5. (Android 11 QPR3, Android 11 Car, Android 12)
    FocusArea에서 하이라이트 패딩을 조정하려면 app:highlightPaddingStart, app:highlightPaddingEnd, app:highlightPaddingTop, app:highlightPaddingBottom, app:highlightPaddingHorizontal, app:highlightPaddingVertical을 사용합니다.
  6. (Android 11 QPR3, Android 11 Car, Android 12)
    FocusArea의 인지 범위를 조정하여 조금씩 이동 타겟을 찾으려면 app:startBoundOffset, app:endBoundOffset, app:topBoundOffset, app:bottomBoundOffset, app:horizontalBoundOffset, app:verticalBoundOffset을 사용합니다.
  7. (Android 11 QPR3, Android 11 Car, Android 12)
    주어진 방향으로 인접한 FocusArea의 ID를 명시적으로 지정하려면 app:nudgeLeft, app:nudgeRight, app:nudgeUp, app:nudgeDown을 사용합니다. 기본적으로 사용되는 기하학적 검색을 통해 원하는 타겟을 찾을 수 없을 때 사용하세요.

조금씩 이동은 일반적으로 FocusArea 간에 이동합니다. 그러나 조금씩 이동 바로가기를 사용하면 FocusArea 내에서 먼저 이동할 때가 있으므로 사용자가 다음 FocusArea로 이동하려고 두 번 조금씩 이동해야 할 수도 있습니다. 조금씩 이동 바로가기는 아래 예와 같이 FocusArea에 긴 목록에 이어 플로팅 작업 버튼이 포함되어 있을 때 유용합니다.

조금씩 이동 바로가기
그림 3. 조금씩 이동 바로가기

조금씩 이동 바로가기가 없으면 사용자는 전체 목록을 회전해야 FAB에 도달할 수 있습니다.

포커스 하이라이트 맞춤설정

위에서 언급했듯이 RotaryService는 Android 프레임워크의 기존 뷰 포커스 개념에 기반합니다. 사용자가 회전하거나 조금씩 이동할 때 RotaryService는 포커스를 이동하여 한 뷰에 포커스를 맞추고 다른 뷰의 포커스를 제거합니다. Android에서는 뷰에 포커스가 있을 때 다음과 같습니다.

  • 뷰가 자체 포커스 하이라이트를 지정한 경우 Android는 뷰의 포커스 하이라이트를 그립니다.
  • 뷰가 포커스 하이라이트를 지정하지 않고 기본 포커스 하이라이트가 사용 중지되지 않은 경우 Android는 뷰의 기본 포커스 하이라이트를 그립니다.

터치용으로 설계된 앱은 일반적으로 적절한 포커스 하이라이트를 지정하지 않습니다.

기본 포커스 하이라이트는 Android 프레임워크에서 제공하고 OEM에서 재정의할 수 있습니다. 앱 개발자는 사용하는 테마가 Theme.DeviceDefault에서 파생될 때 이를 수신합니다.

일관된 사용자 환경을 위해 가능하면 기본 포커스 하이라이트를 사용하세요. 맞춤 모양(예: 원형 또는 알약 모양) 포커스 하이라이트가 필요하거나 Theme.DeviceDefault에서 파생되지 않은 테마를 사용한다면 car-ui-library 리소스를 사용하여 각 뷰의 자체 포커스 하이라이트를 지정합니다.

뷰의 맞춤 포커스 하이라이트를 지정하려면 뷰의 배경 또는 전경 드로어블을 뷰에 포커스가 있을 때 달라지는 드로어블로 변경합니다. 일반적으로 배경을 변경합니다. 다음 드로어블은 정사각형 뷰의 배경으로 사용되면 둥근 포커스 하이라이트를 생성합니다.

<selector xmlns:android="http://schemas.android.com/apk/res/android">
   <item android:state_focused="true" android:state_pressed="true">
      <shape android:shape="oval">
         <solid android:color="@color/car_ui_rotary_focus_pressed_fill_color"/>
         <stroke
            android:width="@dimen/car_ui_rotary_focus_pressed_stroke_width"
            android:color="@color/car_ui_rotary_focus_pressed_stroke_color"/>
      </shape>
   </item>
   <item android:state_focused="true">
      <shape android:shape="oval">
         <solid android:color="@color/car_ui_rotary_focus_fill_color"/>
         <stroke
            android:width="@dimen/car_ui_rotary_focus_stroke_width"
            android:color="@color/car_ui_rotary_focus_stroke_color"/>
      </shape>
   </item>
   <item>
      <ripple...>
         ...
      </ripple>
   </item>
</selector>

(Android 11 QPR3, Android 11 Car, Android 12) 위 샘플에서 굵게 표시된 리소스 참조는 car-ui-library에서 정의한 리소스를 식별합니다. OEM은 이를 재정의하여 지정된 기본 포커스 하이라이트와 일관되도록 합니다. 이렇게 하면 사용자가 맞춤 포커스 하이라이트가 있는 뷰와 기본 포커스 하이라이트가 있는 뷰 간에 이동할 때 포커스 하이라이트 색상, 획 너비 등이 변경되지 않습니다. 마지막 항목은 터치에 사용되는 물결 효과입니다. 굵게 표시된 리소스에 사용되는 기본값은 다음과 같이 표시됩니다.

굵게 표시된 리소스의 기본값
그림 4. 굵게 표시된 리소스의 기본값

또한 아래 예와 같이 맞춤 포커스 하이라이트는 사용자의 시선을 끌도록 버튼의 배경을 단색으로 할 때 호출됩니다. 이로 인해 포커스 하이라이트가 잘 표시되지 않을 수 있습니다. 이 상황에서는 보조 색상을 사용하여 맞춤 포커스 하이라이트를 지정하세요.

단색 배경
  • (Android 11 QPR3, Android 11 Car, Android 12)
    car_ui_rotary_focus_fill_secondary_color
    car_ui_rotary_focus_stroke_secondary_color
  • (Android 12)
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

예를 들면 다음과 같습니다.

포커스가 있음, 누르지 않음 포커스가 있음, 누름
포커스가 있음, 누르지 않음 포커스가 있음, 누름

로터리 스크롤

앱에서 RecyclerViews를 사용하면 CarUiRecyclerViews를 대신 사용해야 합니다. 이렇게 하면 UI가 다른 UI와 일관됩니다. OEM의 맞춤설정이 모든 CarUiRecyclerViews에 적용되기 때문입니다.

목록의 요소가 모두 포커스 가능하면 다른 작업을 하지 않아도 됩니다. 로터리 탐색은 목록의 요소를 통해 포커스를 이동하고 목록은 새로 포커스를 둔 요소가 표시되도록 스크롤됩니다.

(Android 11 QPR3, Android 11 Car, Android 12)
포커스 가능 요소와 포커스 불가능 요소가 혼합되어 있거나 모든 요소가 포커스 불가능한 경우 로터리 스크롤을 사용 설정하면 사용자가 로터리 컨트롤러를 사용하여 포커스 불가능 항목을 건너뛰지 않고 목록을 점진적으로 스크롤할 수 있습니다. 로터리 스크롤을 사용 설정하려면 app:rotaryScrollEnabled 속성을 true로 설정합니다.

(Android 11 QPR3, Android 11 Car, Android 12)
CarUiUtilssetRotaryScrollEnabled() 메서드를 사용하여 avCarUiRecyclerView 등 모든 스크롤 가능 뷰에서 로터리 스크롤을 사용 설정할 수 있습니다. 사용 설정하면 다음 작업을 해야 합니다.

  • 스크롤 가능 뷰를 포커스 가능하게 지정하여 표시되는 포커스 가능 하위 뷰가 없을 때 스크롤 가능 뷰에 포커스를 둘 수 있습니다.
  • 스크롤 가능 뷰에 포커스가 맞춰지지 않은 것처럼 보이도록 setDefaultFocusHighlightEnabled(false)를 호출하여 스크롤 가능 뷰의 기본 포커스 하이라이트를 사용 중지합니다.
  • setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS)를 호출하여 하위 요소보다 앞서 스크롤 가능 뷰에 포커스를 맞추도록 합니다.
  • SOURCE_ROTARY_ENCODERAXIS_VSCROLL 또는 AXIS_HSCROLL가 포함된 MotionEvents를 수신 대기하여 스크롤할 거리와 방향을 나타냅니다(기호를 통해).

로터리 스크롤이 CarUiRecyclerView에 사용 설정되어 있고 사용자가 포커스 가능 뷰가 없는 영역으로 회전하면 스크롤바에 포커스가 있다고 나타내는 것처럼 스크롤바가 회색에서 파란색으로 변경됩니다. 원한다면 유사한 효과를 구현할 수 있습니다.

MotionEvents는 소스를 제외하고 마우스의 스크롤 휠로 생성된 것과 같습니다.

직접 조작 모드

일반적으로 조금씩 이동과 회전은 사용자 인터페이스를 이동하고 가운데 버튼은 누르면 실행되긴 하지만 항상 그런 것은 아닙니다. 예를 들어 사용자가 알람 볼륨을 조절하려는 경우 로터리 컨트롤러를 사용하여 볼륨 슬라이더로 이동하고 가운데 버튼을 누른 후 컨트롤러를 회전하여 알람 볼륨을 조절한 다음 뒤로 버튼을 눌러 탐색으로 돌아갈 수 있습니다. 이를 직접 조작(DM) 모드라고 합니다. 이 모드에서는 로터리 컨트롤러가 탐색보다는 뷰와 직접 상호작용하는 데 사용됩니다.

두 가지 방법 중 하나로 DM을 구현합니다. 회전만 처리해야 하고 조작하려는 뷰가 ACTION_SCROLL_FORWARDACTION_SCROLL_BACKWARD AccessibilityEvents에 적절하게 응답하는 경우 간단한 메커니즘을 사용합니다. 그 외 경우에는 고급 메커니즘을 사용하세요.

간단한 메커니즘은 시스템 창의 유일한 옵션으로, 앱은 두 메커니즘 중 하나를 사용할 수 있습니다.

간단한 메커니즘

(Android 11 QPR3, Android 11 Car, Android 12)
앱은 DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable)를 호출해야 합니다. RotaryService는 사용자가 DM 모드일 때를 인식하고 뷰에 포커스가 있는 동안 사용자가 가운데 버튼을 누르면 DM 모드로 전환합니다. DM 모드일 때는 회전이 ACTION_SCROLL_FORWARDACTION_SCROLL_BACKWARD를 실행하고 사용자가 뒤로 버튼을 누르면 DM 모드를 종료합니다. 간단한 메커니즘은 DM 모드를 시작하고 종료할 때 선택된 뷰 상태를 전환합니다.

사용자가 DM 모드에 있다는 시각적 신호를 제공하려면 선택 시 뷰가 다르게 표시되도록 합니다. 예를 들어 android:state_selectedtrue일 때 배경을 변경합니다.

고급 메커니즘

앱은 RotaryService가 DM 모드를 시작하고 종료하는 시점을 결정합니다. 일관된 사용자 환경을 위해 DM 뷰에 포커스가 있는 상태에서 가운데 버튼을 누르면 DM 모드로 전환되고 뒤로 버튼을 누르면 DM 모드가 종료되어야 합니다. 가운데 버튼이나 조금씩 이동이 사용되지 않으면 DM 모드를 종료하는 다른 방법이 될 수 있습니다. 지도와 같은 앱에서는 DM을 나타내는 버튼을 사용하여 DM 모드로 전환할 수 있습니다.

고급 DM 모드를 지원하려면 뷰는 다음과 같아야 합니다.

  1. (Android 11 QPR3, Android 11 Car, Android 12) KEYCODE_DPAD_CENTER 이벤트를 수신 대기하여 DM 모드로 전환하고 KEYCODE_BACK 이벤트를 수신 대기하여 DM 모드를 종료해야 합니다. 각각의 경우에 DirectManipulationHelper.enableDirectManipulationMode()를 호출합니다. 이러한 이벤트를 수신 대기하려면 다음 중 하나를 실행합니다.
    • OnKeyListener를 등록합니다.
    • 또는
    • 뷰를 확장하고 dispatchKeyEvent() 메서드를 재정의합니다.
  2. 뷰가 조금씩 이동을 처리해야 하는 경우 조금씩 이동 이벤트(KEYCODE_DPAD_UP이나 KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT)를 수신 대기해야 합니다.
  3. 뷰가 회전을 처리하려고 하면 MotionEvents를 수신 대기하고 AXIS_SCROLL에서 회전 수를 가져와야 합니다. 다음과 같은 여러 방법이 있습니다.
    1. OnGenericMotionListener를 등록합니다.
    2. 뷰를 확장하고 dispatchTouchEvent() 메서드를 재정의합니다.
  4. DM 모드로 고정되지 않도록 하려면 뷰가 속한 프래그먼트나 활동이 대화형이 아니면 DM 모드를 종료해야 합니다.
  5. 뷰가 DM 모드라고 나타내는 시각적 신호를 제공해야 합니다.

다음은 DM 모드를 사용하여 지도를 이동하거나 확대/축소하는 맞춤 뷰의 샘플입니다.

/** Whether this view is in DM mode. */
private boolean mInDirectManipulationMode;

/** Initializes the view. Called by the constructors. */ private void init() { setOnKeyListener((view, keyCode, keyEvent) -> { boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; switch (keyCode) { // Always consume KEYCODE_DPAD_CENTER and KEYCODE_BACK events. case KeyEvent.KEYCODE_DPAD_CENTER: if (!mInDirectManipulationMode && isActionUp) { mInDirectManipulationMode = true; DirectManipulationHelper.enableDirectManipulationMode(this, true); setSelected(true); // visually indicate DM mode } return true; case KeyEvent.KEYCODE_BACK: if (mInDirectManipulationMode && isActionUp) { mInDirectManipulationMode = false; DirectManipulationHelper.enableDirectManipulationMode(this, false); setSelected(false); } return true; // Consume controller nudge events only when in DM mode. // When in DM mode, nudges pan the map. case KeyEvent.KEYCODE_DPAD_UP: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(0f, -10f); return true; case KeyEvent.KEYCODE_DPAD_DOWN: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(0f, 10f); return true; case KeyEvent.KEYCODE_DPAD_LEFT: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(-10f, 0f); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(10f, 0f); return true; // Don't consume other key events. default: return false; } });
// When in DM mode, rotation zooms the map. setOnGenericMotionListener(((view, motionEvent) -> { if (!mInDirectManipulationMode) return false; float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); zoom(10 * scroll); return true; })); }
@Override public void onPause() { if (mInDirectManipulationMode) { // To ensure that the user doesn't get stuck in DM mode, disable DM mode // when the fragment is not interactive (e.g., a dialog shows up). mInDirectManipulationMode = false; DirectManipulationHelper.enableDirectManipulationMode(this, false); } super.onPause(); }

RotaryPlayground 프로젝트에서 더 많은 예를 확인할 수 있습니다.

ActivityView

ActivityView를 사용하는 경우

  • ActivityView는 포커스 가능하면 안 됩니다.
  • (Android 11 QPR3, Android 11 Car, Android 11에서 지원 중단됨)
    ActivityView의 콘텐츠에는 첫 번째 포커스 가능 뷰로 FocusParkingView가 포함되어야 하고 app:shouldRestoreFocus 속성은 false여야 합니다.
  • ActivityView의 콘텐츠에는 android:focusByDefault 뷰가 없어야 합니다.

사용자의 경우 ActivityViews는 포커스 영역이 ActivityViews를 포괄할 수 없다는 점을 제외하고 탐색에 영향을 미치지 않아야 합니다. 즉, ActivityView 내부 외부에 콘텐츠가 있는 단일 포커스 영역은 있을 수 없습니다. ActivityView에 FocusAreas를 추가하지 않으면 ActivityView의 뷰 계층 구조 루트는 암시적 포커스 영역으로 간주됩니다.

길게 누를 때 작동하는 버튼

대부분의 버튼은 클릭하면 작업이 실행됩니다. 일부 버튼은 대신 길게 누를 때 작동합니다. 예를 들어 빨리 감기와 되감기 버튼은 일반적으로 길게 누를 때 작동합니다. 이러한 버튼이 로터리를 지원하도록 하려면 다음과 같이 KEYCODE_DPAD_CENTER KeyEvents를 수신 대기하세요.

mButton.setOnKeyListener((v, keyCode, event) ->
{
    if (keyCode != KEYCODE_DPAD_CENTER) {
        return false;
    }
    if (event.getAction() == ACTION_DOWN) {
        mButton.setPressed(true);
        mHandler.post(mRunnable);
    } else {
        mButton.setPressed(false);
        mHandler.removeCallbacks(mRunnable);
    }
    return true;
});

여기서 mRunnable은 되감기와 같은 작업을 실행하고 지연 후에 실행되도록 자체적으로 예약합니다.

터치 모드

사용자는 로터리 컨트롤러를 사용하여 두 가지 방법으로 자동차의 헤드 단위와 상호작용할 수 있습니다. 하나는 로터리 컨트롤러를 사용하는 것이고 다른 하나는 화면을 터치하는 것입니다. 로터리 컨트롤러를 사용할 때 포커스 가능 뷰 중 하나가 하이라이트 처리됩니다. 화면을 터치하면 포커스 하이라이트가 표시되지 않습니다. 사용자는 언제든지 다음 입력 모드 간에 전환할 수 있습니다.

  • 로터리 → 터치. 사용자가 화면을 터치하면 포커스 하이라이트가 사라집니다.
  • 터치 → 로터리. 사용자가 가운데 버튼을 조금씩 이동하거나 회전하거나 누르면 포커스 하이라이트가 표시됩니다.

뒤로 버튼과 홈 버튼은 입력 모드에 영향을 미치지 않습니다.

로터리는 Android의 기존 터치 모드 개념에 기반합니다. View.isInTouchMode()를 사용하여 사용자가 사용하는 입력 모드를 파악할 수 있습니다. OnTouchModeChangeListener를 사용하여 변경사항을 수신 대기할 수 있습니다. 현재 입력 모드의 사용자 인터페이스를 맞춤설정하는 데 사용할 수 있지만 주요 변경사항은 피합니다. 혼동을 일으킬 수 있기 때문입니다.

문제 해결

터치용으로 설계된 앱에서는 중첩된 포커스 가능 뷰가 있는 경우가 많습니다. 예를 들어 ImageButton 주위에 FrameLayout이 있을 수 있고 둘 다 포커스 가능합니다. 이는 터치에 해가 되지 않지만 로터리용 사용자 환경이 저하될 수 있습니다. 사용자가 다음 대화형 뷰로 이동하려면 컨트롤러를 두 번 회전해야 하기 때문입니다. 우수한 사용자 환경을 위해 외부 뷰나 내부 뷰 둘 다가 아닌 하나만 포커스 가능하게 지정하는 것이 좋습니다.

로터리 컨트롤러를 통해 눌렀을 때 버튼이나 스위치가 포커스를 잃는 경우 다음 조건 중 하나가 적용될 수 있습니다.

  • 버튼을 눌렀기 때문에 버튼이나 스위치가 잠시 또는 무기한 사용 중지됩니다. 두 경우 모두 다음과 같은 두 가지 방법으로 해결할 수 있습니다.
    • android:enabled 상태를 true로 두고 맞춤 상태를 사용하여 맞춤 상태에서 설명한 대로 버튼이나 스위치를 비활성화합니다.
    • 컨테이너를 사용하여 버튼이나 스위치를 둘러싸고 버튼이나 스위치 대신 컨테이너를 포커스 가능하게 지정합니다. 클릭 리스너가 컨테이너에 있어야 합니다.
  • 버튼이나 스위치가 교체됩니다. 예를 들어 버튼을 누르거나 스위치가 전환될 때 실행되는 작업은 사용 가능한 작업의 새로고침을 트리거하여 새 버튼이 기존 버튼을 대체하게 됩니다. 두 가지 방법으로 이 문제를 해결할 수 있습니다.
    • 새 버튼이나 스위치를 만드는 대신 기존 버튼이나 스위치의 아이콘 또는 텍스트를 설정합니다.
    • 위와 같이 포커스 가능 컨테이너를 버튼이나 스위치 주위에 추가합니다.

RotaryPlayground

RotaryPlayground는 로터리를 위한 참조 앱입니다. 이를 사용하여 로터리 기능을 앱에 통합하는 방법을 알아보세요. RotaryPlayground는 에뮬레이터 빌드 및 Android Automotive OS(AAOS)를 실행하는 기기의 빌드에 포함되어 있습니다.

  • RotaryPlayground 저장소: packages/apps/Car/tests/RotaryPlayground/
  • 버전: Android 11 QPR3, Android 11 Car, Android 12

RotaryPlayground 앱의 왼쪽에 다음 탭이 표시됩니다.

  • 카드. 포커스 영역 탐색과 포커스 불가능 요소 건너뛰기, 텍스트 입력을 테스트합니다.
  • 직접 조작. 고급 및 간단한 직접 조작 모드를 지원하는 위젯을 테스트합니다. 이 탭은 앱 창 내에서 직접 조작하는 용도로만 사용됩니다.
  • Sys UI 조작. 간단한 직접 조작 모드만 지원되는 시스템 창에서 직접 조작을 지원하는 위젯을 테스트합니다.
  • 그리드. 스크롤로 z 패턴 로터리 탐색을 테스트합니다.
  • 알림. 헤드업 알림 내외부로의 조금씩 이동을 테스트합니다.
  • 스크롤. 포커스 가능 콘텐츠와 포커스 불가능 콘텐츠의 스크롤을 테스트합니다.
  • WebView. WebView에서 링크 탐색을 테스트합니다.
  • 맞춤 FocusArea. FocusArea 맞춤설정을 테스트합니다.
    • 랩어라운드
    • android:focusedByDefaultapp:defaultFocus
    • .
    • 명시적인 조금씩 이동 타겟
    • 조금씩 이동 바로가기
    • 포커스 가능 뷰가 없는 FocusArea