Desarrollo de aplicaciones

El siguiente material es para desarrolladores de aplicaciones.

Para que su aplicación sea compatible con la rotación, DEBE:

  1. Coloque un FocusParkingView en el diseño de actividad respectivo.
  2. Asegúrese de que las vistas sean (o no) enfocables.
  3. Use FocusArea s para envolver todas sus vistas enfocables, excepto FocusParkingView .

Cada una de estas tareas se detalla a continuación, una vez que haya configurado su entorno para desarrollar aplicaciones habilitadas para rotación.

Configurar un controlador rotatorio

Antes de que pueda comenzar a desarrollar aplicaciones habilitadas para rotación, necesita un controlador rotatorio o un suplente. Tiene las opciones que se describen a continuación.

emulador

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

También puede usar aosp_car_x86_64-userdebug .

Para acceder al controlador rotatorio emulado:

  1. Toca los tres puntos en la parte inferior de la barra de herramientas:

    Acceder al controlador giratorio emulado
    Figura 1. Acceso al controlador giratorio emulado
  2. Seleccione Coche giratorio en la ventana de controles ampliados:

    Seleccionar coche giratorio
    Figura 2. Select Car rotativo

Teclado USB

  • Conecte un teclado USB a su dispositivo que ejecute Android Automotive OS (AAOS). En algunos casos, esto puede evitar que aparezca el teclado en pantalla.
  • Use una compilación de userdebug de usuario o eng .
  • Habilitar filtrado de eventos clave:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • Consulte la siguiente tabla para encontrar la tecla correspondiente a cada acción:
    Llave Acción rotatoria
    q Girar en sentido antihorario
    mi Rotar las agujas del reloj
    A Empujar a la izquierda
    D Empujar a la derecha
    W empujar hacia arriba
    S empujar hacia abajo
    Para coma Botón central
    R o ESC Botón de retroceso

Comandos BAD

Puede usar los comandos car_service para inyectar eventos de entrada rotativos. Estos comandos se pueden ejecutar en dispositivos que ejecutan Android Automotive OS (AAOS) o en un emulador.

comandos de servicio de coche Entrada rotativa
adb shell cmd car_service inject-rotary Girar en sentido antihorario
adb shell cmd car_service inject-rotary -c true Rotar las agujas del reloj
adb shell cmd car_service inject-rotary -dt 100 50 Gire en sentido contrario a las agujas del reloj varias veces (hace 100 ms y hace 50 ms)
adb shell cmd car_service inject-key 282 Empujar a la izquierda
adb shell cmd car_service inject-key 283 Empujar a la derecha
adb shell cmd car_service inject-key 280 empujar hacia arriba
adb shell cmd car_service inject-key 281 empujar hacia abajo
adb shell cmd car_service inject-key 23 Clic en el botón central
adb shell input keyevent inject-key 4 Haga clic en el botón Atrás

Controlador rotativo OEM

Cuando el hardware de su controlador giratorio está funcionando, esta es la opción más realista. Es particularmente útil para probar la rotación rápida.

EnfoqueEstacionamientoVista

FocusParkingView es una vista transparente en Car UI Library (car-ui-library) . RotaryService lo usa para admitir la navegación del controlador rotatorio. FocusParkingView debe ser la primera vista enfocable en el diseño. Debe colocarse fuera de todas las FocusArea . Cada ventana debe tener un FocusParkingView . Si ya está usando el diseño base car-ui-library, que contiene un FocusParkingView , no necesita agregar otro FocusParkingView . A continuación se muestra un ejemplo de FocusParkingView en 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>

Estas son las razones por las que necesita un FocusParkingView :

  1. Android no borra el enfoque automáticamente cuando el enfoque se establece en otra ventana. Si intenta borrar el enfoque en la ventana anterior, Android vuelve a enfocar una vista en esa ventana, lo que da como resultado que se enfocan dos ventanas simultáneamente. Agregar un FocusParkingView a cada ventana puede solucionar este problema. Esta vista es transparente y su resaltado de enfoque predeterminado está deshabilitado, por lo que es invisible para el usuario sin importar si está enfocado o no. Puede tomar el foco para que RotaryService pueda estacionar el foco en él para eliminar el resaltado del foco.
  2. Si solo hay un FocusArea en la ventana actual, girar el controlador en FocusArea hace que RotaryService mueva el foco de la vista de la derecha a la vista de la izquierda (y viceversa). Agregar esta vista a cada ventana puede solucionar el problema. Cuando RotaryService determina que el objetivo del foco es un FocusParkingView , puede determinar que está a punto de ocurrir una vuelta y en ese momento evita la vuelta al no mover el foco.
  3. Cuando el control giratorio inicia una aplicación, Android enfoca la primera vista enfocable, que siempre es FocusParkingView . FocusParkingView determina la vista óptima para enfocar y luego aplica el enfoque.

