Desenvolvendo aplicativos

O material a seguir é para desenvolvedores de aplicativos.

Para tornar o suporte do seu aplicativo rotativo, você DEVE:

  1. Coloque um FocusParkingView no respectivo layout de atividade.
  2. Certifique-se de que as visualizações sejam (ou não) focalizáveis.
  3. Use FocusArea s para envolver todas as suas visualizações focalizáveis, exceto FocusParkingView .

Cada uma dessas tarefas é detalhada abaixo, depois de configurar seu ambiente para desenvolver aplicativos habilitados para rotação.

Configurar um controlador rotativo

Antes de começar a desenvolver aplicativos habilitados para rotação, você precisa de um controlador giratório ou de um substituto. Você tem as opções descritas abaixo.

Emulador

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

Você também pode usar aosp_car_x86_64-userdebug .

Para acessar o controlador rotativo emulado:

  1. Toque nos três pontos na parte inferior da barra de ferramentas:

    Acesse o controlador rotativo emulado
    Figura 1. Acesse o controlador rotativo emulado
  2. Selecione Carro giratório na janela de controles estendidos:

    Selecione a rotativa do carro
    Figura 2. Selecione o carro giratório

Teclado USB

  • Conecte um teclado USB ao seu dispositivo que executa o Android Automotive OS (AAOS). Em alguns casos, isso pode impedir que o teclado na tela apareça.
  • Use um userdebug ou eng build.
  • Ativar filtragem de eventos de chave:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • Consulte a tabela abaixo para encontrar a tecla correspondente para cada ação:
    Chave Ação do Rotary
    Q Girar no sentido anti-horário
    E Rode no sentido dos ponteiros do relógio
    UMA Empurrar para a esquerda
    D Deslocar para a direita
    C Empurrar para cima
    S Empurrar para baixo
    F ou Vírgula Botão central
    R ou Esc Botão "voltar

Comandos ADB

Você pode usar comandos car_service para injetar eventos de entrada rotativos. Esses comandos podem ser executados em dispositivos que executam o Android Automotive OS (AAOS) ou em um emulador.

comandos car_service Entrada rotativa
adb shell cmd car_service inject-rotary Girar no sentido anti-horário
adb shell cmd car_service inject-rotary -c true Rode no sentido dos ponteiros do relógio
adb shell cmd car_service inject-rotary -dt 100 50 Gire no sentido anti-horário várias vezes (100 ms atrás e 50 ms atrás)
adb shell cmd car_service inject-key 282 Empurrar para a esquerda
adb shell cmd car_service inject-key 283 Deslocar para a direita
adb shell cmd car_service inject-key 280 Empurrar para cima
adb shell cmd car_service inject-key 281 Empurrar para baixo
adb shell cmd car_service inject-key 23 Clique no botão central
adb shell input keyevent inject-key 4 Clique no botão Voltar

controlador rotativo OEM

Quando o hardware do seu controlador rotativo está funcionando, esta é a opção mais realista. É particularmente útil para testar a rotação rápida.

FocusParkingView

FocusParkingView é uma visualização transparente na Car UI Library (car-ui-library) . RotaryService o usa para dar suporte à navegação do controlador rotativo. FocusParkingView deve ser a primeira visualização focalizável no layout. Ele deve ser colocado fora de todos os FocusArea s. Cada janela deve ter um FocusParkingView . Se você já estiver usando o layout base car-ui-library, que contém um FocusParkingView , não será necessário adicionar outro FocusParkingView . Abaixo está um exemplo de FocusParkingView no 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>

Aqui estão os motivos pelos quais você precisa de um FocusParkingView :

  1. O Android não limpa o foco automaticamente quando o foco é definido em outra janela. Se você tentar limpar o foco na janela anterior, o Android refocará uma visualização nessa janela, o que resultará em duas janelas sendo focadas simultaneamente. Adicionar um FocusParkingView a cada janela pode corrigir esse problema. Essa visualização é transparente e seu destaque de foco padrão está desabilitado, de modo que fica invisível para o usuário, independentemente de estar focado ou não. Ele pode tirar o foco para que o RotaryService possa estacionar o foco nele para remover o destaque do foco.
  2. Se houver apenas uma FocusArea na janela atual, girar o controlador na FocusArea fará com que o RotaryService mova o foco da visualização à direita para a visualização à esquerda (e vice-versa). Adicionar essa visualização a cada janela pode corrigir o problema. Quando RotaryService determina que o alvo do foco é um FocusParkingView , ele pode determinar que um wrap-around está prestes a ocorrer em que ponto ele evita o wrap-around ao não mover o foco.
  3. Quando o controle giratório inicia um aplicativo, o Android foca a primeira visualização focalizável, que é sempre a FocusParkingView . O FocusParkingView determina a visualização ideal para focar e, em seguida, aplica o foco.

