Desenvolver apps

O material a seguir é destinado a desenvolvedores de apps.

Para que o app ofereça suporte a controles rotativos, é PRECISO:

  1. Coloque uma FocusParkingView no layout da atividade correspondente.
  2. Verifique se as visualizações podem ou não ser focalizadas.
  3. Use FocusAreas para agrupar todas as visualizações com foco, exceto FocusParkingView.

Cada uma dessas tarefas é detalhada abaixo, depois que você configurar seu ambiente para desenvolver apps com botão giratório.

Configurar um controle giratório

Antes de começar a desenvolver apps com controle giratório, você precisa de um controle giratório ou um substituto. Confira as opções descritas abaixo.

Emulador

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

Também é possível usar aosp_car_x86_64-userdebug.

Para acessar o controle giratório emulado:

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

    Acessar o controle giratório emulado
    Figura 1. Acessar o controle giratório emulado
  2. Selecione Car rotary na janela de controles estendidos:

    Selecionar o botão Car
    Figura 2. Selecione o botão rotativo Car

Teclado USB

  • Conecte um teclado USB ao dispositivo que executa o Android Automotive OS (AAOS). Em alguns casos, isso impede que o teclado na tela apareça.
  • Use um build userdebug ou eng.
  • Ativar a filtragem de eventos principais:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • Consulte a tabela abaixo para encontrar a chave correspondente a cada ação:
    Chave Ação de rotação
    P Girar no sentido anti-horário
    E Girar no sentido horário
    A Deslocar para esquerda
    D Deslocar para a direita
    W Deslocar para cima
    S Deslocar para baixo
    F ou vírgula Botão central
    R ou Esc Botão "Voltar"

Comandos ADB

É possível usar comandos car_service para injetar eventos de entrada por seletor giratório. Esses comandos podem ser executados em dispositivos com o Android Automotive OS (AAOS) ou em um emulador.

comandos car_service Entrada por seletor giratório
adb shell cmd car_service inject-rotary Girar no sentido anti-horário
adb shell cmd car_service inject-rotary -c true Girar no sentido horário
adb shell cmd car_service inject-rotary -dt 100 50 Girar no sentido anti-horário várias vezes (há 100 ms e 50 ms)
adb shell cmd car_service inject-key 282 Deslocar para esquerda
adb shell cmd car_service inject-key 283 Deslocar para a direita
adb shell cmd car_service inject-key 280 Deslocar para cima
adb shell cmd car_service inject-key 281 Deslocar 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"

Controle giratório OEM

Quando o hardware do controlador rotativo estiver em funcionamento, essa é a opção mais realista. É especialmente útil para testar a rotação rápida.

FocusParkingView

FocusParkingView é uma visualização transparente na biblioteca de interface do carro (car-ui-library). O RotaryService usa essa API para oferecer suporte à navegação com o controle giratório. FocusParkingView precisa ser a primeira visualização focada no layout. Ele precisa ser colocado fora de todos os FocusAreas. Cada janela precisa ter um FocusParkingView. Se você já estiver usando o layout base da car-ui-library, que contém um FocusParkingView, não será necessário adicionar outro FocusParkingView. Confira abaixo um exemplo de FocusParkingView em 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>

Confira os motivos para usar um FocusParkingView:

  1. O Android não limpa o foco automaticamente quando ele é definido em outra janela. Se você tentar limpar o foco na janela anterior, o Android vai redirecionar uma visualização nessa janela, o que resulta em duas janelas com foco ao mesmo tempo. Adicionar um FocusParkingView a cada janela pode corrigir esse problema. Essa visualização é transparente, e o destaque de foco padrão é desativado, para que fique invisível para o usuário, independentemente de estar com foco ou não. Ele pode receber o foco para que RotaryService possa estacionar o foco nele para remover o destaque de foco.
  2. Se houver apenas um FocusArea na janela atual, girar o controle no FocusArea fará com que 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 destino de foco é um FocusParkingView, ele pode determinar que uma contagem regressiva está prestes a ocorrer, momento em que ele evita a contagem regressiva sem mover o foco.
  3. Quando o controle giratório inicia um app, o Android foca a primeira visualização focalizável, que é sempre a FocusParkingView. O FocusParkingView determina a visualização ideal para aplicar o foco.

Visualizações focalizáveis

O RotaryService se baseia no conceito de focus de visualização do framework do Android, que remonta à época em que os smartphones tinham teclados físicos e direcionais D-pad. O atributo android:nextFocusForward existente foi modificado para rotary (consulte Personalização de FocusArea), mas android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp e android:nextFocusDown não foram.

