desarrollar aplicaciones

El siguiente material está dirigido a desarrolladores de aplicaciones.

Para que tu aplicación sea compatible con rotativa, DEBES:

  1. Coloque un FocusParkingView en el diseño de actividad respectivo.
  2. Asegúrese de que las vistas sean (o no) enfocables.
  3. Utilice FocusArea s para abarcar todas las 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 dispositivos rotativos.

Configurar un controlador giratorio

Antes de poder comenzar a desarrollar aplicaciones habilitadas para dispositivos rotativos, necesita un controlador rotativo o un sustituto. Tienes 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 puedes usar aosp_car_x86_64-userdebug .

Para acceder al controlador giratorio emulado:

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

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

    Seleccione Coche rotativo
    Figura 2. Select Car rotativo

teclado USB

  • Conecte un teclado USB a su dispositivo que ejecuta el sistema operativo Android Automotive (AAOS). En algunos casos, esto evita que aparezca el teclado en pantalla.
  • Utilice una compilación userdebug 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 clave correspondiente a cada acción:
    Llave Acción rotatoria
    q Girar en sentido antihorario
    mi Rotar las agujas del reloj
    A Empujar hacia la izquierda
    D Empujar hacia la derecha
    W. empujar hacia arriba
    S empujar hacia abajo
    F o coma Botón central
    R o Esc Botón de retroceso

Comandos BAD

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

comandos car_service 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 Girar en sentido antihorario varias veces (hace 100 ms y hace 50 ms)
adb shell cmd car_service inject-key 282 Empujar hacia la izquierda
adb shell cmd car_service inject-key 283 Empujar hacia 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é en funcionamiento, esta es la opción más realista. Es particularmente útil para probar la rotación rápida.

EnfoqueEstacionamientoVer

FocusParkingView es una vista transparente en la biblioteca de UI del automóvil (car-ui-library) . RotaryService lo utiliza para admitir la navegación del controlador giratorio. 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ás usando el diseño base car-ui-library, que contiene un FocusParkingView , no necesitas 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 foco automáticamente cuando el foco se establece en otra ventana. Si intenta borrar el foco en la ventana anterior, Android vuelve a enfocar una vista en esa ventana, lo que da como resultado que se enfoquen 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 de enfoque es un FocusParkingView , puede determinar que está a punto de ocurrir una envoltura y en ese momento evita la envoltura al no mover el enfoque.
  3. Cuando el control giratorio inicia una aplicación, Android enfoca la primera vista enfocable, que siempre es FocusParkingView . FocusParkingView determina la vista óptima en la que enfocarse y luego aplica el enfoque.

Vistas enfocables

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

RotaryService solo se centra en vistas que son enfocables. Algunas vistas, como las de Button , suelen ser enfocables. Otros, como TextView y ViewGroup , normalmente no lo son. Las vistas en las que se puede hacer clic se pueden enfocar automáticamente y se puede hacer clic en las vistas automáticamente cuando tienen un detector de clics. Si esta lógica automática da como resultado la capacidad de enfoque deseada, no es necesario 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 configure mediante programación la capacidad de enfoque de la vista con View.setFocusable(boolean) . Para que RotaryService se centre en ello, una vista DEBE cumplir 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 la 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 realizar un seguimiento de 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 es false .
  6. Para que el botón aparezca deshabilitado, en el fondo dibujable de tu vista, usa 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 algún diseño, reemplace android:enabled="false" con app:state_rotary_enabled="false" y luego agregue el espacio de nombres 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 ser coherente con otras aplicaciones. Por ejemplo, si su aplicación tiene una barra de herramientas, ésta debe estar en un FocusArea separada 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 deberían 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 la biblioteca car-ui. Cuando esta función está habilitada, FocusArea resalta cuando uno de sus descendientes está enfocado. Para obtener más información, consulte Personalización de aspectos destacados de Focus .

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

NO anide un FocusArea en otro FocusArea . Hacerlo conduce 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 desplazamiento, 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 por todas las vistas enfocables, lo que podría ser adecuado para los diálogos.

Personalización del área de enfoque