Vistas enfocables

RotaryService se basa en el concepto existente de enfoque de vista del marco de trabajo de Android, que se remonta a cuando los teléfonos tenían teclados físicos y D-pads. El atributo android:nextFocusForward existente se reutilizó para rotativo (consulte Personalización de FocusArea ), pero android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp y android:nextFocusDown no lo son.

RotaryService solo se enfoca en vistas que son enfocables. Algunas vistas, como Button s, suelen ser enfocables. Otros, como TextView y ViewGroup , por lo general no lo son. Las vistas en las que se puede hacer clic se pueden enfocar automáticamente y las vistas se pueden hacer clic automáticamente cuando tienen un detector de clics. Si esta lógica automática da como resultado la capacidad de enfoque deseada, no necesita establecer explícitamente la capacidad de enfoque de la vista. Si la lógica automática no da como resultado la capacidad de enfoque deseada, establezca el atributo android:focusable en true o false , o establezca mediante programación la capacidad de enfoque de la vista con View.setFocusable(boolean) . Para que RotaryService se centre en ella, una vista DEBE cumplir con los siguientes requisitos:

  • Enfocable
  • Activado
  • Visible
  • Tener valores distintos de cero para ancho y alto

Si una vista no cumple con todos estos requisitos, por ejemplo, un botón enfocable pero deshabilitado, el usuario no puede usar el control giratorio para enfocarlo. Si desea centrarse en las vistas deshabilitadas, considere usar un estado personalizado en lugar de android:state_enabled para controlar cómo aparece la vista sin indicar que Android debería considerarla deshabilitada. Su aplicación puede informar al usuario por qué la vista está deshabilitada cuando se toca. La siguiente sección explica cómo hacer esto.

Estado personalizado

Para agregar un estado personalizado:

  1. Para agregar un atributo personalizado a su vista. Por ejemplo, para agregar un estado personalizado state_rotary_enabled a la clase de vista CustomView , use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. Para rastrear este estado, agregue una variable de instancia a su vista junto con los métodos de acceso:
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. Para leer el valor de su atributo cuando se crea su vista:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. En su clase de vista, anule el método onCreateDrawableState() y luego agregue el estado personalizado, cuando corresponda. Por ejemplo:
    @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. Haga que el controlador de clics de su vista funcione de manera diferente según su estado. Por ejemplo, es posible que el controlador de clics no haga nada o que muestre un mensaje emergente cuando mRotaryEnabled sea false .
  6. Para que el botón aparezca deshabilitado, en el fondo dibujable de su vista, use app:state_rotary_enabled en lugar de android:state_enabled . Si aún no lo tiene, deberá agregar:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Si su vista está deshabilitada en cualquier diseño, reemplace android:enabled="false" con app:state_rotary_enabled="false" y luego agregue el espacio de nombres de la app , como se indicó anteriormente.
  8. Si su vista está deshabilitada mediante programación, reemplace las llamadas a setEnabled() con llamadas a setRotaryEnabled() .

Area de enfoque

Utilice FocusAreas para dividir las vistas enfocables en bloques para facilitar la navegación y mantener la coherencia con otras aplicaciones. Por ejemplo, si su aplicación tiene una barra de herramientas, la barra de herramientas debe estar en un FocusArea separado del resto de su aplicación. Las barras de pestañas y otros elementos de navegación también deben estar separados del resto de la aplicación. Las listas grandes generalmente deben tener su propia FocusArea . De lo contrario, los usuarios deben rotar por toda la lista para acceder a algunas vistas.

FocusArea es una subclase de LinearLayout en car-ui-library. Cuando esta característica está habilitada, un FocusArea resaltará cuando uno de sus descendientes esté enfocado. Para obtener más información, consulte Personalización de resaltado de enfoque .

Al crear un bloque de navegación en el archivo de diseño, si pretende usar un LinearLayout como contenedor para ese bloque, use un FocusArea en su lugar. De lo contrario, envuelva el bloque en un FocusArea .

NO anide un FocusArea en otro FocusArea . Si lo hace, dará lugar a un comportamiento de navegación indefinido. Asegúrese de que todas las vistas enfocables estén anidadas dentro de un FocusArea .

A continuación se muestra un ejemplo de un FocusArea en 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 funciona de la siguiente manera:

  1. Al manejar acciones de rotación y empuje, RotaryService busca instancias de FocusArea en la jerarquía de vistas.
  2. Al recibir un evento de rotación, RotaryService mueve el foco a otra Vista que puede enfocarse en la misma FocusArea .
  3. Al recibir un evento de empujón, RotaryService mueve el foco a otra vista que puede enfocarse en otra FocusArea (normalmente adyacente).