RotaryService se concentra apenas em visualizações que podem ser focalizadas. Algumas visualizações, como Buttons, geralmente podem ser focalizadas. Outros, como TextViews e ViewGroups, geralmente não são. As visualizações clicáveis são automaticamente focalizáveis e clicáveis quando têm um listener de cliques. Se essa lógica automática resultar na capacidade de foco desejada, não será necessário definir explicitamente a capacidade de foco da visualização. Se a lógica automática não resultar na capacidade de foco desejada, defina o atributo android:focusable como true ou false, ou defina a capacidade de foco da visualização de forma programática com View.setFocusable(boolean). Para que RotaryService se concentre nele, uma visualização PRECISA atender aos seguintes requisitos:

  • Focalizável
  • Ativado
  • Visível
  • Ter valores diferentes de zero para largura e altura

Se uma visualização não atender a todos esses requisitos, por exemplo, um botão com foco, mas desativado, o usuário não poderá usar o controle giratório para focar nele. Se você quiser focar em visualizações desativadas, use um estado personalizado em vez de android:state_enabled para controlar como a visualização aparece sem indicar que o Android precisa considerá-la desativada. O app pode informar ao usuário por que a visualização está desativada quando ele toca nela. A próxima seção explica como fazer isso.

Estado personalizado

Para adicionar um estado personalizado:

  1. Para adicionar um atributo personalizado à visualização. Por exemplo, para adicionar um estado personalizado state_rotary_enabled à classe de visualização CustomView, use:
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. Para acompanhar esse estado, adicione uma variável de instância à visualização com 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 atributo quando a vista é criada:
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. Na classe de visualização, substitua o método onCreateDrawableState() e adicione o estado personalizado, quando apropriado. 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 gerenciador de cliques da visualização funcione de maneira diferente dependendo do estado dela. Por exemplo, o gerenciador de cliques pode não fazer nada ou exibir uma mensagem pop-up quando mRotaryEnabled for false.
  6. Para fazer com que o botão apareça desativado, no drawable de plano de fundo da sua visualização, use app:state_rotary_enabled em vez de android:state_enabled. Se você ainda não tiver, adicione:
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. Se a visualização estiver desativada em qualquer layout, substitua android:enabled="false" por app:state_rotary_enabled="false" e adicione o namespace app, conforme mostrado acima.
  8. Se a visualização estiver desativada programaticamente, substitua as chamadas para setEnabled() por chamadas para setRotaryEnabled().

FocusArea

Use FocusAreas para dividir as visualizações com foco em blocos para facilitar a navegação e manter a consistência com outros apps. Por exemplo, se o app tiver uma barra de ferramentas, ela precisa estar em uma FocusArea separada do restante do app. As barras de guias e outros elementos de navegação também precisam ser separados do restante do app. Listas grandes geralmente têm uma FocusArea própria. Caso contrário, os usuários precisam percorrer toda a lista para acessar algumas visualizações.

FocusArea é uma subclasse de LinearLayout na biblioteca car-ui-library. Quando esse recurso está ativado, FocusArea mostra um destaque quando um dos descendentes está em foco. Para saber mais, consulte Personalização do 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 FocusArea. Caso contrário, encapsule o bloco em um FocusArea.

NÃO aninhe um FocusArea em outro FocusArea. Isso leva a um comportamento de navegação indefinido. Verifique se todas as visualizações com foco estão aninhadas em um FocusArea.

Confira abaixo um exemplo de FocusArea em 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 da seguinte maneira:

  1. Ao processar ações de rotação e empurrão, 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 visualização que pode receber o foco no mesmo FocusArea.
  3. Ao receber um evento de lembrete, o RotaryService move o foco para outra visualização que pode receber o foco em outra FocusArea (geralmente adjacente).

Se você não incluir FocusAreas no layout, a visualização raiz será tratada como uma área de foco implícita. O usuário não pode tocar para navegar no app. Em vez disso, ele vai alternar entre todas as visualizações com foco, o que pode ser adequado para caixas de diálogo.

Personalização de FocusArea