Se pueden utilizar 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 utiliza para controlar el orden de tabulación para la navegación con el teclado. NO utilice este atributo para crear un bucle. En su lugar, utilice 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 utilice este atributo y app:defaultFocus (ver más abajo) en la misma FocusArea .

FocusArea también define algunos atributos para personalizar la navegación giratoria. 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 se acerca a esta FocusArea .
  2. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    app:defaultFocusOverridesHistory se puede establecer en true para que la vista especificada anteriormente se enfoque incluso si tiene un historial para indicar que se ha enfocado otra vista en esta FocusArea .
  3. ( Androide 12 )
    Utilice 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 desplazamiento a continuación.

    ( Android 11 QPR3, Android 11 Car, obsoleto en Android 12 ) app:nudgeShortcut y app:nudgeShortcutDirection solo admitían un atajo de desplazamiento.

  4. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para permitir que la rotación se ajuste a esta FocusArea , app:wrapAround se puede establecer en true . Esto se utiliza con mayor frecuencia cuando las vistas están dispuestas en un círculo u ó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 (o áreas) adyacentes en las instrucciones dadas, use app:nudgeLeft , app:nudgeRight , app:nudgeUp y app:nudgeDown . Utilícelo cuando la búsqueda geométrica utilizada por defecto no encuentre el objetivo deseado.

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

Empujar atajo
Figura 3. Atajo de desplazamiento

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 de enfoque de vista existente en el marco de Android. Cuando el usuario gira y empuja, RotaryService mueve el enfoque, enfocando una vista y desenfocando otra. En Android, cuando una vista está enfocada, 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 aspectos destacados de enfoque adecuados.

El resaltado de enfoque predeterminado lo proporciona el marco de trabajo de Android y el OEM puede anularlo. Los desarrolladores de aplicaciones lo reciben cuando el tema que utilizan 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 con una forma personalizada (por ejemplo, redonda o en forma de píldora), o si está usando un tema no derivado de Theme.DeviceDefault , use los recursos 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. Normalmente, cambiarías el fondo. El siguiente dibujable, 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 que se utiliza para tocar. 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 asigna un color de fondo sólido para llamar la atención del usuario, como en el siguiente ejemplo. Esto puede hacer que el foco sea difícil de ver. En esta situación, especifique un resaltado de enfoque personalizado utilizando 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
  • ( Androide 12 )
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

Por ejemplo:

Centrado, no presionadoCentrado, presionado
Centrado, no presionado Centrado, presionado

Desplazamiento giratorio

Si su aplicación usa RecyclerView s, DEBE 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 .

Si todos los elementos de su lista son enfocables, no necesita hacer nada más. La navegación giratoria mueve el foco 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, lo que permite al usuario usar el controlador giratorio para desplazarse gradualmente por la lista sin omitir 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, incluida av CarUiRecyclerView , con el método setRotaryScrollEnabled() en CarUiUtils . Si lo hace, necesita:

  • Haga que la vista desplazable sea enfocable para que pueda enfocarse cuando ninguna de sus vistas descendientes enfocables esté visible.
  • Deshabilite el resaltado de enfoque predeterminado en la vista desplazable llamando 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 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Escuche MotionEvents con SOURCE_ROTARY_ENCODER y AXIS_VSCROLL o AXIS_HSCROLL para indicar la distancia a desplazar y la dirección (a través del letrero).

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. Puedes implementar un efecto similar si lo deseas.

Los MotionEvents son los mismos que los generados por la rueda de desplazamiento del 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 toman acción, aunque este no es siempre el caso. 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 regresar a la navegación. . Esto se conoce como modo de manipulación directa (DM) . En este modo, el controlador giratorio se utiliza para interactuar directamente con la vista 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 apropiadamente a ACTION_SCROLL_FORWARD y ACTION_SCROLL_BACKWARD AccessibilityEvent , 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 utilizar cualquiera de los dos mecanismos.

Mecanismo sencillo

( Android 11 QPR3, Android 11 Coche, Android 12 )
Su aplicación debe llamar 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 la seleccione. Por ejemplo, cambie el fondo cuando android:state_selected sea true .

Mecanismo avanzado