Si no incluye ninguna FocusAreas en su diseño, la vista raíz se trata como un área de enfoque implícita. El usuario no puede empujar para navegar en la aplicación. En cambio, rotarán a través de todas las vistas enfocables, lo que puede ser adecuado para los diálogos.

Personalización del área de enfoque

Se pueden usar dos atributos de vista estándar para personalizar la navegación giratoria:

  • android:nextFocusForward permite a los desarrolladores de aplicaciones especificar el orden de rotación en un área de enfoque. Este es el mismo atributo que se usa para controlar el orden de tabulación para la navegación con el teclado. NO use este atributo para crear un bucle. En su lugar, use app:wrapAround (ver más abajo) para crear un bucle.
  • android:focusedByDefault permite a los desarrolladores de aplicaciones especificar la vista de enfoque predeterminada en la ventana. NO use este atributo y app:defaultFocus (ver más abajo) en la misma FocusArea .

FocusArea también define algunos atributos para personalizar la navegación rotatoria. Las áreas de enfoque implícitas no se pueden personalizar con estos atributos.

  1. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    app:defaultFocus se puede usar para especificar el ID de una vista descendiente enfocable, en la que se debe enfocar cuando el usuario empuja a este FocusArea .
  2. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    app:defaultFocusOverridesHistory se puede establecer en true para que la vista especificada anteriormente tome el foco, incluso si con el historial para indicar que se ha enfocado otra vista en esta FocusArea .
  3. ( Android 12 )
    Usa app:nudgeLeftShortcut , app:nudgeRightShortcut , app:nudgeUpShortcut y app:nudgeDownShortcut para especificar el ID de una vista descendiente enfocable, en la que se debe enfocar cuando el usuario empuja en una dirección determinada. Para obtener más información, consulte el contenido de los atajos de empuje a continuación.

    ( Android 11 QPR3, Android 11 Car, en desuso en Android 12 ) app:nudgeShortcut y app:nudgeShortcutDirection solo un atajo de empuje.

  4. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para permitir que la rotación se ajuste en esta FocusArea , app:wrapAround se puede establecer en true . Esto se suele utilizar cuando las vistas se organizan en un círculo o un óvalo.
  5. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para ajustar el relleno del resaltado en esta FocusArea , use app:highlightPaddingStart , app:highlightPaddingEnd , app:highlightPaddingTop , app:highlightPaddingBottom , app:highlightPaddingHorizontal y app:highlightPaddingVertical .
  6. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para ajustar los límites percibidos de esta FocusArea para encontrar un objetivo de empuje, use app:startBoundOffset , app:endBoundOffset , app:topBoundOffset , app:bottomBoundOffset , app:horizontalBoundOffset y app:verticalBoundOffset .
  7. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para especificar explícitamente el ID de un FocusArea adyacente (o áreas) en las direcciones dadas, use app:nudgeLeft , app:nudgeRight , app:nudgeUp y app:nudgeDown . Utilícelo cuando la búsqueda geométrica utilizada de forma predeterminada no encuentre el objetivo deseado.

Empujar generalmente navega entre FocusAreas. Pero con los atajos de toque, a veces el toque primero navega dentro de un FocusArea , por lo que es posible que el usuario necesite empujar dos veces para navegar al siguiente FocusArea . Los atajos de desplazamiento son útiles cuando un área de FocusArea contiene una lista larga seguida de un botón de acción flotante , como en el ejemplo a continuación:

Atajo de empujón
Figura 3. Atajo de empuje

Sin el atajo de empuje, el usuario tendría que rotar por toda la lista para llegar al FAB.

Personalización de resaltado de enfoque

Como se señaló anteriormente, RotaryService se basa en el concepto existente de enfoque de vista del marco de trabajo de Android. Cuando el usuario gira y empuja, RotaryService mueve el foco, enfocando una vista y desenfocando otra. En Android, cuando se enfoca una vista, si la vista:

  • ha especificado su propio resaltado de enfoque, Android dibuja el resaltado de enfoque de la vista.
  • no especifica un resaltado de enfoque y el resaltado de enfoque predeterminado no está deshabilitado, Android dibuja el resaltado de enfoque predeterminado para la vista.

Las aplicaciones diseñadas para el tacto generalmente no especifican los resaltados de enfoque apropiados.

El marco de Android proporciona el resaltado de enfoque predeterminado y el OEM puede anularlo. Los desarrolladores de aplicaciones lo reciben cuando el tema que usan se deriva de Theme.DeviceDefault .