Visualizações focalizáveis

RotaryService baseia-se no conceito existente de foco de visualização da estrutura do Android, que remonta à época em que os telefones tinham teclados físicos e D-pads. O atributo android:nextFocusForward existente é reaproveitado para rotativo (consulte Customização FocusArea ), mas android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp e android:nextFocusDown não são.

RotaryService se concentra apenas em pontos de vista que podem ser focalizados. Algumas visualizações, como Button s, geralmente são focalizáveis. Outros, como TextView s e ViewGroup s, geralmente não são. As visualizações clicáveis ​​são automaticamente focalizáveis ​​e as visualizações são automaticamente clicáveis ​​quando possuem um ouvinte de cliques. Se essa lógica automática resultar na focabilidade desejada, você não precisará definir explicitamente a focabilidade da exibição. Se a lógica automática não resultar na focabilidade desejada, defina o atributo android:focusable como true ou false ou defina programaticamente a focabilidade da visualização com View.setFocusable(boolean) . Para que o RotaryService se concentre nisso, uma visão DEVE atender aos seguintes requisitos:

  • Focável
  • Habilitado
  • Visível
  • Tem valores diferentes de zero para largura e altura

Se uma visualização não atender a todos esses requisitos, por exemplo, um botão focalizável, mas desabilitado, o usuário não poderá usar o controle giratório para focar nela. Se você quiser focar em visualizações desativadas, considere usar um estado personalizado em vez de android:state_enabled para controlar como a visualização aparece sem indicar que o Android deve considerá-la desativada. Seu aplicativo pode informar ao usuário por que a visualização é desativada quando tocada. A próxima seção explica como fazer isso.

Estado personalizado

Para adicionar um estado personalizado:

  1. Para adicionar um atributo personalizado à sua visualização. Por exemplo, para adicionar um estado personalizado state_rotary_enabled à classe de exibição CustomView , use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. Para rastrear esse estado, adicione uma variável de instância à sua visualização junto com os métodos de acesso:
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. Para ler o valor do seu atributo quando sua visualização é criada:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. Em sua classe de exibição, substitua o método onCreateDrawableState() e adicione o estado personalizado, quando apropriado. Por exemplo:
    @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. Faça com que o manipulador de cliques da sua visualização tenha um desempenho diferente dependendo de seu estado. Por exemplo, o manipulador de cliques pode não fazer nada ou pode exibir uma notificação quando mRotaryEnabled for false .
  6. Para fazer o botão parecer desabilitado, no desenhável em segundo plano da sua visualização, use app:state_rotary_enabled em vez de android:state_enabled . Se você ainda não o tiver, precisará adicionar:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Se sua visualização estiver desativada em qualquer layout, substitua android:enabled="false" por app:state_rotary_enabled="false" e adicione o namespace do app , como acima.
  8. Se sua visualização estiver desabilitada programaticamente, substitua as chamadas para setEnabled() por chamadas para setRotaryEnabled() .

Área de foco

Use FocusAreas para particionar as visualizações focalizáveis ​​em blocos para facilitar a navegação e ser consistente com outros aplicativos. Por exemplo, se seu aplicativo tiver uma barra de ferramentas, a barra de ferramentas deverá estar em uma FocusArea separada do restante do aplicativo. Barras de guias e outros elementos de navegação também devem ser separados do restante do aplicativo. Listas grandes geralmente devem ter seu próprio FocusArea . Caso contrário, os usuários devem percorrer toda a lista para acessar algumas visualizações.

FocusArea é uma subclasse de LinearLayout na car-ui-library. Quando este recurso está habilitado, um FocusArea irá desenhar um destaque quando um de seus descendentes estiver em foco. Para saber mais, consulte Personalização de destaque de foco .

Ao criar um bloco de navegação no arquivo de layout, se você pretende usar um LinearLayout como um contêiner para esse bloco, use um FocusArea . Caso contrário, envolva o bloco em um FocusArea .

NÃO aninhe um FocusArea em outro FocusArea . Isso levará a um comportamento de navegação indefinido. Certifique-se de que todas as visualizações focáveis ​​estejam aninhadas em um FocusArea .

Um exemplo de FocusArea no RotaryPlayground é mostrado abaixo:

<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 da seguinte forma:

  1. Ao lidar com ações de rotação e deslocamento, RotaryService procura instâncias de FocusArea na hierarquia de visualização.
  2. Ao receber um evento de rotação, RotaryService move o foco para outra View que pode ter foco na mesma FocusArea .
  3. Ao receber um evento de nudge, o RotaryService move o foco para outra visualização que pode focar em outra FocusArea (normalmente adjacente).

