Разработка приложений

Следующий материал предназначен для разработчиков приложений.

Чтобы ваше приложение поддерживало ротацию, вы ДОЛЖНЫ:

  1. Поместите FocusParkingView в соответствующий макет активности.
  2. Убедитесь, что представления доступны (или нет) для фокусировки.
  3. Используйте FocusArea s, чтобы обернуть все ваши фокусируемые представления, кроме 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. Нажмите на три точки внизу панели инструментов:

    Доступ к эмулированному поворотному контроллеру
    Рис. 1. Доступ к эмулируемому поворотному контроллеру
  2. Выберите Автомобильный поворот в окне расширенных элементов управления:

    Выберите автомобильный поворотный
    Рисунок 2. Выберите поворотный автомобиль

USB-клавиатура

  • Подключите USB-клавиатуру к своему устройству под управлением Android Automotive OS (AAOS). В некоторых случаях это может помешать отображению экранной клавиатуры.
  • Используйте userdebug или eng .
  • Включить фильтрацию ключевых событий:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • См. таблицу ниже, чтобы найти соответствующую клавишу для каждого действия:
    Ключ Вращательное действие
    Вопрос Вращать против часовой стрелки
    Е Повернуть по часовой стрелке
    А Сдвинуть влево
    Д Сдвинуть вправо
    Вт Подтолкнуть вверх
    С Подтолкнуть вниз
    F или запятая Центральная кнопка
    R или Esc Кнопка назад

Команды АБР

Вы можете использовать команды 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 Несколько раз повернуть против часовой стрелки (100 мс назад и 50 мс назад)
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 — это прозрачное представление в Car UI Library (car-ui-library) . RotaryService использует его для поддержки навигации с помощью поворотного контроллера. FocusParkingView должен быть первым фокусируемым представлением в макете. Он должен быть размещен за пределами всех FocusArea s. Каждое окно должно иметь один FocusParkingView . Если вы уже используете базовый макет car-ui-library, который содержит FocusParkingView , вам не нужно добавлять еще один FocusParkingView . Ниже показан пример FocusParkingView в RotaryPlayground .

<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 опирается на существующую концепцию фокуса представления в среде Android, восходящую к временам, когда в телефонах были физические клавиатуры и крестовины. Существующий атрибут android:nextFocusForward переназначен для поворота (см . Настройка FocusArea ), но android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp и android:nextFocusDown - нет.

RotaryService фокусируется только на представлениях, которые можно сфокусировать. Некоторые представления, такие как Button , обычно могут быть сфокусированы. Другие, такие как TextView и ViewGroup , обычно не являются таковыми. Кликабельные представления автоматически фокусируются, а представления автоматически кликабельны, когда у них есть прослушиватель кликов. Если эта автоматическая логика приводит к желаемой фокусируемости, вам не нужно явно устанавливать фокусируемость представления. Если автоматическая логика не приводит к желаемой фокусируемости, установите для атрибута android:focusable значение true или false или программно установите фокусируемость представления с помощью View.setFocusable(boolean) . Чтобы RotaryService сосредоточился на этом, представление ДОЛЖНО соответствовать следующим требованиям:

  • Фокусируемый
  • Включено
  • Видимый
  • Иметь ненулевые значения ширины и высоты

Если представление не соответствует всем этим требованиям, например, кнопка с возможностью фокусировки, но отключена, пользователь не может использовать поворотный элемент управления, чтобы сфокусироваться на нем. Если вы хотите сосредоточиться на отключенных представлениях, рассмотрите возможность использования пользовательского состояния, а не 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. Заставьте обработчик кликов вашего представления работать по-разному в зависимости от его состояния. Например, обработчик кликов может ничего не делать или может отображать всплывающее уведомление, когда mRotaryEnabled имеет значение false .
  6. Чтобы кнопка отображалась отключенной, в фоновом режиме вашего представления используйте app:state_rotary_enabled вместо android:state_enabled . Если у вас его еще нет, вам нужно добавить:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Если ваше представление отключено в каких-либо макетах, замените android:enabled="false" на app:state_rotary_enabled="false" , а затем добавьте пространство имен app , как указано выше.
  8. Если ваше представление отключено программно, замените вызовы setEnabled() вызовами setRotaryEnabled() .

Зона фокусировки

Используйте FocusAreas для разделения фокусируемых представлений на блоки, чтобы упростить навигацию и обеспечить согласованность с другими приложениями. Например, если в вашем приложении есть панель инструментов, она должна находиться в FocusArea , отдельной от остального приложения. Панели вкладок и другие элементы навигации также должны быть отделены от остальной части приложения. Большие списки, как правило, должны иметь свою собственную FocusArea . В противном случае пользователи должны просмотреть весь список, чтобы получить доступ к некоторым представлениям.