Para una experiencia de usuario consistente, confíe en el resaltado de enfoque predeterminado siempre que sea posible. Si necesita un resaltado de enfoque personalizado (por ejemplo, redondo o en forma de pastilla), o si está usando un tema que no se deriva de Theme.DeviceDefault , use los recursos de car-ui-library para especificar su propio resaltado de enfoque para cada vista.

Para especificar un resaltado de enfoque personalizado para una vista, cambie el elemento de diseño de fondo o de primer plano de la vista a un elemento de diseño que difiera cuando se enfoca la vista. Por lo general, cambiaría el fondo. El siguiente elemento de diseño, si se usa como fondo para una vista cuadrada, produce un resaltado de enfoque redondo:

<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 ) Las referencias de recursos en negrita en el ejemplo anterior identifican los recursos definidos por car-ui-library. El OEM los anula para que sean coherentes con el resaltado de enfoque predeterminado que especifican. Esto garantiza que el color de resaltado de enfoque, el ancho del trazo, etc., no cambien cuando el usuario navega entre una vista con un resaltado de enfoque personalizado y una vista con el resaltado de enfoque predeterminado. El último elemento es una onda utilizada para el tacto. Los valores predeterminados utilizados para los recursos en negrita aparecen de la siguiente manera:

Valores predeterminados para recursos en negrita
Figura 4. Valores predeterminados para recursos en negrita

Además, se requiere un resaltado de enfoque personalizado cuando a un botón se le da un color de fondo sólido para llamar la atención del usuario, como en el ejemplo a continuación. Esto puede hacer que el punto culminante del enfoque sea difícil de ver. En esta situación, especifique un resaltado de enfoque personalizado usando colores secundarios :

Color de fondo sólido
  • ( Android 11 QPR3, Android 11 Coche, 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

Por ejemplo:

Enfocado, no presionadoEnfocado, presionado
Enfocado, no presionado Enfocado, presionado

Desplazamiento rotatorio

Si su aplicación usa RecyclerView s, DEBERÍA usar CarUiRecyclerView s en su lugar. Esto garantiza que su interfaz de usuario sea coherente con las demás porque la personalización de un OEM se aplica a todos los CarUiRecyclerView s.

Si todos los elementos de su lista son enfocables, no necesita hacer nada más. La navegación giratoria mueve el enfoque a través de los elementos de la lista y la lista se desplaza para hacer visible el elemento recién enfocado.

( Android 11 QPR3, Android 11 Coche, Android 12 )
Si hay una combinación de elementos enfocables y no enfocables, o si todos los elementos no son enfocables, puede habilitar el desplazamiento giratorio, que permite al usuario usar el controlador giratorio para desplazarse gradualmente por la lista sin omitir los elementos no enfocables. Para habilitar el desplazamiento giratorio, establezca el atributo app:rotaryScrollEnabled en true .

( Android 11 QPR3, Android 11 Coche, Android 12 )
Puede habilitar el desplazamiento giratorio en cualquier vista desplazable, incluido av CarUiRecyclerView , con el método setRotaryScrollEnabled() en CarUiUtils . Si lo hace, necesita:

  • Haga que la vista desplazable sea enfocable para que se pueda enfocar cuando ninguna de sus vistas descendientes enfocables esté visible,
  • Deshabilite el resaltado de enfoque predeterminado en la vista desplazable llamando a setDefaultFocusHighlightEnabled(false) para que la vista desplazable no parezca estar enfocada,
  • Asegúrese de que la vista desplazable esté enfocada antes que sus descendientes llamando a setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Escuche MotionEvents con SOURCE_ROTARY_ENCODER y AXIS_VSCROLL o AXIS_HSCROLL para indicar la distancia de desplazamiento y la dirección (a través del signo).

Cuando el desplazamiento giratorio está habilitado en un CarUiRecyclerView y el usuario gira a un área donde no hay vistas enfocables, la barra de desplazamiento cambia de gris a azul, como para indicar que la barra de desplazamiento está enfocada. Puede implementar un efecto similar si lo desea.

Los MotionEvents son los mismos que los generados por una rueda de desplazamiento en un mouse, excepto por la fuente.

Modo de manipulación directa

Normalmente, los empujones y la rotación navegan a través de la interfaz de usuario, mientras que las pulsaciones del botón central actúan, aunque no siempre es así. Por ejemplo, si un usuario desea ajustar el volumen de la alarma, puede usar el controlador giratorio para navegar hasta el control deslizante de volumen, presionar el botón central, girar el controlador para ajustar el volumen de la alarma y luego presionar el botón Atrás para volver a la navegación. . Esto se conoce como modo de manipulación directa (DM) . En este modo, el controlador giratorio se usa para interactuar con la vista directamente en lugar de navegar.

Implemente DM de una de dos maneras. Si solo necesita manejar la rotación y la vista que desea manipular responde a ACTION_SCROLL_FORWARD y ACTION_SCROLL_BACKWARD AccessibilityEvent s de manera adecuada, use el mecanismo simple . De lo contrario, utilice el mecanismo avanzado .

El mecanismo simple es la única opción en las ventanas del sistema; las aplicaciones pueden usar cualquier mecanismo.

Mecanismo sencillo

( Android 11 QPR3, Android 11 Coche, Android 12 )
Tu aplicación debe llamar a DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable) . RotaryService reconoce cuando el usuario está en modo DM e ingresa al modo DM cuando el usuario presiona el botón central mientras una vista está enfocada. Cuando está en modo DM, las rotaciones realizan ACTION_SCROLL_FORWARD o ACTION_SCROLL_BACKWARD y sale del modo DM cuando el usuario presiona el botón Atrás. El mecanismo simple alterna el estado seleccionado de la vista al entrar y salir del modo DM.