Se você não incluir nenhuma FocusAreas em seu layout, a exibição raiz será tratada como uma área de foco implícita. O usuário não pode empurrar para navegar no aplicativo. Em vez disso, eles girarão por todas as visualizações focalizáveis, o que pode ser adequado para diálogos.

Personalização da área de foco

Dois atributos de visualização padrão podem ser usados ​​para personalizar a navegação rotativa:

  • android:nextFocusForward permite que os desenvolvedores de aplicativos especifiquem a ordem de rotação em uma área de foco. Este é o mesmo atributo usado para controlar a ordem de tabulação para navegação pelo teclado. NÃO use este atributo para criar um loop. Em vez disso, use app:wrapAround (veja abaixo) para criar um loop.
  • android:focusedByDefault permite que os desenvolvedores de aplicativos especifiquem a visualização de foco padrão na janela. NÃO use este atributo e app:defaultFocus (veja abaixo) na mesma FocusArea .

FocusArea também define alguns atributos para personalizar a navegação rotativa. As áreas de foco implícitas não podem ser personalizadas com esses atributos.

  1. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    app:defaultFocus pode ser usado para especificar o ID de uma visualização descendente focalizável, que deve ser focada quando o usuário direcionar para este FocusArea .
  2. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    app:defaultFocusOverridesHistory pode ser definido como true para fazer com que a exibição especificada acima fique em foco, mesmo que com histórico para indicar que outra exibição nesta FocusArea foi focada.
  3. ( Android12 )
    Use app:nudgeLeftShortcut , app:nudgeRightShortcut , app:nudgeUpShortcut e app:nudgeDownShortcut para especificar o ID de uma visualização descendente focalizável, que deve ser focada quando o usuário se deslocar em uma determinada direção. Para saber mais, consulte o conteúdo dos atalhos de deslocamento abaixo.

    ( Android 11 QPR3, Android 11 Car, obsoleto no Android 12 ) app:nudgeShortcut e app:nudgeShortcutDirection suportavam apenas um atalho de deslocamento.

  4. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    Para habilitar a rotação para envolver neste FocusArea , app:wrapAround pode ser definido como true . Isso é mais usado quando as vistas são organizadas em um círculo ou oval.
  5. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    Para ajustar o preenchimento do destaque nesta FocusArea , use app:highlightPaddingStart , app:highlightPaddingEnd , app:highlightPaddingTop , app:highlightPaddingBottom , app:highlightPaddingHorizontal e app:highlightPaddingVertical .
  6. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    Para ajustar os limites percebidos desta FocusArea para encontrar um destino de deslocamento, use app:startBoundOffset , app:endBoundOffset , app:topBoundOffset , app:bottomBoundOffset , app:horizontalBoundOffset e app:verticalBoundOffset .
  7. ( Android 11 QPR3, Android 11 Carro, Android 12 )
    Para especificar explicitamente o ID de uma FocusArea adjacente (ou áreas) nas direções fornecidas, use app:nudgeLeft , app:nudgeRight , app:nudgeUp e app:nudgeDown . Use isso quando a pesquisa geométrica usada por padrão não encontrar o alvo desejado.

Nudging geralmente navega entre FocusAreas. Mas com atalhos de deslocamento, o deslocamento às vezes primeiro navega dentro de um FocusArea para que o usuário precise dar um empurrão duas vezes para navegar para o próximo FocusArea . Atalhos de deslocamento são úteis quando um FocusArea contém uma lista longa seguida por um botão de ação flutuante , como no exemplo abaixo:

Atalho de deslocamento
Figura 3. Atalho de deslocamento

Sem o atalho de deslocamento, o usuário teria que percorrer toda a lista para chegar ao FAB.

Personalização de destaque de foco

Conforme observado acima, o RotaryService baseia-se no conceito existente de foco de visualização da estrutura do Android. Quando o usuário gira e empurra, o RotaryService move o foco, focando uma visão e desfocando outra. No Android, quando uma visualização está focada, se a visualização:

  • especificou seu próprio destaque de foco, o Android desenha o destaque de foco da visualização.
  • não especifica um realce de foco e o realce de foco padrão não está desabilitado, o Android desenha o realce de foco padrão para a exibição.

Os aplicativos projetados para toque geralmente não especificam os destaques de foco apropriados.

O destaque de foco padrão é fornecido pela estrutura do Android e pode ser substituído pelo OEM. Os desenvolvedores de aplicativos o recebem quando o tema que estão usando é derivado de Theme.DeviceDefault .