Dois atributos padrão da visualização podem ser usados para personalizar a navegação por botão giratório:

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

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

  1. (Android 11 QPR3, Android 11 Car, Android 12)
    app:defaultFocus pode ser usado para especificar o ID de uma visualização de descendente com foco, que precisa ser focada quando o usuário toca nela.FocusArea
  2. (Android 11 QPR3, Android 11 Car, Android 12)
    app:defaultFocusOverridesHistory pode ser definido como true para que a visualização especificada acima receba o foco, mesmo que com histórico para indicar que outra visualização em FocusArea foi focada.
  3. (Android 12)
    Use app:nudgeLeftShortcut, app:nudgeRightShortcut, app:nudgeUpShortcut e app:nudgeDownShortcut para especificar o ID de uma visualização descendente com foco, que precisa ser focada quando o usuário desliza em uma determinada direção. Para saber mais, consulte o conteúdo sobre atalhos de sugestão abaixo.

    (Android 11 QPR3, Android 11 Car, descontinuado no Android 12) app:nudgeShortcut e app:nudgeShortcutDirection tinham suporte apenas a um atalho de toque.

  4. (Android 11 QPR3, Android 11 Car, Android 12)
    Para permitir que a rotação seja aplicada a este FocusArea, a app:wrapAround pode ser definida como true. Isso é mais comum quando as visualizações são organizadas em um círculo ou oval.
  5. (Android 11 QPR3, Android 11 Car, Android 12)
    Para ajustar o padding do destaque neste FocusArea, use app:highlightPaddingStart, app:highlightPaddingEnd, app:highlightPaddingTop, app:highlightPaddingBottom, app:highlightPaddingHorizontal e app:highlightPaddingVertical.
  6. (Android 11 QPR3, Android 11 Car, Android 12)
    Para ajustar os limites percebidos dessa FocusArea e encontrar um alvo de empurrão, use app:startBoundOffset, app:endBoundOffset, app:topBoundOffset, app:bottomBoundOffset, app:horizontalBoundOffset e app:verticalBoundOffset.
  7. (Android 11 QPR3, Android 11 Car, Android 12)
    Para especificar explicitamente o ID de um FocusArea adjacente (ou áreas) nas direções fornecidas, use app:nudgeLeft, app:nudgeRight, app:nudgeUp e app:nudgeDown. Use essa opção quando a pesquisa geométrica usada por padrão não encontrar o destino desejado.

O nudge geralmente navega entre as áreas de foco. No entanto, com atalhos de nudge, às vezes o nudge primeiro navega dentro de um FocusArea, de modo que o usuário pode precisar fazer o nudge duas vezes para navegar até o próximo FocusArea. Os atalhos de empurrão 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 para lembrete
Figura 3. Atalho de lembrete

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

Personalização do destaque de foco

Como observado acima, o RotaryService é baseado no conceito de foco de visualização do framework do Android. Quando o usuário gira e move, RotaryService move o foco, focando uma visualização e desfocando outra. No Android, quando uma visualização está em foco, se ela:

  • Especificou o próprio destaque de foco. O Android desenha o destaque de foco da visualização.
  • Não especifica um destaque de foco e o destaque de foco padrão não está desativado. O Android mostra o destaque de foco padrão para a visualização.

Os apps projetados para toque geralmente não especificam os destaques de foco adequados.

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

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

Para especificar um destaque de foco personalizado para uma visualização, mude o drawable de plano de fundo ou de primeiro plano da visualização para um drawable diferente quando a visualização estiver em foco. Normalmente, você muda o plano de fundo. O drawable a seguir, se usado como plano de fundo para uma visualização 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 recursos definidos pela car-ui-library. O OEM substitui esses valores para que eles sejam consistentes com o destaque de foco padrão especificado. Isso garante que a cor do destaque de foco, a largura do traço e assim por diante não mudem quando o usuário navega entre uma visualização com um destaque de foco personalizado e uma visualização com o destaque 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 maneira:

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

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