Para proporcionar una señal visual de que el usuario está en modo DM, haga que su vista parezca diferente cuando se seleccione. Por ejemplo, cambia el fondo cuando android:state_selected es true .

Mecanismo avanzado

La aplicación determina cuándo RotaryService ingresa y sale del modo DM. Para una experiencia de usuario consistente, al presionar el botón central con una vista de DM enfocada, debe ingresar al modo DM y el botón Atrás debe salir del modo DM. Si no se utilizan el botón central y/o el empujón, pueden ser formas alternativas de salir del modo DM. Para aplicaciones como Maps, se puede usar un botón para representar DM para ingresar al modo DM.

Para admitir el modo DM avanzado, una vista:

  1. ( Android 11 QPR3, Android 11 Car, Android 12 ) DEBE escuchar un evento KEYCODE_DPAD_CENTER para ingresar al modo DM y escuchar un evento KEYCODE_BACK para salir del modo DM, llamando a DirectManipulationHelper.enableDirectManipulationMode() en cada caso. Para escuchar estos eventos, realice una de las siguientes acciones:
    • Registre un OnKeyListener .
    • o,
    • Extienda la vista y luego anule su método dispatchKeyEvent() .
  2. DEBERÍA escuchar eventos de empujón ( KEYCODE_DPAD_UP , KEYCODE_DPAD_DOWN , KEYCODE_DPAD_LEFT o KEYCODE_DPAD_RIGHT ) si la vista debe manejar empujones.
  3. DEBERÍA escuchar MotionEvent s y obtener el recuento de rotación en AXIS_SCROLL si la vista quiere manejar la rotación. Hay varias maneras de hacer esto:
    1. Registre un OnGenericMotionListener .
    2. Extienda la vista y anule su método dispatchTouchEvent() .
  4. Para evitar quedarse atascado en el modo DM, DEBE salir del modo DM cuando el Fragmento o la Actividad a la que pertenece la vista no es interactiva.
  5. DEBERÍA proporcionar una señal visual para indicar que la vista está en modo DM.

A continuación se proporciona un ejemplo de una vista personalizada que utiliza el modo DM para desplazar y hacer zoom en un mapa:

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

Se pueden encontrar más ejemplos en el proyecto RotaryPlayground .

Vista de actividad

Al usar un ActivityView:

  • El ActivityView no debe ser enfocable.
  • ( Android 11 QPR3, Android 11 Car, obsoleto en Android 11 )
    El contenido de ActivityView DEBE contener FocusParkingView como la primera vista enfocable, y su atributo app:shouldRestoreFocus DEBE ser false .
  • El contenido de ActivityView no debe tener vistas android:focusByDefault .

Para el usuario, las vistas de actividad no deberían tener ningún efecto en la navegación, excepto que las áreas de enfoque no pueden abarcar vistas de actividad. En otras palabras, no puede tener un área de enfoque única que tenga contenido dentro y fuera de una vista de ActivityView . Si no agrega ninguna FocusAreas a su ActivityView , la raíz de la jerarquía de vistas en ActivityView se considera un área de enfoque implícita.

Botones que funcionan cuando se mantienen presionados

La mayoría de los botones provocan alguna acción cuando se hace clic en ellos. Algunos botones funcionan cuando se mantienen presionados. Por ejemplo, los botones de avance rápido y rebobinado normalmente funcionan cuando se mantienen presionados. Para que dichos botones sean compatibles con la rotación, escuche KEYCODE_DPAD_CENTER KeyEvents de la siguiente manera:

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

En el que mRunnable realiza una acción (como rebobinar) y se programa para ejecutarse después de un retraso.

Modo táctil