FocusArea является подклассом LinearLayout в библиотеке car-ui-library. Когда эта функция включена, FocusArea будет подсвечиваться, когда фокусируется один из его потомков. Дополнительные сведения см. в разделе Настройка выделения фокуса .

При создании блока навигации в файле макета, если вы собираетесь использовать LinearLayout в качестве контейнера для этого блока, используйте вместо него FocusArea . В противном случае оберните блок в FocusArea .

НЕ FocusArea в другой FocusArea . Это приведет к неопределенному поведению навигации. Убедитесь, что все фокусируемые представления вложены в FocusArea .

Пример FocusArea в RotaryPlayground показан ниже:

<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

Для настройки поворотной навигации можно использовать два стандартных атрибута View:

  • android:nextFocusForward позволяет разработчикам приложений указывать порядок вращения в области фокуса. Это тот же атрибут, который используется для управления порядком табуляции для навигации с помощью клавиатуры. НЕ используйте этот атрибут для создания цикла. Вместо этого используйте app:wrapAround (см. ниже), чтобы создать цикл.
  • android:focusedByDefault позволяет разработчикам приложений указать представление фокуса по умолчанию в окне. НЕ используйте этот атрибут и app:defaultFocus (см. ниже) в одном и том же FocusArea .

FocusArea также определяет некоторые атрибуты для настройки поворотной навигации. Неявные области фокуса нельзя настроить с помощью этих атрибутов.

  1. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    app:defaultFocus можно использовать для указания идентификатора фокусируемого представления-потомка, на котором следует сфокусироваться, когда пользователь подталкивает к этому FocusArea .
  2. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    app:defaultFocusOverridesHistory можно установить значение true , чтобы указанное выше представление было в фокусе, даже если с историей, указывающей на другое представление в этой FocusArea , на котором было сфокусировано внимание.
  3. ( Андроид 12 )
    Используйте app:nudgeLeftShortcut , app:nudgeRightShortcut , app:nudgeUpShortcut и app:nudgeDownShortcut , чтобы указать идентификатор фокусируемого представления-потомка, на котором следует сосредоточиться, когда пользователь перемещается в заданном направлении. Чтобы узнать больше, ознакомьтесь с содержимым быстрых клавиш для перемещения ниже.

    ( Android 11 QPR3, Android 11 Car, устарело в Android 12 ) app:nudgeShortcut и app:nudgeShortcutDirection поддерживали только один ярлык подталкивания.

  4. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    Чтобы включить циклическое вращение в этом FocusArea , app:wrapAround можно установить значение true . Это чаще всего используется, когда представления расположены в круге или овале.
  5. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    Чтобы настроить отступ выделения в этом FocusArea , используйте app:highlightPaddingStart , app:highlightPaddingEnd , app:highlightPaddingTop , app:highlightPaddingBottom , app:highlightPaddingHorizontal и app:highlightPaddingVertical .
  6. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    Чтобы настроить воспринимаемые границы этого FocusArea для поиска цели смещения, используйте app:startBoundOffset , app:endBoundOffset , app:topBoundOffset , app:bottomBoundOffset , app:horizontalBoundOffset и app:verticalBoundOffset .
  7. ( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
    Чтобы явно указать идентификатор соседнего FocusArea (или областей) в заданных направлениях, используйте app:nudgeLeft , app:nudgeRight , app:nudgeUp и app:nudgeDown . Используйте это, когда геометрический поиск, используемый по умолчанию, не находит желаемую цель.

Подталкивание обычно перемещает между FocusAreas. Но с быстрыми перемещениями подталкивание иногда сначала перемещается внутри 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 для автомобилей, Android 12 )
    car_ui_rotary_focus_fill_secondary_color
    car_ui_rotary_focus_stroke_secondary_color
  • ( Андроид 12 )
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

Например:

Сосредоточено, не нажатоСосредоточенный, нажатый
Сосредоточено, не нажато Сосредоточенный, нажатый

Вращающаяся прокрутка

Если ваше приложение использует RecyclerView , вам СЛЕДУЕТ использовать вместо него CarUiRecyclerView . Это гарантирует, что ваш пользовательский интерфейс совместим с другими, поскольку настройка OEM применяется ко всем CarUiRecyclerView s.