Cor sólida de plano de fundo
  • (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

Exemplo:

Focado, não pressionado Foco, pressionado
Em foco, não pressionado Foco, pressionado

Rolagem por seletor giratório

Se o app usa RecyclerViews, use CarUiRecyclerViews. Isso garante que sua interface seja consistente com outras, porque a personalização de um OEM é aplicada a todos os CarUiRecyclerViews.

Se todos os elementos da lista puderem ser focados, não será necessário fazer mais nada. A navegação rotativa move o foco pelos elementos na lista, e a lista rola para tornar o elemento recém-focado visível.

(Android 11 QPR3, Android 11 Car, Android 12)
Se houver uma mistura de elementos com e sem foco, ou se todos os elementos não tiverem foco, será possível ativar a rolagem por botão giratório, que permite que o usuário use o controle giratório para rolar gradualmente pela lista sem pular itens sem foco. Para ativar a rolagem giratória, defina o atributo app:rotaryScrollEnabled como true.

(Android 11 QPR3, Android 11 Car, Android 12)
É possível ativar a rolagem giratória em qualquer visualização rolável, incluindo avCarUiRecyclerView, com o método setRotaryScrollEnabled() em CarUiUtils. Se você fizer isso, faça o seguinte:

  • Tornar a visualização rolável focalizável para que ela possa ser focada quando nenhuma das visualizações filhas focalizáveis estiverem visíveis.
  • Desativar o destaque de foco padrão na visualização rolável chamando setDefaultFocusHighlightEnabled(false) para que a visualização rolável não pareça estar em foco.
  • Chame setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) para garantir que a visualização rolável seja focada antes dos descendentes.
  • Detecte MotionEvents com SOURCE_ROTARY_ENCODER e AXIS_VSCROLL ou AXIS_HSCROLL para indicar a distância de rolagem e a direção (pelo sinal).

Quando a rolagem giratória é ativada em um CarUiRecyclerView e o usuário gira para uma área em que não há visualizações focadas, 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 iguais aos gerados por uma roda de rolagem em um mouse, exceto pela origem.

Modo de manipulação direta

Normalmente, os empurrões e a rotação navegam pela interface do usuário, enquanto as pressões no botão central executam uma ação, embora nem sempre seja assim. Por exemplo, se um usuário quiser ajustar o volume do alarme, ele poderá 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 é chamado de modo de manipulação direta (DM). Nesse modo, o controle giratório é usado para interagir com a visualização diretamente, em vez de navegar.

Implemente a DM de duas maneiras. Se você só precisa processar a rotação e a visualização que quer manipular responde corretamente a AccessibilityEvents ACTION_SCROLL_FORWARD e ACTION_SCROLL_BACKWARD, use o mecanismo simples. Caso contrário, use o mecanismo avançado.

O mecanismo simples é a única opção nas janelas do sistema. Os apps podem usar qualquer um dos mecanismos.

Mecanismo simples

(Android 11 QPR3, Android 11 Car, Android 12)
O app precisa 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á em foco. No modo DM, as rotações executam ACTION_SCROLL_FORWARD ou ACTION_SCROLL_BACKWARD e saem 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 dica visual de que o usuário está no modo de DM, faça com que a visualização apareça de forma diferente quando selecionada. Por exemplo, mude o plano de fundo quando android:state_selected for true.

Mecanismo avançado

O app determina quando RotaryService entra e sai do modo DM. Para uma experiência consistente do usuário, pressionar o botão central com uma visualização de DM em foco deve entrar no modo DM e o botão "Voltar" deve sair do modo DM. Se o botão Centralizar e/ou o toque não forem usados, eles podem ser maneiras alternativas de sair do modo DM. Para apps como o Maps, um botão para representar a DM pode ser usado para entrar no modo DM.

Para oferecer suporte ao modo avançado de DM, uma visualização:

  1. (Android 11 QPR3, Android 11 Car, Android 12) É PRECISO detectar um evento KEYCODE_DPAD_CENTER para entrar no modo DM e detectar um evento KEYCODE_BACK para sair do modo DM, chamando DirectManipulationHelper.enableDirectManipulationMode() em cada caso. Para detectar esses eventos, siga um destes procedimentos:
    • Registre um OnKeyListener.
    • ou
    • Estenda a visualização e substitua o método dispatchKeyEvent() dela.
  2. DEVE detectar eventos de alerta (KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT ou KEYCODE_DPAD_RIGHT) se a visualização precisar processar alertas.
  3. DEVE detectar MotionEvents e receber a contagem de rotações em AXIS_SCROLL se a visualização quiser processar a rotação. Há várias maneiras de fazer isso:
    1. Registre um OnGenericMotionListener.
    2. Estenda a visualização e substitua o método dispatchTouchEvent() dela.
  4. Para evitar ficar preso no modo DM, é PRECISO sair do modo DM quando o fragmento ou a atividade a que a visualização pertence não é interativa.
  5. DEVE fornecer uma indicação visual para indicar que a visualização está no modo DM.

Confira abaixo um exemplo de visualização personalizada que usa o modo DM para mover e aplicar zoom em um 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(); }

Mais exemplos podem ser encontrados no projeto RotaryPlayground.

ActivityView