Los usuarios pueden usar un controlador giratorio para interactuar con la unidad principal en un automóvil de dos maneras, ya sea usando el controlador giratorio o tocando la pantalla. Al usar el controlador giratorio, se resaltará una de las vistas enfocables. Al tocar la pantalla, no aparece ningún resaltado de enfoque. El usuario puede cambiar entre estos modos de entrada en cualquier momento:

  • Giratorio → táctil. Cuando el usuario toca la pantalla, el resaltado de enfoque desaparece.
  • Toque → giratorio. Cuando el usuario empuja, gira o presiona el botón central, aparece el resaltado de enfoque.

Los botones Atrás y Inicio no tienen efecto en el modo de entrada.

Rotary aprovecha el concepto existente de modo táctil de Android. Puede usar View.isInTouchMode() para determinar qué modo de entrada está usando el usuario. Puede usar OnTouchModeChangeListener para escuchar los cambios. Si bien esto se puede usar para personalizar su interfaz de usuario para el modo de entrada actual, evite cambios importantes, ya que pueden ser desconcertantes.

Solución de problemas

En una aplicación diseñada para tocar, no es raro tener vistas enfocables anidadas. Por ejemplo, puede haber un FrameLayout alrededor de un ImageButton , ambos enfocables. Esto no hace daño al tacto, pero puede resultar en una experiencia de usuario deficiente para el rotativo porque el usuario debe rotar el controlador dos veces para pasar a la siguiente vista interactiva. Para una buena experiencia de usuario, Google recomienda que se pueda enfocar la vista exterior o la interior, pero no ambas.

Si un botón o interruptor pierde el foco cuando se presiona a través del controlador giratorio, se puede aplicar una de estas condiciones:

  • El botón o interruptor se está desactivando (breve o indefinidamente) debido a que se presionó el botón. En cualquier caso, hay dos formas de abordar esto:
    • Deje el estado android:enabled como true y use un estado personalizado para atenuar el botón o cambiar como se describe en Estado personalizado .
    • Utilice un contenedor para rodear el botón o el interruptor y hacer que el contenedor se pueda enfocar en lugar del botón o el interruptor. (El detector de clics debe estar en el contenedor).
  • El botón o interruptor está siendo reemplazado. Por ejemplo, la acción realizada cuando se presiona el botón o se alterna el interruptor puede desencadenar una actualización de las acciones disponibles, lo que hace que los nuevos botones reemplacen a los existentes. Hay dos formas de abordar esto:
    • En lugar de crear un nuevo botón o interruptor, configure el icono y/o el texto del botón o interruptor existente.
    • Como arriba, agregue un contenedor enfocable alrededor del botón o interruptor.

Patio de recreo giratorio

RotaryPlayground es una aplicación de referencia para rotativo. Úselo para aprender a integrar funciones rotativas en sus aplicaciones. RotaryPlayground se incluye en compilaciones de emuladores y en compilaciones para dispositivos que ejecutan Android Automotive OS (AAOS).

  • Repositorio RotaryPlayground : packages/apps/Car/tests/RotaryPlayground/
  • Versiones: Android 11 QPR3, Android 11 Car y Android 12

La aplicación RotaryPlayground muestra las siguientes pestañas a la izquierda:

  • Tarjetas. Pruebe la navegación por las áreas de enfoque, omitiendo los elementos no enfocables y la entrada de texto.
  • Manipulación directa. Pruebe widgets que admitan el modo de manipulación directa simple y avanzada. Esta pestaña es específicamente para la manipulación directa dentro de la ventana de la aplicación.
  • Manipulación de la interfaz de usuario del sistema. Pruebe los widgets que admiten la manipulación directa en las ventanas del sistema donde solo se admite el modo de manipulación directa simple.
  • Cuadrícula. Pruebe la navegación rotatoria de patrón z con desplazamiento.
  • Notificación. Pruebe la entrada y salida de notificaciones emergentes.
  • Desplazarse. Pruebe el desplazamiento a través de una combinación de contenido enfocable y no enfocable.
  • WebView. Pruebe la navegación a través de enlaces en un WebView .
  • FocusArea personalizada. Pruebe la personalización de FocusArea :
    • Envolver alrededor.
    • android:focusedByDefault y app:defaultFocus
    • .
    • Empuje de objetivos explícitos.
    • Empujar atajos.
    • FocusArea sin vistas enfocables.
,

El siguiente material es para desarrolladores de aplicaciones.

Para que su aplicación sea compatible con la rotación, DEBE:

  1. Coloque un FocusParkingView en el diseño de actividad respectivo.
  2. Asegúrese de que las vistas sean (o no) enfocables.
  3. Use FocusArea s para envolver todas sus vistas enfocables, excepto FocusParkingView .

Cada una de estas tareas se detalla a continuación, una vez que haya configurado su entorno para desarrollar aplicaciones habilitadas para rotación.