Если все элементы в вашем списке доступны для фокусировки, вам больше ничего не нужно делать. Вращающаяся навигация перемещает фокус по элементам в списке, а список прокручивается, чтобы сделать видимым только что выбранный элемент.

( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
Если есть сочетание фокусируемых и нефокусируемых элементов или если все элементы не фокусируются, вы можете включить вращающуюся прокрутку, которая позволяет пользователю использовать поворотный контроллер для постепенной прокрутки списка, не пропуская нефокусируемые элементы. Чтобы включить вращающуюся прокрутку, установите для атрибута app:rotaryScrollEnabled значение true .

( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
Вы можете включить вращающуюся прокрутку в любом прокручиваемом представлении, включая av CarUiRecyclerView , с помощью setRotaryScrollEnabled() в CarUiUtils . Если вы это сделаете, вам необходимо:

  • Сделайте прокручиваемый вид фокусируемым, чтобы на него можно было сфокусироваться, когда ни одно из его фокусируемых представлений-потомков не видно,
  • Отключите выделение фокуса по умолчанию в прокручиваемом представлении, вызвав setDefaultFocusHighlightEnabled(false) , чтобы прокручиваемое представление не выглядело сфокусированным,
  • Убедитесь, что прокручиваемое представление сфокусировано перед его потомками, вызвав setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Слушайте MotionEvents с SOURCE_ROTARY_ENCODER и либо AXIS_VSCROLL либо AXIS_HSCROLL , чтобы указать расстояние для прокрутки и направление (через знак).

Когда поворотная прокрутка включена в CarUiRecyclerView , и пользователь поворачивается к области, где нет фокусируемых представлений, полоса прокрутки меняется с серой на синюю, как будто указывая на то, что полоса прокрутки сфокусирована. Вы можете реализовать аналогичный эффект, если хотите.

MotionEvents такие же, как те, которые генерируются колесом прокрутки мыши, за исключением источника.

Режим прямого манипулирования

Обычно перемещение по пользовательскому интерфейсу осуществляется с помощью подталкивания и поворота, а нажатие центральной кнопки приводит к действию, хотя это не всегда так. Например, если пользователь хочет отрегулировать громкость будильника, он может использовать поворотный контроллер, чтобы перейти к ползунку громкости, нажать центральную кнопку, повернуть контроллер, чтобы отрегулировать громкость будильника, а затем нажать кнопку «Назад», чтобы вернуться к навигации. . Это называется режимом прямого управления (DM) . В этом режиме поворотный контроллер используется для непосредственного взаимодействия с видом, а не для навигации.

Реализуйте DM одним из двух способов. Если вам нужно только обработать вращение, а представление, которым вы хотите манипулировать, соответствующим образом реагирует на ACTION_SCROLL_FORWARD и ACTION_SCROLL_BACKWARD AccessibilityEvent , используйте простой механизм. В противном случае используйте расширенный механизм.

Простой механизм — единственный вариант в системных окнах; приложения могут использовать любой механизм.

Простой механизм

( Android 11 QPR3, Android 11 для автомобилей, Android 12 )
Ваше приложение должно вызывать DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable) . RotaryService распознает, когда пользователь находится в режиме DM, и входит в режим DM, когда пользователь нажимает центральную кнопку, когда вид находится в фокусе. В режиме DM повороты выполняют ACTION_SCROLL_FORWARD или ACTION_SCROLL_BACKWARD и выходят из режима DM, когда пользователь нажимает кнопку «Назад». Простой механизм переключает выбранное состояние представления при входе и выходе из режима DM.

Чтобы дать визуальную подсказку о том, что пользователь находится в режиме DM, измените вид при выборе. Например, измените фон, когда android:state_selected имеет значение true .

Расширенный механизм

Приложение определяет, когда 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. СЛЕДУЕТ прослушивать MotionEvent и получать счетчик вращения в 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 не должен фокусироваться.
  • ( Android 11 QPR3, Android 11 Car, устарело в Android 11 )
    Содержимое ActivityView ДОЛЖНО содержать FocusParkingView в качестве первого фокусируемого представления, а его атрибут app:shouldRestoreFocus ДОЛЖЕН иметь значение false .
  • Содержимое ActivityView не должно иметь представлений android:focusByDefault .

Для пользователя представления ActivityView не должны влиять на навигацию, за исключением того, что области фокуса не могут охватывать представления ActivityView. Другими словами, у вас не может быть единой фокусной области с содержимым внутри и вне ActivityView . Если вы не добавите FocusAreas в свой ActivityView , корень иерархии представлений в 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 оба из которых могут быть сфокусированы. Это не повредит сенсорному управлению, но может привести к плохому взаимодействию с пользователем при повороте, поскольку пользователю придется дважды повернуть контроллер, чтобы перейти к следующему интерактивному представлению. Для удобства пользователей Google рекомендует делать фокусируемым либо внешнее, либо внутреннее представление, но не то и другое одновременно.

Если кнопка или переключатель теряет фокус при нажатии на поворотный контроллер, может иметь место одно из следующих условий:

  • Кнопка или переключатель отключены (на короткое время или на неопределенный срок) из-за нажатия кнопки. В любом случае есть два способа решить эту проблему:
    • Оставьте состояние android:enabled равным true и используйте пользовательское состояние, чтобы сделать кнопку или переключатель серым, как описано в разделе Пользовательское состояние .
    • Используйте контейнер, чтобы окружить кнопку или переключатель и сделайте контейнер фокусируемым вместо кнопки или переключателя. (Прослушиватель кликов должен находиться в контейнере.)
  • Выполняется замена кнопки или переключателя. Например, действие, выполняемое при нажатии кнопки или переключении переключателя, может вызвать обновление доступных действий, в результате чего новые кнопки заменят существующие кнопки. Есть два способа решить эту проблему:
    • Вместо создания новой кнопки или переключателя установите значок и/или текст существующей кнопки или переключателя.
    • Как и выше, добавьте фокусируемый контейнер вокруг кнопки или переключателя.

РотариИгровая площадка

RotaryPlayground — эталонное приложение для роторных игр. Используйте его, чтобы узнать, как интегрировать поворотные функции в свои приложения. RotaryPlayground включен в сборки эмулятора и сборки для устройств под управлением Android Automotive OS (AAOS).

  • Репозиторий RotaryPlayground : packages/apps/Car/tests/RotaryPlayground/
  • Версии: Android 11 QPR3, Android 11 Car и Android 12.

Приложение RotaryPlayground показывает следующие вкладки слева:

  • Карты. Протестируйте навигацию по областям фокусировки, пропуская элементы, не находящиеся в фокусе, и ввод текста.
  • Прямая манипуляция. Протестируйте виджеты, поддерживающие простой и расширенный режим прямого манипулирования. Эта вкладка специально предназначена для прямого управления в окне приложения.
  • Управление системным интерфейсом. Тестируйте виджеты, поддерживающие прямое манипулирование в системных окнах, где поддерживается только простой режим прямого манипулирования.
  • Сетка. Протестируйте поворотную навигацию по z-шаблону с прокруткой.
  • Уведомление. Протестируйте включение и выключение хедз-ап уведомлений.
  • Прокрутите. Протестируйте прокрутку сочетания содержимого, на которое можно сфокусироваться, и содержимого, на которое нельзя сфокусироваться.
  • Веб-просмотр. Протестируйте навигацию по ссылкам в WebView .
  • Пользовательская FocusArea . Протестируйте настройку FocusArea :
    • Обертывание.
    • android:focusedByDefault и app:defaultFocus
    • .
    • Явные цели подталкивания.
    • Подтолкнуть ярлыки.
    • FocusArea без фокусируемых представлений.
,

Следующий материал предназначен для разработчиков приложений.

Чтобы ваше приложение поддерживало ротацию, вы ДОЛЖНЫ:

  1. Поместите FocusParkingView в соответствующий макет активности.
  2. Убедитесь, что представления доступны (или нет) для фокусировки.
  3. Используйте FocusArea s, чтобы обернуть все ваши фокусируемые представления, кроме 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. Нажмите на три точки внизу панели инструментов:

    Доступ к эмулированному поворотному контроллеру
    Рис. 1. Доступ к эмулируемому поворотному контроллеру
  2. Выберите Автомобильный поворот в окне расширенных элементов управления:

    Выберите автомобильный поворотный
    Рисунок 2. Выберите поворотный автомобиль

USB-клавиатура

  • Подключите USB-клавиатуру к своему устройству под управлением Android Automotive OS (AAOS). В некоторых случаях это может помешать отображению экранной клавиатуры.
  • Используйте userdebug или eng .
  • Включить фильтрацию ключевых событий:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • См. таблицу ниже, чтобы найти соответствующую клавишу для каждого действия:
    Ключ Вращательное действие
    Вопрос Вращать против часовой стрелки
    Е Повернуть по часовой стрелке
    А Сдвинуть влево
    Д Сдвинуть вправо
    Вт Подтолкнуть вверх
    С Подтолкнуть вниз
    F или запятая Центральная кнопка
    R или Esc Кнопка назад

Команды АБР

Вы можете использовать команды 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 Несколько раз повернуть против часовой стрелки (100 мс назад и 50 мс назад)
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 — это прозрачное представление в Car UI Library (car-ui-library) . RotaryService использует его для поддержки навигации с помощью поворотного контроллера. FocusParkingView должен быть первым фокусируемым представлением в макете. Он должен быть размещен за пределами всех FocusArea s. Каждое окно должно иметь один FocusParkingView . Если вы уже используете базовый макет car-ui-library, который содержит FocusParkingView , вам не нужно добавлять еще один FocusParkingView . Ниже показан пример FocusParkingView в RotaryPlayground .

<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 опирается на существующую концепцию фокуса представления в среде Android, восходящую к временам, когда в телефонах были физические клавиатуры и крестовины. Существующий атрибут android:nextFocusForward переназначен для поворота (см . Настройка FocusArea ), но android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp и android:nextFocusDown - нет.

RotaryService фокусируется только на представлениях, которые можно сфокусировать. Некоторые представления, такие как Button , обычно могут быть сфокусированы. Другие, такие как TextView и ViewGroup , обычно не являются таковыми. Кликабельные представления автоматически фокусируются, а представления автоматически кликабельны, когда у них есть прослушиватель кликов. Если эта автоматическая логика приводит к желаемой фокусируемости, вам не нужно явно устанавливать фокусируемость представления. Если автоматическая логика не приводит к желаемой фокусируемости, установите для атрибута android:focusable значение true или false или программно установите фокусируемость представления с помощью View.setFocusable(boolean) . Чтобы RotaryService сосредоточился на этом, представление ДОЛЖНО соответствовать следующим требованиям:

  • Фокусируемый
  • Включено
  • Видимый
  • Иметь ненулевые значения ширины и высоты

Если представление не соответствует всем этим требованиям, например, кнопка с возможностью фокусировки, но отключена, пользователь не может использовать поворотный элемент управления, чтобы сфокусироваться на нем. Если вы хотите сосредоточиться на отключенных представлениях, рассмотрите возможность использования пользовательского состояния, а не 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. Заставьте обработчик кликов вашего представления работать по-разному в зависимости от его состояния. Например, обработчик кликов может ничего не делать или может отображать всплывающее уведомление, когда mRotaryEnabled имеет значение false .
  6. Чтобы кнопка отображалась отключенной, в фоновом режиме вашего представления используйте app:state_rotary_enabled вместо android:state_enabled . Если у вас его еще нет, вам нужно добавить:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Если ваше представление отключено в каких-либо макетах, замените android:enabled="false" на app:state_rotary_enabled="false" , а затем добавьте пространство имен app , как указано выше.
  8. Если ваше представление отключено программно, замените вызовы setEnabled() вызовами setRotaryEnabled() .

Зона фокусировки

Используйте FocusAreas для разделения фокусируемых представлений на блоки, чтобы упростить навигацию и обеспечить согласованность с другими приложениями. Например, если в вашем приложении есть панель инструментов, она должна находиться в FocusArea , отдельной от остального приложения. Панели вкладок и другие элементы навигации также должны быть отделены от остальной части приложения. Большие списки, как правило, должны иметь свою собственную FocusArea . В противном случае пользователи должны просмотреть весь список, чтобы получить доступ к некоторым представлениям.

FocusArea является подклассом LinearLayout в библиотеке car-ui-library. Когда эта функция включена, FocusArea будет подсвечиваться, когда фокусируется один из его потомков. Дополнительные сведения см. в разделе Настройка выделения фокуса .

При создании блока навигации в файле макета, если вы собираетесь использовать LinearLayout в качестве контейнера для этого блока, используйте вместо него FocusArea . В противном случае оберните блок в FocusArea .

НЕ FocusArea в другой FocusArea . Это приведет к неопределенному поведению навигации. Убедитесь, что все фокусируемые представления вложены в FocusArea .

Пример FocusArea в RotaryPlayground показан ниже:

<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 works as follows:

  1. When handling rotate and nudge actions, RotaryService looks for instances of FocusArea in the view hierarchy.
  2. When receiving a rotation event, RotaryService moves focus to another View that can take focus in the same FocusArea .
  3. When receiving a nudge event, RotaryService move focus to another view that can take focus in another (typically adjacent) FocusArea .

If you don't include any FocusAreas in your layout, the root view is treated as an implicit focus area. The user can't nudge to navigate in the app. Instead, they'll rotate through all focusable views, which may be adequate for dialogs.

FocusArea customization

Two standard View attributes can be used to customize rotary navigation:

  • android:nextFocusForward allows app developers to specify the rotation order in a focus area. This is the same attribute used to control the Tab order for keyboard navigation. Do NOT use this attribute to create a loop. Instead, use app:wrapAround (see below) to create a loop.
  • android:focusedByDefault allows app developers to specify the default focus view in the window. Do NOT use this attribute and app:defaultFocus (see below) in the same FocusArea .

FocusArea also defines some attributes to customize rotary navigation. Implicit focus areas can't be customized with these attributes.

  1. ( Android 11 QPR3, Android 11 Car, Android 12 )
    app:defaultFocus can be used to specify the ID of a focusable descendant view, which should be focused on when the user nudges to this FocusArea .
  2. ( Android 11 QPR3, Android 11 Car, Android 12 )
    app:defaultFocusOverridesHistory can be set to true to make the view specified above take focus even if with history to indicate another view in this FocusArea had been focused on.
  3. ( Android 12 )
    Use app:nudgeLeftShortcut , app:nudgeRightShortcut , app:nudgeUpShortcut , and app:nudgeDownShortcut to specify the ID of a focusable descendant view, which should be focused on when the user nudges in a given direction. To learn more, see the content for nudge shortcuts below.

    ( Android 11 QPR3, Android 11 Car, deprecated in Android 12 ) app:nudgeShortcut and app:nudgeShortcutDirection supported only one nudge shortcut.

  4. ( Android 11 QPR3, Android 11 Car, Android 12 )
    To enable rotation to wrap around in this FocusArea , app:wrapAround can be set to true . This is most typically used when views are arranged in a circle or oval.
  5. ( Android 11 QPR3, Android 11 Car, Android 12 )
    To adjust the padding of the highlight in this FocusArea , use app:highlightPaddingStart , app:highlightPaddingEnd , app:highlightPaddingTop , app:highlightPaddingBottom , app:highlightPaddingHorizontal , and app:highlightPaddingVertical .
  6. ( Android 11 QPR3, Android 11 Car, Android 12 )
    To adjust the perceived bounds of this FocusArea to find a nudge target, use app:startBoundOffset , app:endBoundOffset , app:topBoundOffset , app:bottomBoundOffset , app:horizontalBoundOffset , and app:verticalBoundOffset .
  7. ( Android 11 QPR3, Android 11 Car, Android 12 )
    To explicitly specify the ID of an adjacent FocusArea (or areas) in the given directions, use app:nudgeLeft , app:nudgeRight , app:nudgeUp , and app:nudgeDown . Use this when the geometric search used by default doesn't find the desired target.

Nudging usually navigates between FocusAreas. But with nudge shortcuts, nudging sometimes first navigates within a FocusArea so that the user may need to nudge twice to navigate to the next FocusArea . Nudge shortcuts are useful when a FocusArea contains a long list followed by a Floating Action Button , as in the example below:

Nudge shortcut
Figure 3. Nudge shortcut

Without the nudge shortcut, the user would have to rotate through the entire list to reach the FAB.

Focus highlight customization

As noted above, RotaryService builds upon the Android framework's existing concept of view focus. When the user rotates and nudges, RotaryService moves the focus around, focusing one view and unfocusing another. In Android, when a view is focused, if the view:

  • has specified its own focus highlight, Android draws the view's focus highlight.
  • doesn't specify a focus highlight, and the default focus highlight is not disabled, Android draws the default focus highlight for the view.

Apps designed for touch usually don't specify the appropriate focus highlights.

The default focus highlight is provided by the Android framework and can be overridden by the OEM. App developers receive it when the theme they're using is derived from Theme.DeviceDefault .

For a consistent user experience, rely on the default focus highlight whenever possible. If you need a custom-shaped (for example, round or pill-shaped) focus highlight, or if you're using a theme not derived from Theme.DeviceDefault , use the car-ui-library resources to specify your own focus highlight for each view.

To specify a custom focus highlight for a view, change the background or foreground drawable of the view to a drawable that differs when the view is focused on. Typically, you'd change the background. The following drawable, if used as the background for a square view, produces a round focus highlight:

<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 ) Bold resource references in the sample above identify resources defined by the car-ui-library. The OEM overrides these to be consistent with the default focus highlight they specify. This ensures that the focus highlight color, stroke width, and so on don't change when the user navigates between a view with a custom focus highlight and a view with the default focus highlight. The last item is a ripple used for touch. Default values used for the bold resources appear as follows:

Default values for bold resources
Figure 4. Default values for bold resources

In addition, a custom focus highlight is called for when a button is given a solid background color to bring it to the user's attention, as in the example below. This can make the focus highlight difficult to see. In this situation, specify a custom focus highlight using secondary colors:

Solid background color
  • ( 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

Например:

Focused, not pressedFocused, pressed
Focused, not pressed Focused, pressed

Rotary scrolling

If your app uses RecyclerView s, you SHOULD use CarUiRecyclerView s instead. This ensures that your UI is consistent with others because an OEM's customization applies to all CarUiRecyclerView s.

If the elements in your list are all focusable, you needn't do anything else. Rotary navigation moves the focus through the elements in the list and the list scrolls to make the newly focused element visible.

( Android 11 QPR3, Android 11 Car, Android 12 )
If there is a mix of focusable and unfocusable elements, or if all the elements are unfocusable, you can enable rotary scrolling, which allows the user to use the rotary controller to gradually scroll through the list without skipping unfocusable items. To enable rotary scrolling, set the app:rotaryScrollEnabled attribute to true .

( Android 11 QPR3, Android 11 Car, Android 12 )
You can enable rotary scrolling in any scrollable view, including av CarUiRecyclerView , with the setRotaryScrollEnabled() method in CarUiUtils . If you do so, you need to:

  • Make the scrollable view focusable so that it can be focused on when none of its focusable descendant views are visible,
  • Disable the default focus highlight on the scrollable view by calling setDefaultFocusHighlightEnabled(false) so that the scrollable view doesn't appear to be focused,
  • Ensure that the scrollable view is focused on before its descendants by calling setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Listen for MotionEvents with SOURCE_ROTARY_ENCODER and either AXIS_VSCROLL or AXIS_HSCROLL to indicate the distance to scroll and the direction (through the sign).

When rotary scrolling is enabled on a CarUiRecyclerView and the user rotates to an area where no focusable views are present, the scrollbar changes from gray to blue, as if to indicate the scrollbar is focused. You can implement a similar effect if you like.

The MotionEvents are the same as those generated by a scroll wheel on a mouse, except for the source.

Direct manipulation mode

Normally, nudges and rotation navigate through the user interface, while Center button presses take action, though this isn't always the case. For example, if a user wants to adjust the alarm volume, they might use the rotary controller to navigate to the volume slider, press the Center button, rotate the controller to adjust the alarm volume, and then press the Back button to return to navigation. This is referred to as direct manipulation (DM) mode. In this mode, the rotary controller is used to interact with the view directly rather than to navigate.

Implement DM in one of two ways. If you only need to handle rotation and the view you want to manipulate responds to ACTION_SCROLL_FORWARD and ACTION_SCROLL_BACKWARD AccessibilityEvent s appropriately, use the simple mechanism. Otherwise, use the advanced mechanism.

The simple mechanism is the only option in system windows; apps can use either mechanism.

Simple mechanism

( Android 11 QPR3, Android 11 Car, Android 12 )
Your app should call DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable) . RotaryService recognizes when the user is in DM mode and enters DM mode when the user presses the Center button while a view is focused. When in DM mode, rotations perform ACTION_SCROLL_FORWARD or ACTION_SCROLL_BACKWARD and exits DM mode when the user presses the Back button. The simple mechanism toggles the selected state of the view when entering and exiting DM mode.