Para uma experiência de usuário consistente, conte com o destaque de foco padrão sempre que possível. Se você precisar de um destaque de foco com formato personalizado (por exemplo, redondo ou em forma de pílula) ou se estiver usando um tema não derivado de Theme.DeviceDefault , use os recursos car-ui-library para especificar seu próprio destaque de foco para cada vista.

Para especificar um realce de foco personalizado para uma exibição, altere o desenhável de plano de fundo ou primeiro plano da exibição para um desenhável que difere quando a exibição é focalizada. Normalmente, você alteraria o plano de fundo. O seguinte drawable, se usado como plano de fundo para uma vista quadrada, produz um destaque de foco 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 ) As referências de recursos em negrito no exemplo acima identificam os recursos definidos pela car-ui-library. O OEM os substitui para ser consistente com o destaque de foco padrão que eles especificam. Isso garante que a cor do realce do foco, a largura do traço e assim por diante não sejam alteradas quando o usuário navegar entre uma exibição com um realce de foco personalizado e uma exibição com o realce de foco padrão. O último item é uma ondulação usada para toque. Os valores padrão usados ​​para os recursos em negrito aparecem da seguinte forma:

Valores padrão para recursos em negrito
Figura 4. Valores padrão para recursos em negrito

Além disso, um destaque de foco personalizado é solicitado quando um botão recebe uma cor de fundo sólida para chamar a atenção do usuário, como no exemplo abaixo. Isso pode dificultar a visualização do destaque do foco. Nessa situação, especifique um destaque de foco personalizado usando cores secundárias :

Cor de fundo sólida
  • ( Android 11 QPR3, Android 11 Carro, Android 12 )
    car_ui_rotary_focus_fill_secondary_color
    car_ui_rotary_focus_stroke_secondary_color
  • ( Android12 )
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

Por exemplo:

Focado, não pressionadoFocado, pressionado
Focado, não pressionado Focado, pressionado

Rolagem rotativa

Se seu aplicativo usa RecyclerView s, você DEVE usar CarUiRecyclerView s. Isso garante que sua interface do usuário seja consistente com outras porque a personalização de um OEM se aplica a todos os CarUiRecyclerView s.

Se todos os elementos da sua lista puderem ser focalizados, você não precisará fazer mais nada. A navegação rotativa move o foco pelos elementos da lista e a lista rola para tornar visível o novo elemento em foco.

( Android 11 QPR3, Android 11 Carro, Android 12 )
Se houver uma mistura de elementos focalizáveis ​​e não focalizáveis, ou se todos os elementos estiverem fora de foco, você poderá habilitar a rolagem rotativa, que permite ao usuário usar o controlador rotativo para rolar gradualmente pela lista sem pular itens não focalizáveis. Para habilitar a rolagem rotativa, defina o atributo app:rotaryScrollEnabled como true .

( Android 11 QPR3, Android 11 Carro, Android 12 )
Você pode habilitar a rolagem rotativa em qualquer visualização rolável, incluindo av CarUiRecyclerView , com o método setRotaryScrollEnabled() em CarUiUtils . Se você fizer isso, você precisa:

  • Torne a visualização rolável focalizável para que ela possa ser focada quando nenhuma de suas visualizações descendentes focáveis ​​estiver visível,
  • Desabilite o destaque de foco padrão na exibição rolável chamando setDefaultFocusHighlightEnabled(false) para que a exibição rolável não pareça estar focada,
  • Certifique-se de que a exibição rolável esteja focada antes de seus descendentes chamando setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) .
  • Ouça MotionEvents com SOURCE_ROTARY_ENCODER e AXIS_VSCROLL ou AXIS_HSCROLL para indicar a distância de rolagem e a direção (através do sinal).

Quando a rolagem rotativa está habilitada em um CarUiRecyclerView e o usuário gira para uma área onde não há visualizações focalizáveis, a barra de rolagem muda de cinza para azul, como se para indicar que a barra de rolagem está focada. Você pode implementar um efeito semelhante, se quiser.

Os MotionEvents são os mesmos gerados por uma roda de rolagem em um mouse, exceto pela fonte.

Modo de manipulação direta

Normalmente, os nudges e a rotação navegam pela interface do usuário, enquanto o botão Central pressiona a ação, embora isso nem sempre seja o caso. Por exemplo, se um usuário deseja ajustar o volume do alarme, ele pode usar o controle giratório para navegar até o controle deslizante de volume, pressionar o botão central, girar o controle para ajustar o volume do alarme e pressionar o botão Voltar para retornar à navegação . Isso é conhecido como modo de manipulação direta (DM) . Nesse modo, o controlador rotativo é usado para interagir diretamente com a visualização, em vez de navegar.