La aplicación determina cuándo RotaryService entra y sale del modo DM. Para una experiencia de usuario consistente, al presionar el botón Central con una vista DM enfocada se 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 empujar, 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,
    • Amplíe la vista y luego anule su método dispatchKeyEvent() .
  2. DEBE 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. DEBE escuchar MotionEvent sy obtener el recuento de rotación en AXIS_SCROLL si la vista quiere manejar la rotación. Hay varias formas de hacer esto:
    1. Registre un OnGenericMotionListener .
    2. Amplíe 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 Actividad a la que pertenece la vista no es interactivo.
  5. DEBE 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 desplazarse 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

Cuando se utiliza un ActivityView:

  • ActivityView no debería poder enfocarse.
  • ( Android 11 QPR3, Android 11 Car, obsoleto en Android 11 )
    El contenido de ActivityView DEBE contener un 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, ActivityViews no debería tener ningún efecto en la navegación, excepto que las áreas de enfoque no pueden abarcar ActivityViews. En otras palabras, no puede tener una única área de enfoque que tenga contenido dentro y fuera 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 giratorios, 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 de un automóvil de dos maneras, ya sea usando el controlador giratorio o tocando la pantalla. Cuando se utiliza el controlador giratorio, se resalta 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 → toque. Cuando el usuario toca la pantalla, el foco resaltado desaparece.
  • Toque → giratorio. Cuando el usuario empuja, gira o presiona el botón central, aparece el resaltado de enfoque.

Los botones Atrás e Inicio no tienen ningún 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 utilizar OnTouchModeChangeListener para escuchar los cambios. Si bien esto se puede utilizar para personalizar su interfaz de usuario para el modo de entrada actual, evite realizar cambios importantes, ya que pueden resultar desconcertantes.

Solución de problemas

En una aplicación diseñada para el tacto, es común tener vistas enfocables anidadas. Por ejemplo, puede haber un FrameLayout alrededor de un ImageButton , los cuales se pueden enfocar. Esto no daña el tacto, pero puede resultar en una mala experiencia de usuario para el modo giratorio porque el usuario debe girar el controlador dos veces para pasar a la siguiente vista interactiva. Para una buena experiencia de usuario, Google recomienda hacer enfocable la vista exterior o la vista interior, pero no ambas.

Si un botón o interruptor pierde el foco cuando se presiona a través del controlador giratorio, podría aplicarse 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 interruptor y haga que el contenedor sea enfocable en lugar del botón o interruptor. (El detector de clics debe estar en el contenedor).
  • Se está reemplazando el botón o interruptor. Por ejemplo, la acción realizada cuando se presiona el botón o se activa el interruptor puede desencadenar una actualización de las acciones disponibles, lo que hace que nuevos botones reemplacen a los botones existentes. Hay dos maneras de abordar esto:
    • En lugar de crear un nuevo botón o interruptor, configure el ícono y/o el texto del botón o interruptor existente.
    • Como arriba, agregue un contenedor enfocable alrededor del botón o interruptor.

Patio de juegos giratorio