To provide a visual cue that the user is in DM mode, make your view appear different when selected. For example, change the background when android:state_selected is true .

Advanced mechanism

The app determines when RotaryService enters and exits DM mode. For a consistent user experience, pressing the Center button with a DM view focused should enter DM mode and the Back button should exit DM mode. If the Center button and/or nudge aren't used, they can be alternative ways to exit DM mode. For apps such as Maps, a button to represent DM can be used to enter DM mode.

To support advanced DM mode, a view:

  1. ( Android 11 QPR3, Android 11 Car, Android 12 ) MUST listen for a KEYCODE_DPAD_CENTER event to enter DM mode and listen for a KEYCODE_BACK event to exit DM mode, calling DirectManipulationHelper.enableDirectManipulationMode() in each case. To listen for these events, do one of the following:
    • Register an OnKeyListener .
    • or,
    • Extend the view and then override its dispatchKeyEvent() method.
  2. SHOULD listen for nudge events ( KEYCODE_DPAD_UP , KEYCODE_DPAD_DOWN , KEYCODE_DPAD_LEFT , or KEYCODE_DPAD_RIGHT ) if the view should handle nudges.
  3. SHOULD listen to MotionEvent s and get rotation count in AXIS_SCROLL if the view wants to handle rotation. There are several ways to do this:
    1. Register an OnGenericMotionListener .
    2. Extend the view and override its dispatchTouchEvent() method.
  4. To avoid being stuck in DM mode, MUST exit DM mode when the Fragment or Activity the view belongs to is not interactive.
  5. SHOULD provide a visual cue to indicate that the view is in DM mode.