Implemente o DM de duas maneiras. Se você só precisa manipular a rotação e a exibição que deseja manipular responde a ACTION_SCROLL_FORWARD e ACTION_SCROLL_BACKWARD AccessibilityEvent s apropriadamente, use o mecanismo simples . Caso contrário, use o mecanismo avançado .

O mecanismo simples é a única opção nas janelas do sistema; os aplicativos podem usar qualquer mecanismo.

Mecanismo simples

( Android 11 QPR3, Android 11 Carro, Android 12 )
Seu aplicativo deve chamar DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable) . RotaryService reconhece quando o usuário está no modo DM e entra no modo DM quando o usuário pressiona o botão central enquanto uma visualização está focada. Quando no modo DM, as rotações executam ACTION_SCROLL_FORWARD ou ACTION_SCROLL_BACKWARD e sai do modo DM quando o usuário pressiona o botão Voltar. O mecanismo simples alterna o estado selecionado da visualização ao entrar e sair do modo DM.

Para fornecer uma indicação visual de que o usuário está no modo DM, faça sua visualização parecer diferente quando selecionada. Por exemplo, altere o plano de fundo quando android:state_selected for true .

Mecanismo avançado

O aplicativo determina quando RotaryService entra e sai do modo DM. Para uma experiência de usuário consistente, pressionar o botão Central com uma visualização DM focada deve entrar no modo DM e o botão Voltar deve sair do modo DM. Se o botão central e/ou o nudge não forem usados, eles podem ser formas alternativas de sair do modo DM. Para aplicativos como o Maps, um botão para representar o DM pode ser usado para entrar no modo DM.

Para suportar o modo DM avançado, uma visualização:

  1. ( Android 11 QPR3, Android 11 Car, Android 12 ) DEVE ouvir um evento KEYCODE_DPAD_CENTER para entrar no modo DM e ouvir um evento KEYCODE_BACK para sair do modo DM, chamando DirectManipulationHelper.enableDirectManipulationMode() em cada caso. Para ouvir esses eventos, siga um destes procedimentos:
    • Registre um OnKeyListener .
    • ou,
    • Estenda a visualização e, em seguida, substitua seu método dispatchKeyEvent() .
  2. DEVE ouvir eventos de deslocamento ( KEYCODE_DPAD_UP , KEYCODE_DPAD_DOWN , KEYCODE_DPAD_LEFT ou KEYCODE_DPAD_RIGHT ) se a exibição deve manipular deslocamentos.
  3. DEVE ouvir MotionEvent s e obter contagem de rotação em AXIS_SCROLL se a exibição quiser manipular a rotação. Existem várias maneiras de fazer isso:
    1. Registre um OnGenericMotionListener .
    2. Estenda a visualização e substitua seu método dispatchTouchEvent() .
  4. Para evitar ficar preso no modo DM, DEVE sair do modo DM quando o Fragmento ou Atividade à qual a visualização pertence não for interativo.
  5. DEVE fornecer uma dica visual para indicar que a visualização está no modo DM.

Uma amostra de uma visualização personalizada que usa o modo DM para deslocar e ampliar um mapa é fornecida abaixo:

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

Mais exemplos podem ser encontrados no projeto RotaryPlayground .

ActivityView

Ao usar um ActivityView:

  • O ActivityView não deve ser focalizável.
  • ( Android 11 QPR3, Android 11 Car, obsoleto no Android 11 )
    O conteúdo do ActivityView DEVE conter um FocusParkingView como a primeira visualização focalizável e seu atributo app:shouldRestoreFocus DEVE ser false .
  • O conteúdo do ActivityView não deve ter visualizações android:focusByDefault .

Para o usuário, ActivityViews não deve ter efeito na navegação, exceto que as áreas de foco não podem abranger ActivityViews. Em outras palavras, você não pode ter uma única área de foco que tenha conteúdo dentro e fora de um ActivityView . Se você não adicionar nenhum FocusAreas ao seu ActivityView , a raiz da hierarquia de exibição no ActivityView será considerada uma área de foco implícita.

Botões que funcionam quando pressionados

A maioria dos botões causa alguma ação quando clicada. Alguns botões operam quando pressionados. Por exemplo, os botões Fast Forward e Rewind normalmente funcionam quando pressionados. Para tornar esses botões compatíveis com o rotativo, ouça KEYCODE_DPAD_CENTER KeyEvents da seguinte forma:

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

Em que mRunnable uma ação (como retroceder) e se programa para ser executado após um atraso.

Modo de toque

Os usuários podem usar um controlador rotativo para interagir com a unidade principal em um carro de duas maneiras, usando o controlador rotativo ou tocando na tela. Ao usar o controlador rotativo, uma das visualizações focalizáveis ​​será realçada. Ao tocar na tela, nenhum destaque de foco aparece. O usuário pode alternar entre esses modos de entrada a qualquer momento:

  • Rotativo → toque. Quando o usuário toca na tela, o destaque do foco desaparece.
  • Toque em → giratório. Quando o usuário empurra, gira ou pressiona o botão central, o destaque do foco aparece.

