Develop apps

The following material is for app developers.

To make your app support rotary, you MUST:

  1. Place a FocusParkingView in the respective activity layout.
  2. Ensure the views that are (or aren't) focusable.
  3. Use FocusAreas to wrap around all your focusable views, except the FocusParkingView.

Each of these tasks is detailed below, after you are set up your environment to develop rotary-enabled apps.

Set up a rotary controller

Before you can start developing rotary-enabled apps, you need either a rotary controller or a stand-in. You have the options described below.


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

You can also use aosp_car_x86_64-userdebug.

To access the emulated rotary controller:

  1. Tap on the three dots at the bottom of the toolbar:

    Access emulated rotary controller
    Figure 1. Access emulated rotary controller
  2. Select Car rotary in the extended controls window:

    Select Car rotary
    Figure 2. Select Car rotary

USB keyboard

  • Plug a USB keyboard into your device that runs Android Automotive OS (AAOS), In some cases, this prevents the on-screen keyboard from appearing.
  • Use a userdebug or eng build.
  • Enable key event filtering:
    adb shell settings put secure 1
  • See the table below to find the corresponding key for each action:
    Key Rotary action
    Q Rotate counterclockwise
    E Rotate clockwise
    A Nudge left
    D Nudge right
    W Nudge up
    S Nudge down
    F or Comma Center button
    R or Esc Back button

ADB commands

You can use car_service commands to inject rotary input events. These commands can be run on devices that run Android Automotive OS (AAOS) or on an emulator.

car_service commands Rotary input
adb shell cmd car_service inject-rotary Rotate counterclockwise
adb shell cmd car_service inject-rotary -c true Rotate clockwise
adb shell cmd car_service inject-rotary -dt 100 50 Rotate counterclockwise multiple times (100 ms ago and 50 ms ago)
adb shell cmd car_service inject-key 282 Nudge left
adb shell cmd car_service inject-key 283 Nudge right
adb shell cmd car_service inject-key 280 Nudge up
adb shell cmd car_service inject-key 281 Nudge down
adb shell cmd car_service inject-key 23 Center button click
adb shell input keyevent inject-key 4 Back button click

OEM rotary controller

When your rotary controller hardware is up and running, this is the most realistic option. It's particularly useful for testing fast rotation.


FocusParkingView is a transparent view in the Car UI Library (car-ui-library). RotaryService uses it to support rotary controller navigation. FocusParkingView must be the first focusable view in the layout. It must be placed outside of all FocusAreas. Each window must have one FocusParkingView. If you're already using the car-ui-library base layout, which contains a FocusParkingView, you don't need to add another FocusParkingView. Shown below is an example of FocusParkingView in RotaryPlayground.


Here are the reasons you need a FocusParkingView:

  1. Android doesn't clear focus automatically when focus is set in another window. If you try to clear focus in the previous window, Android refocuses a view in that window, which results in two windows being focused on simultaneously. Adding a FocusParkingView to each window can fix this problem. This view is transparent and its default focus highlight is disabled, so that it's invisible to the user no matter whether focused or not. It can take focus so that RotaryService can park the focus on it to remove the focus highlight.
  2. If there is only one FocusArea in the current window, rotating the controller in the FocusArea causes RotaryService to move the focus from the view on the right to the view on the left (and vice versa). Adding this view to each window can fix the issue. When RotaryService determines the focus target is a FocusParkingView, it can determine a wrap-around is about to occur at which point it avoids the wrap-around by not moving focus.
  3. When the rotary control launches an app, Android focuses the first focusable view, which is always the FocusParkingView. The FocusParkingView determines the optimal view to focus on and then applies focus.

Focusable views

RotaryService builds upon the Android framework's existing concept of view focus, dating back to when phones had physical keyboards and D-pads. The existing android:nextFocusForward attribute is repurposed for rotary (see FocusArea customization), but android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp, and android:nextFocusDown are not.

RotaryService only focuses on views that are focusable. Some views, such as Buttons, are usually focusable. Others, such as TextViews and ViewGroups, usually aren't. Clickable views are automatically focusable and views are automatically clickable when they have a click listener. If this automatic logic results in the desired focusability, you needn't explicitly set the view's focusability. If automatic logic doesn't result in the desired focusability, set the android:focusable attribute to true or false, or programmatically set the view's focusability with View.setFocusable(boolean). For RotaryService to focus on it, a view MUST meet the following requirements:

  • Focusable
  • Enabled
  • Visible
  • Have non-zero values for width and height

If a view doesn't meet all these requirements, for example a focusable but disabled button, the user can't use the rotary control to focus on it. If you want focus on disabled views, consider using a custom state rather than android:state_enabled to control how the view appears without indicating that Android should consider it disabled. Your app can inform the user why the view is disabled when tapped. The next section explains how to do this.

Custom state

To add a custom state:

  1. To add a custom attribute to your view. For example, to add a state_rotary_enabled custom state to the CustomView view class, use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
  2. To track this state, add an instance variable to your view along with accessor methods:
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
  3. To read the value of your attribute when your view is created:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
  4. In your view class, override the onCreateDrawableState() method and then add the custom state, when appropriate. For example:
    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. Make your view's click handler perform differently depending on its state. For example, the click handler might do nothing or it might pop up a toast when mRotaryEnabled is false.
  6. To make the button appear disabled, in your view's background drawable, use app:state_rotary_enabled instead of android:state_enabled. If you don't already have it, you'll need to add:
  7. If your view is disabled in any layouts, replace android:enabled="false" with app:state_rotary_enabled="false" and then add the app namespace, as above.
  8. If your view is programmatically disabled, replace calls to setEnabled() with calls to setRotaryEnabled().


Use FocusAreas to partition the focusable views into blocks to make navigation easier and to be consistent with other apps. For example, if your app has a toolbar, the toolbar should be in a separate FocusArea from the rest of your app. Tab bars and other navigation elements should also be separated from the rest of the app. Large lists should generally have their own FocusArea. If not, users must rotate through the entire list to access some views.

FocusArea is a subclass of LinearLayout in the car-ui-library. When this feature is enabled, FocusArea draws a highlight when one of its descendants is focused. To learn more, see Focus highlight customization.

When creating a navigation block in the layout file, if you intend to use a LinearLayout as a container for that block, use FocusArea instead. Otherwise, wrap the block in a FocusArea.

Do NOT nest a FocusArea in another FocusArea. Doing so leads to undefined navigation behavior. Ensure that all focusable views are nested within a FocusArea.

An example of a FocusArea in RotaryPlayground is shown below:


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 might 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 might 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 isn't 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="">
   <item android:state_focused="true" android:state_pressed="true">
      <shape android:shape="oval">
         <solid android:color="@color/car_ui_rotary_focus_pressed_fill_color"/>
   <item android:state_focused="true">
      <shape android:shape="oval">
         <solid android:color="@color/car_ui_rotary_focus_fill_color"/>

(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)
  • (Android 12)

For example:

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

Rotary scrolling

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

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 avCarUiRecyclerView, 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 AccessibilityEvents 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 MotionEvents 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.


When using an ActivityView:

  • The ActivityView shouldn't 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) {
    } else {
    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 is 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.


In an app designed for touch, it's common to have nested focusable views. For example, there might 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 might 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 might 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 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.