Ao usar um ActivityView:

  • O ActivityView não pode ser focado.
  • (Android 11 QPR3, Android 11 Car, descontinuado no Android 11)
    O conteúdo da ActivityView PRECISA conter uma FocusParkingView como a primeira visualização com foco, e o atributo app:shouldRestoreFocus PRECISA ser false.
  • O conteúdo do ActivityView não pode ter visualizações android:focusByDefault.

Para o usuário, as ActivityViews não devem ter efeito na navegação, exceto que as áreas de foco não podem abranger ActivityViews. Em outras palavras, não é possível ter uma única área de foco com conteúdo dentro e fora de um ActivityView. Se você não adicionar nenhuma FocusArea ao ActivityView, a raiz da hierarquia de visualizaçã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 clicados. Alguns botões funcionam quando são pressionados. Por exemplo, os botões de avanço rápido e retrocesso normalmente funcionam quando são pressionados. Para que esses botões ofereçam suporte a rotary, ouça KEYCODE_DPAD_CENTER KeyEvents da seguinte maneira:

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 realiza uma ação (como retroceder) e se programa para ser executado após um atraso.

Modo de toque

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

  • Rotativa → toque. Quando o usuário toca na tela, o destaque de foco desaparece.
  • Toque em → rotativo. Quando o usuário empurra, gira ou pressiona o botão Central, o destaque de foco aparece.

Os botões "Voltar" e "Início" não afetam o modo de entrada.

O Rotary usa o conceito existente do modo de toque do Android. É possível usar View.isInTouchMode() para determinar qual modo de entrada o usuário está usando. Você pode usar OnTouchModeChangeListener para detectar mudanças. Embora isso possa ser usado para personalizar a interface do usuário para o modo de entrada atual, evite mudanças importantes, pois elas podem ser perturbadoras.

Solução de problemas

Em um app projetado para toque, é comum ter visualizações focadas aninhadas. Por exemplo, pode haver um FrameLayout em torno de um ImageButton, ambos com foco. Isso não prejudica o toque, mas pode resultar em uma experiência do usuário ruim para o seletor giratório, porque o usuário precisa girar o controle 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 visualização externa ou interna focada, mas não ambas.

Se um botão ou chave perder o foco ao ser pressionado pelo controle rotativo, uma destas condições poderá ser aplicada:

  • O botão ou chave está sendo desativado (brevemente ou indefinidamente) porque está sendo pressionado. Em ambos os casos, há duas maneiras de resolver isso:
    • Deixe o estado android:enabled como true e use um estado personalizado para deixar o botão ou a chave cinza, conforme descrito em Estado personalizado.
    • Use um contêiner para cercar o botão ou o interruptor e tornar o contêiner focalizável em vez do botão ou do interruptor. O listener de clique precisa estar no contêiner.
  • O botão ou a chave está sendo substituído. Por exemplo, a ação realizada quando o botão é pressionado ou o interruptor é acionado pode acionar uma atualização das ações disponíveis, fazendo com que novos botões substituam os botões atuais. Há duas maneiras de resolver isso:
    • Em vez de criar um novo botão ou interruptor, defina o ícone e/ou o texto do item atual.
    • Como acima, adicione um contêiner com foco ao redor do botão ou do interruptor.

RotaryPlayground

RotaryPlayground é um app de referência para seletor giratório. Use-o para aprender a integrar recursos rotativos aos seus apps. RotaryPlayground está incluído em builds de emulador e em builds para dispositivos que executam o Android Automotive OS (AAOS).

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

O app RotaryPlayground mostra as seguintes guias à esquerda:

  • Cards. Teste a navegação pelas áreas de foco, pulando elementos não focados e entradas de texto.
  • Manipulação direta. Teste widgets que oferecem suporte ao modo de manipulação direta simples e avançada. Essa guia é específica para manipulação direta na janela do app.
  • Manipulação da interface do Sys. Teste widgets que oferecem suporte à manipulação direta em janelas do sistema em que apenas o modo de manipulação direta simples é aceito.
  • Grade. Teste a navegação por seletor giratório com rolagem.
  • Notificação. Teste a ativação e desativação das notificações de informações básicas.
  • Role a tela. Teste a rolagem com uma combinação de conteúdo com e sem foco.
  • WebView. Teste a navegação pelos links em um WebView.
  • FocusArea personalizado. Teste a personalização FocusArea:
    • Wrap-around.
    • android:focusedByDefault e app:defaultFocus
    • .
    • Alvos de estímulo explícito.
    • Atalhos de lembretes.
    • FocusArea sem visualizações focalizáveis.