Os botões Voltar e Início não têm efeito no modo de entrada.

Rotary pega carona no conceito existente do Android de modo de toque . Você pode usar View.isInTouchMode() para determinar qual modo de entrada o usuário está usando. Você pode usar OnTouchModeChangeListener para ouvir as alterações. Embora isso possa ser usado para personalizar sua interface de usuário para o modo de entrada atual, evite grandes alterações, pois elas podem ser desconcertantes.

Solução de problemas

Em um aplicativo projetado para toque, não é incomum ter visualizações aninhadas com foco. Por exemplo, pode haver um FrameLayout em torno de um ImageButton , sendo que ambos são focalizáveis. Isso não prejudica o toque, mas pode resultar em uma experiência ruim para o usuário giratório, pois o usuário deve girar o controlador duas vezes para passar para a próxima visualização interativa. Para uma boa experiência do usuário, o Google recomenda que você torne a visão externa ou a visão interna focalizável, mas não ambas.

Se um botão ou interruptor perder o foco quando pressionado pelo controlador rotativo, uma destas condições pode ser aplicada:

  • O botão ou interruptor está sendo desabilitado (brevemente ou indefinidamente) devido ao pressionamento do botão. Em ambos os casos, existem duas maneiras de resolver isso:
    • Deixe o estado android:enabled como true e use um estado personalizado para esmaecer o botão ou alternar conforme descrito em Estado personalizado .
    • Use um contêiner para cercar o botão ou interruptor e torne o contêiner focalizável em vez do botão ou interruptor. (O ouvinte de clique deve estar no contêiner.)
  • O botão ou interruptor está sendo substituído. Por exemplo, a ação realizada quando o botão é pressionado ou o interruptor é alternado pode acionar uma atualização das ações disponíveis, fazendo com que novos botões substituam os botões existentes. Existem duas maneiras de resolver isso:
    • Em vez de criar um novo botão ou interruptor, defina o ícone e/ou texto do botão ou interruptor existente.
    • Como acima, adicione um contêiner focalizável ao redor do botão ou switch.

RotaryPlayground

RotaryPlayground é um aplicativo de referência para rotativo. Use-o para saber como integrar recursos rotativos em seus aplicativos. RotaryPlayground está incluído em compilações de emulador e em compilações para dispositivos que executam o Android Automotive OS (AAOS).

  • Repositório do RotaryPlayground : packages/apps/Car/tests/RotaryPlayground/
  • Versões: Android 11 QPR3, Android 11 Car e Android 12

O aplicativo RotaryPlayground mostra as seguintes guias à esquerda:

  • Cartões. Teste a navegação pelas áreas de foco, pulando elementos não focalizáveis ​​e entrada de texto.
  • Manipulação direta. Widgets de teste que suportam o modo de manipulação direta simples e avançado. Esta guia é especificamente para manipulação direta na janela do aplicativo.
  • Manipulação de UI Sys. Widgets de teste que suportam manipulação direta em janelas do sistema onde apenas o modo de manipulação direta simples é suportado.
  • Grade. Teste a navegação rotativa com padrão z com rolagem.
  • Notificação. Teste a entrada e saída de notificações de alerta.
  • Rolagem. Teste a rolagem em uma mistura de conteúdo focalizável e não focalizável.
  • WebView. Teste a navegação por links em um WebView .
  • Área de FocusArea personalizada. Teste a personalização do FocusArea :
    • Envolver em torno.
    • android:focusedByDefault e app:defaultFocus
    • .
    • Alvos de empurrão explícitos.
    • Atalhos de empurrão.
    • FocusArea sem visualizações focalizáveis.
,

O material a seguir é para desenvolvedores de aplicativos.

Para tornar o suporte do seu aplicativo rotativo, você DEVE:

  1. Coloque um FocusParkingView no respectivo layout de atividade.
  2. Certifique-se de que as visualizações sejam (ou não) focalizáveis.
  3. Use FocusArea s para envolver todas as suas visualizações focalizáveis, exceto FocusParkingView .

Cada uma dessas tarefas é detalhada abaixo, depois de configurar seu ambiente para desenvolver aplicativos habilitados para rotação.

Configurar um controlador rotativo

Antes de começar a desenvolver aplicativos habilitados para rotação, você precisa de um controlador giratório ou de um substituto. Você tem as opções descritas abaixo.

Emulador

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

Você também pode usar aosp_car_x86_64-userdebug .