Configurar un controlador rotatorio

Antes de que pueda comenzar a desarrollar aplicaciones habilitadas para rotación, necesita un controlador rotatorio o un suplente. Tiene las opciones que se describen a continuación.

emulador

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

También puede usar aosp_car_x86_64-userdebug .

Para acceder al controlador rotatorio emulado:

  1. Toca los tres puntos en la parte inferior de la barra de herramientas:

    Acceder al controlador giratorio emulado
    Figura 1. Acceso al controlador giratorio emulado
  2. Seleccione Coche giratorio en la ventana de controles ampliados:

    Seleccionar coche giratorio
    Figura 2. Select Car rotativo

Teclado USB

  • Conecte un teclado USB a su dispositivo que ejecute Android Automotive OS (AAOS). En algunos casos, esto puede evitar que aparezca el teclado en pantalla.
  • Use una compilación de userdebug de usuario o eng .
  • Habilitar filtrado de eventos clave:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • Consulte la siguiente tabla para encontrar la tecla correspondiente a cada acción:
    Llave Acción rotatoria
    q Girar en sentido antihorario
    mi Rotar las agujas del reloj
    A Empujar a la izquierda
    D Empujar a la derecha
    W empujar hacia arriba
    S empujar hacia abajo
    Para coma Botón central
    R o ESC Botón de retroceso

Comandos BAD

Puede usar los comandos car_service para inyectar eventos de entrada rotativos. Estos comandos se pueden ejecutar en dispositivos que ejecutan Android Automotive OS (AAOS) o en un emulador.

comandos de servicio de coche Entrada rotativa
adb shell cmd car_service inject-rotary Girar en sentido antihorario
adb shell cmd car_service inject-rotary -c true Rotar las agujas del reloj
adb shell cmd car_service inject-rotary -dt 100 50 Gire en sentido contrario a las agujas del reloj varias veces (hace 100 ms y hace 50 ms)
adb shell cmd car_service inject-key 282 Empujar a la izquierda
adb shell cmd car_service inject-key 283 Empujar a la derecha
adb shell cmd car_service inject-key 280 empujar hacia arriba
adb shell cmd car_service inject-key 281 empujar hacia abajo
adb shell cmd car_service inject-key 23 Clic en el botón central
adb shell input keyevent inject-key 4 Haga clic en el botón Atrás

Controlador rotativo OEM

Cuando el hardware de su controlador giratorio está funcionando, esta es la opción más realista. Es particularmente útil para probar la rotación rápida.

EnfoqueEstacionamientoVista

FocusParkingView es una vista transparente en Car UI Library (car-ui-library) . RotaryService lo usa para admitir la navegación del controlador rotatorio. FocusParkingView debe ser la primera vista enfocable en el diseño. Debe colocarse fuera de todas las FocusArea . Cada ventana debe tener un FocusParkingView . Si ya está usando el diseño base car-ui-library, que contiene un FocusParkingView , no necesita agregar otro FocusParkingView . A continuación se muestra un ejemplo de FocusParkingView en 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>

Estas son las razones por las que necesita un FocusParkingView :

  1. Android no borra el enfoque automáticamente cuando el enfoque se establece en otra ventana. Si intenta borrar el enfoque en la ventana anterior, Android vuelve a enfocar una vista en esa ventana, lo que da como resultado que se enfocan dos ventanas simultáneamente. Agregar un FocusParkingView a cada ventana puede solucionar este problema. Esta vista es transparente y su resaltado de enfoque predeterminado está deshabilitado, por lo que es invisible para el usuario sin importar si está enfocado o no. Puede tomar el foco para que RotaryService pueda estacionar el foco en él para eliminar el resaltado del foco.
  2. Si solo hay un FocusArea en la ventana actual, girar el controlador en FocusArea hace que RotaryService mueva el foco de la vista de la derecha a la vista de la izquierda (y viceversa). Agregar esta vista a cada ventana puede solucionar el problema. Cuando RotaryService determina que el objetivo del foco es un FocusParkingView , puede determinar que está a punto de ocurrir una vuelta y en ese momento evita la vuelta al no mover el foco.
  3. Cuando el control giratorio inicia una aplicación, Android enfoca la primera vista enfocable, que siempre es FocusParkingView . FocusParkingView determina la vista óptima para enfocar y luego aplica el enfoque.

Vistas enfocables

RotaryService se basa en el concepto existente de enfoque de vista del marco de trabajo de Android, que se remonta a cuando los teléfonos tenían teclados físicos y D-pads. El atributo android:nextFocusForward existente se reutilizó para rotativo (consulte Personalización de FocusArea ), pero android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp y android:nextFocusDown no lo son.