RotaryPlayground es una aplicación de referencia para Rotary. Úselo para aprender cómo integrar funciones giratorias en sus aplicaciones. RotaryPlayground se incluye en compilaciones de emuladores y en compilaciones para dispositivos que ejecutan el sistema operativo Android Automotive (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 navegar por las áreas de enfoque, omitiendo elementos no enfocables y entrada de texto.
  • Manipulación directa. Pruebe widgets que admitan el modo de manipulación directa simple y avanzado. Esta pestaña es específicamente para 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 ventanas del sistema donde solo se admite el modo de manipulación directa simple.
  • Red. Pruebe la navegación giratoria con patrón z con desplazamiento.
  • Notificación. Pruebe cómo entrar y salir de las notificaciones de aviso.
  • Desplazarse. Pruebe desplazarse por una combinación de contenido enfocable y no enfocable.
  • Vista web. Pruebe navegar a través de enlaces en un WebView .
  • FocusArea personalizada. Pruebe la personalización FocusArea :
    • Envolver alrededor.
    • android:focusedByDefault y app:defaultFocus
    • .
    • Objetivos de empujón explícitos.
    • Empujar atajos.
    • FocusArea sin vistas enfocables.
,

El siguiente material está dirigido a desarrolladores de aplicaciones.

Para que tu aplicación sea compatible con rotativa, DEBES:

  1. Coloque un FocusParkingView en el diseño de actividad respectivo.
  2. Asegúrese de que las vistas sean (o no) enfocables.
  3. Utilice FocusArea s para abarcar todas las 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 dispositivos rotativos.

Configurar un controlador giratorio

Antes de poder comenzar a desarrollar aplicaciones habilitadas para dispositivos rotativos, necesita un controlador rotativo o un sustituto. Tienes 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 puedes usar aosp_car_x86_64-userdebug .

Para acceder al controlador giratorio emulado:

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

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

    Seleccione Coche rotativo
    Figura 2. Select Car rotativo

teclado USB

  • Conecte un teclado USB a su dispositivo que ejecuta el sistema operativo Android Automotive (AAOS). En algunos casos, esto evita que aparezca el teclado en pantalla.
  • Utilice una compilación userdebug 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 clave correspondiente a cada acción:
    Llave Acción rotatoria
    q Girar en sentido antihorario
    mi Rotar las agujas del reloj
    A Empujar hacia la izquierda
    D Empujar hacia la derecha
    W. empujar hacia arriba
    S empujar hacia abajo
    F o coma Botón central
    R o Esc Botón de retroceso

Comandos BAD

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

comandos car_service 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 Girar en sentido antihorario varias veces (hace 100 ms y hace 50 ms)
adb shell cmd car_service inject-key 282 Empujar hacia la izquierda
adb shell cmd car_service inject-key 283 Empujar hacia 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é en funcionamiento, esta es la opción más realista. Es particularmente útil para probar la rotación rápida.

EnfoqueEstacionamientoVer

FocusParkingView es una vista transparente en la biblioteca de UI del automóvil (car-ui-library) . RotaryService lo utiliza para admitir la navegación del controlador giratorio. 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ás usando el diseño base car-ui-library, que contiene un FocusParkingView , no necesitas 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 foco automáticamente cuando el foco se establece en otra ventana. Si intenta borrar el foco en la ventana anterior, Android vuelve a enfocar una vista en esa ventana, lo que da como resultado que se enfoquen 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 de enfoque es un FocusParkingView , puede determinar que está a punto de ocurrir una envoltura y en ese momento evita la envoltura al no mover el enfoque.
  3. Cuando el control giratorio inicia una aplicación, Android enfoca la primera vista enfocable, que siempre es FocusParkingView . FocusParkingView determina la vista óptima en la que enfocarse y luego aplica el enfoque.

Vistas enfocables

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

RotaryService solo se centra en vistas que son enfocables. Algunas vistas, como las de Button , suelen ser enfocables. Otros, como TextView y ViewGroup , normalmente no lo son. Las vistas en las que se puede hacer clic se pueden enfocar automáticamente y se puede hacer clic en las vistas automáticamente cuando tienen un detector de clics. Si esta lógica automática da como resultado la capacidad de enfoque deseada, no es necesario 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 configure mediante programación la capacidad de enfoque de la vista con View.setFocusable(boolean) . Para que RotaryService se centre en ello, una vista DEBE cumplir 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 la 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 realizar un seguimiento de 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 es false .
  6. Para que el botón aparezca deshabilitado, en el fondo dibujable de tu vista, usa 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 algún diseño, reemplace android:enabled="false" con app:state_rotary_enabled="false" y luego agregue el espacio de nombres 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 ser coherente con otras aplicaciones. Por ejemplo, si su aplicación tiene una barra de herramientas, ésta debe estar en un FocusArea separada 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 deberían 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 la biblioteca car-ui. Cuando esta función está habilitada, FocusArea resalta cuando uno de sus descendientes está enfocado. Para obtener más información, consulte Personalización de aspectos destacados de Focus .

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

NO anide un FocusArea en otro FocusArea . Hacerlo conduce 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 desplazamiento, 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 por todas las vistas enfocables, lo que podría ser adecuado para los diálogos.

Personalización del área de enfoque

Se pueden utilizar 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 utiliza para controlar el orden de tabulación para la navegación con el teclado. NO utilice este atributo para crear un bucle. En su lugar, utilice 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 utilice este atributo y app:defaultFocus (ver más abajo) en la misma FocusArea .

FocusArea también define algunos atributos para personalizar la navegación giratoria. 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 se acerca a esta FocusArea .
  2. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    app:defaultFocusOverridesHistory se puede establecer en true para que la vista especificada anteriormente se enfoque incluso si tiene un historial para indicar que se ha enfocado otra vista en esta FocusArea .
  3. ( Androide 12 )
    Utilice 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 desplazamiento a continuación.

    ( Android 11 QPR3, Android 11 Car, obsoleto en Android 12 ) app:nudgeShortcut y app:nudgeShortcutDirection solo admitían un atajo de desplazamiento.

  4. ( Android 11 QPR3, Android 11 Coche, Android 12 )
    Para permitir que la rotación se ajuste a esta FocusArea , app:wrapAround se puede establecer en true . Esto se utiliza con mayor frecuencia cuando las vistas están dispuestas en un círculo u ó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 (o áreas) adyacentes en las instrucciones dadas, use app:nudgeLeft , app:nudgeRight , app:nudgeUp y app:nudgeDown . Utilícelo cuando la búsqueda geométrica utilizada por defecto no encuentre el objetivo deseado.

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

Empujar atajo
Figura 3. Atajo de desplazamiento

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 de enfoque de vista existente en el marco de Android. Cuando el usuario gira y empuja, RotaryService mueve el enfoque, enfocando una vista y desenfocando otra. En Android, cuando una vista está enfocada, 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 aspectos destacados de enfoque adecuados.

El resaltado de enfoque predeterminado lo proporciona el marco de trabajo de Android y el OEM puede anularlo. Los desarrolladores de aplicaciones lo reciben cuando el tema que utilizan 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 con una forma personalizada (por ejemplo, redonda o en forma de píldora), o si está usando un tema no derivado de Theme.DeviceDefault , use los recursos 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. Normalmente, cambiarías el fondo. El siguiente dibujable, 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 que se utiliza para tocar. 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 asigna un color de fondo sólido para llamar la atención del usuario, como en el siguiente ejemplo. Esto puede hacer que el foco sea difícil de ver. En esta situación, especifique un resaltado de enfoque personalizado utilizando 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
  • ( Androide 12 )
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

Por ejemplo:

Centrado, no presionadoCentrado, presionado
Centrado, no presionado Centrado, presionado

Desplazamiento giratorio

Si su aplicación usa RecyclerView s, DEBE 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 .

Si todos los elementos de su lista son enfocables, no necesita hacer nada más. La navegación giratoria mueve el foco 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, lo que permite al usuario usar el controlador giratorio para desplazarse gradualmente por la lista sin omitir 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, incluida av CarUiRecyclerView , con el método setRotaryScrollEnabled() en CarUiUtils . Si lo hace, necesita:

  • Haga que la vista desplazable sea enfocable para que pueda enfocarse cuando ninguna de sus vistas descendientes enfocables esté visible.
  • Deshabilite el resaltado de enfoque predeterminado en la vista desplazable llamando 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 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Escuche MotionEvents con SOURCE_ROTARY_ENCODER y AXIS_VSCROLL o AXIS_HSCROLL para indicar la distancia a desplazar y la dirección (a través del letrero).

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. Puedes implementar un efecto similar si lo deseas.

Los MotionEvents son los mismos que los generados por la rueda de desplazamiento del 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 toman acción, aunque este no es siempre el caso. 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 regresar a la navegación. . Esto se conoce como modo de manipulación directa (DM) . En este modo, el controlador giratorio se utiliza para interactuar directamente con la vista 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 apropiadamente a ACTION_SCROLL_FORWARD y ACTION_SCROLL_BACKWARD AccessibilityEvent , 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 utilizar cualquiera de los dos mecanismos.

Mecanismo sencillo

( Android 11 QPR3, Android 11 Coche, Android 12 )
Su aplicación debe llamar 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 la seleccione. Por ejemplo, cambie el fondo cuando android:state_selected sea true .

Mecanismo avanzado

La aplicación determina cuándo RotaryService entra y sale del modo DM. Para una experiencia de usuario consistente, al presionar el botón Central con una vista DM enfocada se 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 empujar, 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,
    • Amplíe la vista y luego anule su método dispatchKeyEvent() .
  2. DEBE 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. DEBE escuchar MotionEvent sy obtener el recuento de rotación en AXIS_SCROLL si la vista quiere manejar la rotación. Hay varias formas de hacer esto:
    1. Registre un OnGenericMotionListener .
    2. Amplíe 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 Actividad a la que pertenece la vista no es interactivo.
  5. DEBE 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 desplazarse 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

Cuando se utiliza un ActivityView:

  • ActivityView no debería poder enfocarse.
  • ( Android 11 QPR3, Android 11 Car, obsoleto en Android 11 )
    El contenido de ActivityView DEBE contener un 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, ActivityViews no debería tener ningún efecto en la navegación, excepto que las áreas de enfoque no pueden abarcar ActivityViews. En otras palabras, no puede tener una única área de enfoque que tenga contenido dentro y fuera 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 giratorios, 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 de un automóvil de dos maneras, ya sea usando el controlador giratorio o tocando la pantalla. Cuando se utiliza el controlador giratorio, se resalta 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 → toque. Cuando el usuario toca la pantalla, el foco resaltado desaparece.
  • Toque → giratorio. Cuando el usuario empuja, gira o presiona el botón central, aparece el resaltado de enfoque.

Los botones Atrás e Inicio no tienen ningún 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 utilizar OnTouchModeChangeListener para escuchar los cambios. Si bien esto se puede utilizar para personalizar su interfaz de usuario para el modo de entrada actual, evite realizar cambios importantes, ya que pueden resultar desconcertantes.

Solución de problemas

En una aplicación diseñada para el tacto, es común tener vistas enfocables anidadas. Por ejemplo, puede haber un FrameLayout alrededor de un ImageButton , los cuales se pueden enfocar. Esto no daña el tacto, pero puede resultar en una mala experiencia de usuario para el modo giratorio porque el usuario debe girar el controlador dos veces para pasar a la siguiente vista interactiva. Para una buena experiencia de usuario, Google recomienda hacer enfocable la vista exterior o la vista interior, pero no ambas.

Si un botón o interruptor pierde el foco cuando se presiona a través del controlador giratorio, podría aplicarse 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 interruptor y haga que el contenedor sea enfocable en lugar del botón o interruptor. (El detector de clics debe estar en el contenedor).
  • Se está reemplazando el botón o interruptor. Por ejemplo, la acción realizada cuando se presiona el botón o se activa el interruptor puede desencadenar una actualización de las acciones disponibles, lo que hace que nuevos botones reemplacen a los botones existentes. Hay dos maneras de abordar esto:
    • En lugar de crear un nuevo botón o interruptor, configure el ícono y/o el texto del botón o interruptor existente.
    • Como arriba, agregue un contenedor enfocable alrededor del botón o interruptor.

Patio de juegos giratorio

RotaryPlayground es una aplicación de referencia para Rotary. Úselo para aprender cómo integrar funciones giratorias en sus aplicaciones. RotaryPlayground se incluye en compilaciones de emuladores y en compilaciones para dispositivos que ejecutan el sistema operativo Android Automotive (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 navegar por las áreas de enfoque, omitiendo elementos no enfocables y entrada de texto.
  • Manipulación directa. Pruebe widgets que admitan el modo de manipulación directa simple y avanzado. Esta pestaña es específicamente para 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 ventanas del sistema donde solo se admite el modo de manipulación directa simple.
  • Red. Pruebe la navegación giratoria con patrón z con desplazamiento.
  • Notificación. Pruebe cómo entrar y salir de las notificaciones de aviso.
  • Desplazarse. Pruebe desplazarse por una combinación de contenido enfocable y no enfocable.
  • Vista web. Pruebe navegar a través de enlaces en un WebView .
  • FocusArea personalizada. Pruebe la personalización FocusArea :
    • Envolver alrededor.
    • android:focusedByDefault y app:defaultFocus
    • .
    • Objetivos de empujón explícitos.
    • Empujar atajos.
    • FocusArea sin vistas enfocables.