Para acessar o controlador rotativo emulado:

  1. Toque nos três pontos na parte inferior da barra de ferramentas:

    Acesse o controlador rotativo emulado
    Figura 1. Acesse o controlador rotativo emulado
  2. Selecione Carro giratório na janela de controles estendidos:

    Selecione a rotativa do carro
    Figura 2. Selecione o carro giratório

Teclado USB

  • Conecte um teclado USB ao seu dispositivo que executa o Android Automotive OS (AAOS). Em alguns casos, isso pode impedir que o teclado na tela apareça.
  • Use um userdebug ou eng build.
  • Ativar filtragem de eventos de chave:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • Consulte a tabela abaixo para encontrar a tecla correspondente para cada ação:
    Chave Ação do Rotary
    Q Girar no sentido anti-horário
    E Rode no sentido dos ponteiros do relógio
    UMA Empurrar para a esquerda
    D Deslocar para a direita
    C Empurrar para cima
    S Empurrar para baixo
    F ou Vírgula Botão central
    R ou Esc Botão "voltar

Comandos ADB

Você pode usar comandos car_service para injetar eventos de entrada rotativos. Esses comandos podem ser executados em dispositivos que executam o Android Automotive OS (AAOS) ou em um emulador.

comandos car_service Entrada rotativa
adb shell cmd car_service inject-rotary Girar no sentido anti-horário
adb shell cmd car_service inject-rotary -c true Rode no sentido dos ponteiros do relógio
adb shell cmd car_service inject-rotary -dt 100 50 Gire no sentido anti-horário várias vezes (100 ms atrás e 50 ms atrás)
adb shell cmd car_service inject-key 282 Empurrar para a esquerda
adb shell cmd car_service inject-key 283 Deslocar para a direita
adb shell cmd car_service inject-key 280 Empurrar para cima
adb shell cmd car_service inject-key 281 Empurrar para baixo
adb shell cmd car_service inject-key 23 Clique no botão central
adb shell input keyevent inject-key 4 Clique no botão Voltar

controlador rotativo OEM

Quando o hardware do seu controlador rotativo está funcionando, esta é a opção mais realista. É particularmente útil para testar a rotação rápida.

FocusParkingView

FocusParkingView é uma visualização transparente na Car UI Library (car-ui-library) . RotaryService o usa para dar suporte à navegação do controlador rotativo. FocusParkingView deve ser a primeira visualização focalizável no layout. Ele deve ser colocado fora de todos os FocusArea s. Cada janela deve ter um FocusParkingView . Se você já estiver usando o layout base car-ui-library, que contém um FocusParkingView , não será necessário adicionar outro FocusParkingView . Abaixo está um exemplo de FocusParkingView no 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>

Aqui estão os motivos pelos quais você precisa de um FocusParkingView :

  1. O Android não limpa o foco automaticamente quando o foco é definido em outra janela. Se você tentar limpar o foco na janela anterior, o Android refocará uma visualização nessa janela, o que resultará em duas janelas sendo focadas simultaneamente. Adicionar um FocusParkingView a cada janela pode corrigir esse problema. Essa visualização é transparente e seu destaque de foco padrão está desabilitado, de modo que fica invisível para o usuário, independentemente de estar focado ou não. Ele pode tirar o foco para que o RotaryService possa estacionar o foco nele para remover o destaque do foco.
  2. Se houver apenas uma FocusArea na janela atual, girar o controlador na FocusArea fará com que o RotaryService mova o foco da visualização à direita para a visualização à esquerda (e vice-versa). Adicionar essa visualização a cada janela pode corrigir o problema. Quando RotaryService determina que o alvo do foco é um FocusParkingView , ele pode determinar que um wrap-around está prestes a ocorrer em que ponto ele evita o wrap-around ao não mover o foco.
  3. Quando o controle giratório inicia um aplicativo, o Android foca a primeira visualização focalizável, que é sempre a FocusParkingView . O FocusParkingView determina a visualização ideal para focar e, em seguida, aplica o foco.

Visualizações focalizáveis

RotaryService baseia-se no conceito existente de foco de visualização da estrutura do Android, que remonta à época em que os telefones tinham teclados físicos e D-pads. O atributo android:nextFocusForward existente é reaproveitado para rotativo (consulte Customização FocusArea ), mas android:nextFocusLeft , android:nextFocusRight , android:nextFocusUp e android:nextFocusDown não são.