A sample of a custom view that uses DM mode to pan and zoom a map is provided below:

/** 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(); }

More examples can be found in the RotaryPlayground project.

ActivityView

When using an ActivityView:

  • The ActivityView should not be focusable.
  • ( Android 11 QPR3, Android 11 Car, deprecated in Android 11 )
    The contents of the ActivityView MUST contain a FocusParkingView as the first focusable view, and its app:shouldRestoreFocus attribute MUST be false .
  • The contents of the ActivityView should have no android:focusByDefault views.

For the user, ActivityViews should have no effect on navigation except that focus areas can't span ActivityViews. In other words, you can't have a single focus area that has content inside and outside an ActivityView . If you don't add any FocusAreas to your ActivityView , the root of the view hierarchy in the ActivityView is considered an implicit focus area.

Buttons that operate when held down

Most buttons cause some action when clicked. Some buttons operate when held down instead. For example, the Fast Forward and Rewind buttons typically operate when held down. To make such buttons support rotary, listen for KEYCODE_DPAD_CENTER KeyEvents as follows:

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;
});

In which mRunnable takes an action (such as rewinding) and schedules itself to be run after a delay.

Touch mode

Users can use a rotary controller to interact with the head unit in a car in two ways, either by using the rotary controller or by touching the screen. When using the rotary controller, one of the focusable views will be highlighted. When touching the screen, no focus highlight appears. The user can switch between these input modes at any time:

  • Rotary → touch. When the user touches the screen, the focus highlight disappears.
  • Touch → rotary. When the user nudges, rotates, or presses the Center button, the focus highlight appears.

The Back and Home buttons have no effect on the input mode.

Rotary piggybacks on Android's existing concept of touch mode . You can use View.isInTouchMode() to determine which input mode the user is using. You can use OnTouchModeChangeListener to listen for changes. While this can be used to customize your user interface for the current input mode, avoid any major changes as they can be disconcerting.

Troubleshooting

In an app designed for touch, it's not uncommon to have nested focusable views. For example, there may be a FrameLayout around an ImageButton , both of which are focusable. This does no harm for touch but it can result in a poor user experience for rotary because the user must rotate the controller twice to move to the next interactive view. For a good user experience, Google recommends you make either the outer view or the inner view focusable, but not both.

If a button or switch loses focus when pressed through the rotary controller, one of these conditions may apply:

  • The button or switch is being disabled (briefly or indefinitely) due to the button being pressed. In either case, there are two ways to address this:
    • Leave the android:enabled state as true and use a custom state to gray out the button or switch as described in Custom State .
    • Use a container to surround the button or switch and make the container focusable instead of the button or switch. (The click listener must be on the container.)
  • The button or switch is being replaced. For example, the action taken when the button is pressed or the switch is toggled may trigger a refresh of the available actions causing new buttons to replace existing buttons. There are two ways to address this:
    • Instead of creating a new button or switch, set the icon and/or text of the existing button or switch.
    • As above, add a focusable container around the button or switch.

RotaryPlayground

RotaryPlayground is a reference app for rotary. Use it to learn how to integrate rotary features into your apps. RotaryPlayground is included in emulator builds and in builds for devices that run Android Automotive OS (AAOS).

  • RotaryPlayground repository: packages/apps/Car/tests/RotaryPlayground/
  • Versions: Android 11 QPR3, Android 11 Car, and Android 12

The RotaryPlayground app shows the following tabs on the left:

  • Cards. Test navigating around focus areas, skipping unfocusable elements and text input.
  • Direct Manipulation. Test widgets that support simple and advanced direct manipulation mode. This tab is specifically for direct manipulation within the app window.
  • Sys UI Manipulation. Test widgets that support direct manipulation in system windows where only simple direct manipulation mode is supported.
  • Grid. Test z-pattern rotary navigation with scrolling.
  • Notification. Test nudging in and out of heads-up notifications.
  • Scroll. Test scrolling through a mix of focusable and unfocusable content.
  • WebView. Test navigating through links in a WebView .
  • Custom FocusArea . Test FocusArea customization:
    • Wrap-around.
    • android:focusedByDefault and app:defaultFocus
    • .
    • Explicit nudge targets.
    • Nudge shortcuts.
    • FocusArea with no focusable views.