RotaryService solo se enfoca en vistas que son enfocables. Algunas vistas, como Button s, suelen ser enfocables. Otros, como TextView y ViewGroup , por lo general no lo son. Las vistas en las que se puede hacer clic se pueden enfocar automáticamente y las vistas se pueden hacer clic automáticamente cuando tienen un detector de clics. Si esta lógica automática da como resultado la capacidad de enfoque deseada, no necesita establecer explícitamente la capacidad de enfoque de la vista. Si la lógica automática no da como resultado la capacidad de enfoque deseada, establezca el atributo android:focusable en true o false , o establezca mediante programación la capacidad de enfoque de la vista con View.setFocusable(boolean) . Para que RotaryService se centre en ella, una vista DEBE cumplir con los siguientes requisitos:

  • Enfocable
  • Activado
  • Visible
  • Tener valores distintos de cero para ancho y alto

Si una vista no cumple con todos estos requisitos, por ejemplo, un botón enfocable pero deshabilitado, el usuario no puede usar el control giratorio para enfocarlo. Si desea centrarse en las vistas deshabilitadas, considere usar un estado personalizado en lugar de android:state_enabled para controlar cómo aparece la vista sin indicar que Android debería considerarla deshabilitada. Su aplicación puede informar al usuario por qué la vista está deshabilitada cuando se toca. La siguiente sección explica cómo hacer esto.

Estado personalizado

Para agregar un estado personalizado:

  1. Para agregar un atributo personalizado a su vista. Por ejemplo, para agregar un estado personalizado state_rotary_enabled a la clase de vista CustomView , use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. Para rastrear este estado, agregue una variable de instancia a su vista junto con los métodos de acceso:
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. Para leer el valor de su atributo cuando se crea su vista:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. En su clase de vista, anule el método onCreateDrawableState() y luego agregue el estado personalizado, cuando corresponda. Por ejemplo:
    @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. Haga que el controlador de clics de su vista funcione de manera diferente según su estado. Por ejemplo, es posible que el controlador de clics no haga nada o que muestre un mensaje emergente cuando mRotaryEnabled sea false .
  6. Para que el botón aparezca deshabilitado, en el fondo dibujable de su vista, use app:state_rotary_enabled en lugar de android:state_enabled . Si aún no lo tiene, deberá agregar:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Si su vista está deshabilitada en cualquier diseño, reemplace android:enabled="false" con app:state_rotary_enabled="false" y luego agregue el espacio de nombres de la app , como se indicó anteriormente.
  8. Si su vista está deshabilitada mediante programación, reemplace las llamadas a setEnabled() con llamadas a setRotaryEnabled() .

Area de enfoque

Utilice FocusAreas para dividir las vistas enfocables en bloques para facilitar la navegación y mantener la coherencia con otras aplicaciones. Por ejemplo, si su aplicación tiene una barra de herramientas, la barra de herramientas debe estar en un FocusArea separado del resto de su aplicación. Las barras de pestañas y otros elementos de navegación también deben estar separados del resto de la aplicación. Las listas grandes generalmente deben tener su propia FocusArea . De lo contrario, los usuarios deben rotar por toda la lista para acceder a algunas vistas.

FocusArea es una subclase de LinearLayout en car-ui-library. Cuando esta característica está habilitada, un FocusArea resaltará cuando uno de sus descendientes esté enfocado. Para obtener más información, consulte Personalización de resaltado de enfoque .

Al crear un bloque de navegación en el archivo de diseño, si pretende usar un LinearLayout como contenedor para ese bloque, use un FocusArea en su lugar. De lo contrario, envuelva el bloque en un FocusArea .

NO anide un FocusArea en otro FocusArea . Si lo hace, dará lugar a un comportamiento de navegación indefinido. Asegúrese de que todas las vistas enfocables estén anidadas dentro de un FocusArea .

A continuación se muestra un ejemplo de un FocusArea en 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 funciona de la siguiente manera:

  1. Al manejar acciones de rotación y empuje, RotaryService busca instancias de FocusArea en la jerarquía de vistas.
  2. Al recibir un evento de rotación, RotaryService mueve el foco a otra Vista que puede enfocarse en la misma FocusArea .
  3. Al recibir un evento de empujón, RotaryService mueve el foco a otra vista que puede enfocarse en otra FocusArea (normalmente adyacente).

Si no incluye ninguna FocusAreas en su diseño, la vista raíz se trata como un área de enfoque implícita. El usuario no puede empujar para navegar en la aplicación. 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 Coche, 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 Coche, 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 Coche, 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 Coche, 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 Coche, 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 Coche, 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 Coche, 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

Por ejemplo:

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 Coche, 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 Coche, 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 Coche, 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.

Solución de problemas

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.