RotaryService se concentra apenas em pontos de vista que podem ser focalizados. Algumas visualizações, como Button s, geralmente são focalizáveis. Outros, como TextView s e ViewGroup s, geralmente não são. As visualizações clicáveis ​​são automaticamente focalizáveis ​​e as visualizações são automaticamente clicáveis ​​quando possuem um ouvinte de cliques. Se essa lógica automática resultar na focabilidade desejada, você não precisará definir explicitamente a focabilidade da exibição. Se a lógica automática não resultar na focabilidade desejada, defina o atributo android:focusable como true ou false ou defina programaticamente a focabilidade da visualização com View.setFocusable(boolean) . Para que o RotaryService se concentre nisso, uma visão DEVE atender aos seguintes requisitos:

  • Focável
  • Habilitado
  • Visível
  • Tem valores diferentes de zero para largura e altura

Se uma visualização não atender a todos esses requisitos, por exemplo, um botão focalizável, mas desabilitado, o usuário não poderá usar o controle giratório para focar nela. Se você quiser focar em visualizações desativadas, considere usar um estado personalizado em vez de android:state_enabled para controlar como a visualização aparece sem indicar que o Android deve considerá-la desativada. Seu aplicativo pode informar ao usuário por que a visualização é desativada quando tocada. A próxima seção explica como fazer isso.

Estado personalizado

Para adicionar um estado personalizado:

  1. Para adicionar um atributo personalizado à sua visualização. Por exemplo, para adicionar um estado personalizado state_rotary_enabled à classe de exibição CustomView , use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. Para rastrear esse estado, adicione uma variável de instância à sua visualização junto com os métodos de acesso:
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. Para ler o valor do seu atributo quando sua visualização é criada:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. Em sua classe de exibição, substitua o método onCreateDrawableState() e adicione o estado personalizado, quando apropriado. Por exemplo:
    @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. Faça com que o manipulador de cliques da sua visualização tenha um desempenho diferente dependendo de seu estado. Por exemplo, o manipulador de cliques pode não fazer nada ou pode exibir uma notificação quando mRotaryEnabled for false .
  6. Para fazer o botão parecer desabilitado, no desenhável em segundo plano da sua visualização, use app:state_rotary_enabled em vez de android:state_enabled . Se você ainda não o tiver, precisará adicionar:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Se sua visualização estiver desativada em qualquer layout, substitua android:enabled="false" por app:state_rotary_enabled="false" e adicione o namespace do app , como acima.
  8. Se sua visualização estiver desabilitada programaticamente, substitua as chamadas para setEnabled() por chamadas para setRotaryEnabled() .

Área de foco

Use FocusAreas para particionar as visualizações focalizáveis ​​em blocos para facilitar a navegação e ser consistente com outros aplicativos. Por exemplo, se seu aplicativo tiver uma barra de ferramentas, a barra de ferramentas deverá estar em uma FocusArea separada do restante do aplicativo. Barras de guias e outros elementos de navegação também devem ser separados do restante do aplicativo. Listas grandes geralmente devem ter seu próprio FocusArea . Caso contrário, os usuários devem percorrer toda a lista para acessar algumas visualizações.

FocusArea é uma subclasse de LinearLayout na car-ui-library. Quando este recurso está habilitado, um FocusArea irá desenhar um destaque quando um de seus descendentes estiver em foco. Para saber mais, consulte Personalização de destaque de foco .

Ao criar um bloco de navegação no arquivo de layout, se você pretende usar um LinearLayout como um contêiner para esse bloco, use um FocusArea . Caso contrário, envolva o bloco em um FocusArea .

NÃO aninhe um FocusArea em outro FocusArea . Isso levará a um comportamento de navegação indefinido. Certifique-se de que todas as visualizações focáveis ​​estejam aninhadas em um FocusArea .

Um exemplo de FocusArea no RotaryPlayground é mostrado abaixo:

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

FocusArea works as follows:

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

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

FocusArea customization

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

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

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

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

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

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

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

Nudge shortcut
Figure 3. Nudge shortcut

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

Focus highlight customization

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

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

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

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

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

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

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

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

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

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

Solid background color
  • ( Android 11 QPR3, Android 11 Car, Android 12 )
    car_ui_rotary_focus_fill_secondary_color
    car_ui_rotary_focus_stroke_secondary_color
  • ( Android 12 )
    car_ui_rotary_focus_pressed_fill_secondary_color
    car_ui_rotary_focus_pressed_stroke_secondary_color

Por exemplo:

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

Rotary scrolling

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

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

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

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

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

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

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

Direct manipulation mode

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

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

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

Simple mechanism

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

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

Advanced mechanism

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

To support advanced DM mode, a view:

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

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

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

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

More examples can be found in the RotaryPlayground project.

ActivityView

When using an ActivityView:

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

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

Buttons that operate when held down

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

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

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

Touch mode

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

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

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

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

Troubleshooting

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

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

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

RotaryPlayground

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

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

The RotaryPlayground app shows the following tabs on the left:

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