アプリを開発する

以下は、アプリ デベロッパー向けの資料です。

アプリがロータリーをサポートするには、次のことを行う必要があります。

  1. それぞれのアクティビティ レイアウトに FocusParkingView を配置する。
  2. フォーカス可能な(またはフォーカス可能でない)ビューを確認する。
  3. FocusArea を使用して、FocusParkingView を除くすべてのフォーカス可能なビューをラップアラウンドする。

これらのタスクは、ロータリー対応アプリを開発するための環境を設定した後に行います。以下では、それぞれのタスクについて詳しく説明します。

ロータリー コントローラを設定する

ロータリー対応アプリの開発を開始するには、ロータリー コントローラまたは代替のツールが必要です。代替のツールには、次の選択肢があります。

エミュレータ

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

aosp_car_x86_64-userdebug を使用することもできます。

エミュレートしたロータリー コントローラにアクセスするには:

  1. ツールバーの下部にあるその他アイコンをタップします。

    エミュレートされたロータリー コントローラにアクセスする
    図 1. エミュレートされたロータリー コントローラにアクセスする
  2. 拡張コントロール ウィンドウで [車両のロータリー] を選択します。

    車両のロータリーを選択
    図 2. 車両のロータリーを選択

USB キーボード

  • Android Automotive OS(AAOS)を搭載しているデバイスに USB キーボードを接続します(画面キーボードが表示されなくなる場合があります)。
  • userdebug ビルドまたは eng ビルドを使用します。
  • キーイベントのフィルタリングを有効にします。
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • 以下の表で、各アクションに対応するキーを探します。
    キー ロータリーのアクション
    Q 反時計回りに回転
    E 時計回りに回転
    A 左に移動
    D 右に移動
    W 上に移動
    S 下に移動
    F またはカンマ 中央ボタン
    R または Esc 戻るボタン

ADB コマンド

car_service コマンドを使用して、ロータリー入力イベントを挿入できます。以下のコマンドは、Android Automotive OS(AAOS)を搭載しているデバイスまたはエミュレータで実行できます。

car_service コマンド ロータリーの入力
adb shell cmd car_service inject-rotary 反時計回りに回転
adb shell cmd car_service inject-rotary -c true 時計回りに回転
adb shell cmd car_service inject-rotary -dt 100 50 反時計回りに複数回回転(100 ミリ秒前と 50 ミリ秒前)
adb shell cmd car_service inject-key 282 左に移動
adb shell cmd car_service inject-key 283 右に移動
adb shell cmd car_service inject-key 280 上に移動
adb shell cmd car_service inject-key 281 下に移動
adb shell cmd car_service inject-key 23 中央ボタンをクリック
adb shell input keyevent inject-key 4 戻るボタンをクリック

OEM ロータリー コントローラ

ロータリー コントローラ ハードウェアを用意している場合は、これが最も現実的な選択肢です。高速ローテーションをテストする場合に、特に便利です。

FocusParkingView

FocusParkingView は、Car UI ライブラリ(car-ui-library)の透過ビューです。RotaryService は、これを使用してロータリー コントローラのナビゲーションをサポートします。 FocusParkingView は、レイアウト内の最初のフォーカス可能なビューである必要があります。これはすべての FocusArea の外部に配置する必要があります。各ウィンドウに 1 つの FocusParkingView が必要です。FocusParkingView を含む car-ui-library ベース レイアウトをすでに使用している場合、別の FocusParkingView を追加する必要はありません。RotaryPlayground 内の FocusParkingView の例を以下に示します。

<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>

FocusParkingView が必要な理由は次のとおりです。

  1. Android では、別のウィンドウにフォーカスが設定されても、フォーカスは自動的にクリアされません。前のウィンドウのフォーカスを削除しようとしても、Android はそのウィンドウのビューにリフォーカスします。これにより、2 つのウィンドウが同時にフォーカスされることになります。この問題は、各ウィンドウに FocusParkingView を追加することで解決できます。このビューは透明で、デフォルトでフォーカス ハイライトが無効になっているため、フォーカスされているかどうかにかかわらずユーザーには表示されません。RotaryService が存在すると、それにフォーカスが移動し、パーキングされるため、フォーカスのハイライトを削除できるようになります。
  2. 現在のウィンドウに FocusArea が 1 つしかない場合、FocusArea 内でコントローラを回すと、RotaryService はフォーカスを右側のビューから左側のビューに移動します(またはその逆)。このビューを各ウィンドウに追加すると、問題を解決できます。RotaryService は、フォーカス ターゲットが FocusParkingView であると判断した場合、ラップアラウンドが間もなく発生するものと判断し、その時点でフォーカスを移動しないことによりラップアラウンドを回避できます。
  3. ロータリー コントロールがアプリを起動すると、Android は最初のフォーカス可能なビュー(必ず FocusParkingView)にフォーカスします。FocusParkingView は、フォーカスする最適なビューを判断し、フォーカスを適用します。

フォーカス可能なビュー

RotaryService は、Android フレームワークの従来のビュー フォーカスのコンセプトをベースに構築されています。これはスマートフォンに物理キーボードと D-pad が搭載されていた時代のコンセプトです。 既存の android:nextFocusForward 属性はロータリー用に再利用されていますが(FocusArea のカスタマイズを参照)、android:nextFocusLeftandroid:nextFocusRightandroid:nextFocusUpandroid:nextFocusDown は再利用されていません。

RotaryService は、フォーカス可能なビューにのみフォーカスします。Button などの一部のビューは、通常はフォーカス可能です。他のビュー(TextViewViewGroup など)は、通常はフォーカス可能ではありません。クリック可能なビューは自動的にフォーカス可能となり、クリック リスナーがあるビューは自動的にクリック可能となります。この自動的なロジックで目的とするフォーカス可能性を実現できているのであれば、ビューのフォーカス可能性を明示的に設定する必要はありません。自動的なロジックで目的とするフォーカス可能性を実現できていない場合は、android:focusable 属性を true または false に設定するか、プログラムで View.setFocusable(boolean) を使用してビューのフォーカス可能性を設定します。RotaryService がフォーカスするには、ビューが次の要件を満たしている必要があります。

  • フォーカス可能である
  • 有効である
  • 表示されている
  • 幅と高さにゼロ以外の値が指定されている

ビューがこれらの要件のいずれかを満たしていない場合(フォーカス可能だが無効なボタンなど)、ユーザーがロータリー コントロールを使用してそれにフォーカスすることはできません。無効になっているビューにフォーカスする場合は、android:state_enabled ではなく、カスタム ステータスを使用して、無効とみなすように Android に指示せずにビューの表示方法を制御することを検討してください。タップしたときに、ビューが無効になっている理由をアプリからユーザーに通知できます。次のセクションでは、その方法について説明します。

カスタム ステータス

カスタム ステータスを追加する方法:

  1. ビューにカスタム属性を追加します。たとえば state_rotary_enabled カスタム ステータスを CustomView ビュークラスに追加する場合は、次のようにします。
    <declare-styleable name="CustomView">
        <attr name="state_rotary_enabled" format="boolean" />
    </declare-styleable>
    
  2. このステータスを追跡するには、ビューにインスタンス変数とアクセサ メソッドを追加します。
    private boolean mRotaryEnabled;
    public boolean getRotaryEnabled() { return mRotaryEnabled; }
    public void setRotaryEnabled(boolean rotaryEnabled) {
        mRotaryEnabled = rotaryEnabled;
    }
    
  3. 次のようにして、ビューの作成時に属性の値を読み取ります。
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
    mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
    
  4. ビュークラスで onCreateDrawableState() メソッドをオーバーライドし、必要な場合はカスタム ステータスを追加します。次に例を示します。
    @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. ビューのクリック ハンドラがそのステータスに応じて異なる動作をするようにします。たとえば、クリック ハンドラが何もしない場合もあれば、mRotaryEnabledfalse の場合にトーストをポップアップする場合もあるようにします。
  6. ボタンを無効の状態で表示するには、ビューの背景ドローアブルで、android:state_enabled ではなく app:state_rotary_enabled を使用します。 まだない場合は、以下を追加する必要があります。
    xmlns:app="http://schemas.android.com/apk/res-auto"
    
  7. ビューがいずれかのレイアウトで無効になる場合は、android:enabled="false"app:state_rotary_enabled="false" に置き換え、上記のように app 名前空間を追加します。
  8. ビューをプログラムによって無効にする場合は、setEnabled() の呼び出しを setRotaryEnabled() の呼び出しに置き換えます。

FocusArea

FocusAreas を使用してフォーカス可能なビューをブロックに分割することで、ナビゲーションを容易にし、他のアプリとの一貫性を持たせることができます。たとえば、アプリにツールバーがある場合、ツールバーはアプリの他の部分とは別の FocusArea にある必要があります。タブバーなどのナビゲーション要素も、アプリの他の部分から分離する必要があります。通常、大規模なリストには独自の FocusArea が必要です。これがない場合、ユーザーは一部のビューにアクセスするためにリスト全体を順番にローテーションする必要があります。

FocusArea は、car-ui-library の LinearLayout のサブクラスです。 この機能を有効にした場合、FocusArea の子のいずれかがフォーカスされると、ハイライトが描画されます。詳しくは、フォーカス ハイライトのカスタマイズをご覧ください。

レイアウト ファイル内にナビゲーション ブロックを作成する際に、そのブロックのコンテナとして LinearLayout の使用を考えている場合は、代わりに FocusArea を使用してください。それ以外の場合は、FocusArea でブロックをラップします。

FocusArea を別の FocusArea にネストしないでください。 この操作を行った場合、未定義のナビゲーション動作の原因となります。フォーカス可能なすべてのビューが FocusArea 内にネストされていることを確認します。

RotaryPlaygroundFocusArea の例を次に示します。

<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 は次のように機能します。

  1. ローテーション アクションと移動アクションを処理する場合、RotaryService はビュー階層内で FocusArea のインスタンスを探します。
  2. RotaryService はローテーション イベントを受け取ると、同じ FocusArea でフォーカスを取得可能な別のビューにフォーカスを移動させます。
  3. RotaryService は移動イベントを受け取ると、別の(通常は隣の)FocusArea でフォーカスを取得可能な別のビューにフォーカスを移動します。

レイアウトに FocusAreas を含めない場合、ルートビューは暗黙的なフォーカス領域として扱われます。この場合、ユーザーはアプリ内でローテーションすることができません。代わりに、フォーカス可能なすべてのビューが順に表示されます。これはダイアログに適している場合があります。

FocusArea のカスタマイズ

ロータリー ナビゲーションをカスタマイズするには、2 つの標準のビュー属性を使用できます。

  • android:nextFocusForward を使用すると、アプリ デベロッパーはフォーカス エリア内でローテーション順序を指定できます。これはキーボード ナビゲーションのタブの順序を制御する場合と同じ属性です。この属性は、ループの作成には使用しないでください。 ループを作成するには、代わりに app:wrapAround(下記参照)を使用してください。
  • アプリ デベロッパーは、android:focusedByDefault を使用して、ウィンドウ内でデフォルトのフォーカス ビューを指定できます。この属性と app:defaultFocus(下記参照)は同じ FocusArea 内で使用しないでください。

FocusArea では、ロータリー ナビゲーションをカスタマイズする属性も定義します。 暗黙的なフォーカス領域は、次の属性ではカスタマイズできません。

  1. (Android 11 QPR3、Android 11 Car、Android 12
    app:defaultFocus を使用することで、フォーカス可能な子孫ビューの ID を指定できます。このビューは、ユーザーがこの FocusArea に移動した時点でフォーカスされます。
  2. (Android 11 QPR3、Android 11 Car、Android 12
    app:defaultFocusOverridesHistorytrue に設定することで、この FocusArea の別のビューがフォーカスされていたことを示す履歴があっても、上記で指定したビューがフォーカスされるようにすることができます。
  3. (Android 12
    app:nudgeLeftShortcutapp:nudgeRightShortcutapp:nudgeUpShortcutapp:nudgeDownShortcut を使用することで、フォーカス可能な子孫ビューの ID を指定します。このビューには、ユーザーが特定の方向に移動した時点でフォーカスします。詳しくは、以下の移動のショートカットをご覧ください。

    (Android 11 QPR3、Android 11 Car。Android 12 ではサポート終了app:nudgeShortcutapp:nudgeShortcutDirection は 1 つの移動ショートカットのみをサポートします。

  4. (Android 11 QPR3、Android 11 Car、Android 12
    この FocusArea でラップアラウンドするためにローテーションを許可する場合、app:wrapAroundtrue に設定します。通常は、ビューを円または楕円に配置する場合に使用します。
  5. (Android 11 QPR3、Android 11 Car、Android 12
    この FocusArea でハイライトのパディングを調整する場合、app:highlightPaddingStartapp:highlightPaddingEndapp:highlightPaddingTopapp:highlightPaddingBottomapp:highlightPaddingHorizontalapp:highlightPaddingVertical を使用します。
  6. (Android 11 QPR3、Android 11 Car、Android 12
    この FocusArea の認識済みの境界を調整して移動のターゲットを探すには、app:startBoundOffsetapp:endBoundOffsetapp:topBoundOffsetapp:bottomBoundOffsetapp:horizontalBoundOffsetapp:verticalBoundOffset を使用します。
  7. (Android 11 QPR3、Android 11 Car、Android 12
    特定の方向に隣接する FocusArea(またはエリア)の ID を明示的に指定する場合は、app:nudgeLeftapp:nudgeRightapp:nudgeUpapp:nudgeDown を使用します。これは、デフォルトで使用されているジオメトリ検索によって目的のターゲットが見つからない場合に使用します。

通常、移動では FocusAreas 間を移動します。しかし、移動ショートカットを使用すると最初に FocusArea 内で移動することがあり、その場合ユーザーは 2 回移動して次の FocusArea に移動することが必要となる場合があります。移動ショートカットは、次の例に示すように、FocusArea に長いリストがあり、その後にフローティング操作ボタンがある場合に便利です。

移動ショートカット
図 3. 移動ショートカット

移動ショートカットがない場合、ユーザーはリスト全体でローテーションして FAB にアクセスすることになります。

フォーカス ハイライトのカスタマイズ

前述のように、RotaryService は、Android フレームワークの既存のビュー フォーカスのコンセプトに基づいています。ユーザーがローテーションと移動を行うと、RotaryService はフォーカス間を移動し、1 つのビューにフォーカスを合わせ、別のビューのフォーカスを解除します。Android では、ビューがフォーカスされている場合、次のようになります。

  • ビューで独自のフォーカス ハイライトが指定されている場合、Android はビューのフォーカス ハイライトを描画します。
  • ビューでフォーカス ハイライトが指定されておらず、デフォルトのフォーカス ハイライトが無効になっていない場合、Android はビューのデフォルトのフォーカス ハイライトを描画します。

通常、タップ向けに設計されたアプリでは、適切なフォーカス ハイライトが指定されません。

デフォルトのフォーカス ハイライトは Android フレームワークによって提供されます。また、OEM によってオーバーライドされることもあります。アプリ デベロッパーは、使用しているテーマが Theme.DeviceDefault から派生したものであるときにデフォルトのフォーカス ハイライトを使用できます。

ユーザー エクスペリエンスが安定するように、可能な限りデフォルトのフォーカス ハイライト表示を使用してください。 カスタム シェイプの(円形やピル型などの)フォーカス ハイライトが必要な場合、または Theme.DeviceDefault から派生していないテーマを使用している場合は、car-ui-library リソースを使用し、各ビューに独自のフォーカス ハイライトを指定します。

ビューにカスタム フォーカス ハイライトを指定するには、ビューのバックグラウンドまたはフォアグラウンド ドローアブルを、ビューがフォーカスされているときに別のドローアブルに変更します。通常はバックグラウンドを変更します。次のドローアブルを正方形ビューのバックグラウンドとして使用すると、丸いフォーカスのハイライト表示が生成されます。

<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)上記のサンプルの太字のリソース参照は、car-ui-library で定義されたリソースを指定しています。OEM は、これらをオーバライドすることで、自身が指定したデフォルトのフォーカス ハイライトと一貫性を持たせることができます。これにより、カスタムのフォーカス ハイライトを持つビューとデフォルトのフォーカス ハイライトを持つビューの間でユーザーが移動を行っても、フォーカスのハイライトの色やストローク幅などが変化しなくなります。最後の項目は、タップに使用されるリップルです。太字のリソースに使用されるデフォルト値は次のように表示されます。

太字のリソースのデフォルト値
図 4. 太字のリソースのデフォルト値

また、ボタンに単色の背景色が割り当てられているときに、ユーザーの注意を引くためにカスタム フォーカス ハイライトが呼び出されると、次の例のようになります。この場合、フォーカス ハイライトが見づらくなります。この場合は、セカンダリ カラーを使用してカスタム フォーカス ハイライトを指定します。

無地の背景色
  • (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

例:

フォーカスされていて、押されていない フォーカスされていて、押されている
フォーカスされていて、押されていない フォーカスされていて、押されている

ロータリーのスクロール

アプリが RecyclerView を使用している場合、代わりに CarUiRecyclerView を使用する必要があります。OEM のカスタマイズはすべての CarUiRecyclerView に適用されるため、これにより UI に他の UI との一貫性を持たせることができます。

リスト内の要素がすべてフォーカス可能である場合、他に何もする必要はありません。ロータリー ナビゲーションでは、リスト内の要素間でフォーカスが移動してリストがスクロールされ、新しくフォーカスされた要素が表示されます。

(Android 11 QPR3、Android 11 Car、Android 12
フォーカス可能な要素とフォーカス不可能な要素が混在している場合、またはすべての要素がフォーカス不可能である場合、ロータリーのスクロールを有効にすることで、ユーザーはロータリー コントローラを使用して、フォーカス不可能な項目をスキップせずにリストを段階的にスクロールできます。ロータリーのスクロールを有効にするには、app:rotaryScrollEnabled 属性を true に設定します。

(Android 11 QPR3、Android 11 Car、Android 12
CarUiUtilssetRotaryScrollEnabled() を使用すると、avCarUiRecyclerView などのスクロール可能な任意のビューでロータリーのスクロールを有効にできます。これを行うには、次の操作を行う必要があります。

  • スクロール可能なビューをフォーカス可能にして、フォーカス可能な子孫ビューがどれも表示されていないときにこのビューがフォーカスされるようにします。
  • setDefaultFocusHighlightEnabled(false) を呼び出して、スクロール可能なビューでデフォルトのフォーカス ハイライト表示を無効にして、スクロール可能なビューがフォーカスされた状態で表示されないようにします。
  • setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS) を呼び出して、スクロール可能なビューが子孫より前にフォーカスされるようにします。
  • SOURCE_ROTARY_ENCODER と、AXIS_VSCROLL または AXIS_HSCROLL のいずれかを使用して MotionEvents をリッスンし、スクロールする距離と方向(符号で表します)を指定します。

CarUiRecyclerView でロータリーのスクロールが有効で、ユーザーがフォーカス可能なビューがない領域にローテーションすると、スクロールバーがフォーカスされている場合と同様に、スクロールバーがグレーから青色に変わります。必要であれば、同様の効果を実装できます。

MotionEvents は、マウスのスクロール ホイールで生成されるイベントと同様ですが、ソースは異なります。

直接操作モード

通常、移動とローテーションではユーザー インターフェース内の移動が発生し、中央ボタンの押下ではアクションが実行されます。ただし、常にそうであるとは限りません。たとえば、アラームの音量を調整する場合は、ロータリー コントローラを使用して音量スライダーに移動し、中央のボタンを押してからコントローラをローテーションさせ、アラームの音量を調整します。戻るボタンを押すとナビゲーションに戻ります。これは、直接操作(DM)モードと呼ばれます。このモードでは、ロータリー コントローラはナビゲーションにではなく、ビューを直接操作するために使用されます。

DM を実装する方法は 2 通りあります。ローテーションの処理のみが必要で、操作対象のビューが ACTION_SCROLL_FORWARDACTION_SCROLL_BACKWARD AccessibilityEvent に適切に対応している場合は、シンプルなメカニズムを使用します。それ以外の場合は、高度なメカニズムを使用します。

システム ウィンドウではシンプルなメカニズムが唯一のオプションです。アプリではどちらのメカニズムも使用できます。

シンプルなメカニズム

(Android 11 QPR3、Android 11 Car、Android 12
アプリで DirectManipulationHelper.setSupportsRotateDirectly(View view, boolean enable) を呼び出す必要があります。 RotaryService は、ユーザーが DM モードであることを認識します。また、ビューがフォーカスされているときにユーザーが中央のボタンを押すと DM モードに移行します。DM モードの場合、ローテーション操作により ACTION_SCROLL_FORWARD または ACTION_SCROLL_BACKWARD が実行され、ユーザーが戻るボタンを押すと DM モードが終了します。シンプルなメカニズムでは、DM モードの開始時と終了時に、ビューの選択されている状態を切り替えます。

ユーザーが DM モードになっていることを視覚的に示すには、ビューを選択したときに表示が変わるようにします。たとえば、android:state_selectedtrue のときに背景を変更します。

高度なメカニズム

RotaryService が DM モードを開始する時点と終了する時点は、アプリが決定します。一貫したユーザー エクスペリエンスを確保するため、DM ビューをフォーカスした状態で中央ボタンを押すと DM モードに移行し、戻るボタンを押すと DM モードが終了するようにします。中央のボタンや移動を使用しない場合は、別の方法で DM モードを終了できます。マップなどのアプリの場合、DM を表すボタンを使用して DM モードに移行できるようにします。

高度な DM モードをサポートするには、ビューが以下の条件を満たす必要があります。

  1. (Android 11 QPR3、Android 11 Car、Android 12KEYCODE_DPAD_CENTER イベントをリッスンして DM モードに移行し、KEYCODE_BACK イベントをリッスンして DM を終了できる必要があります。いずれの場合も DirectManipulationHelper.enableDirectManipulationMode() を呼び出します。 これらのイベントをリッスンするには、次のいずれかを行います。
    • OnKeyListener を登録する。
    • または
    • ビューを拡張し、dispatchKeyEvent() メソッドをオーバーライドする。
  2. ビューが移動を処理する場合は、移動イベント(KEYCODE_DPAD_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFTKEYCODE_DPAD_RIGHT)をリッスンする必要があります。
  3. ビューがローテーションを処理する場合は、MotionEvent をリッスンし、AXIS_SCROLL でローテーション数を取得する必要があります。これを行うには、いくつかの方法があります。
    1. OnGenericMotionListener を登録する。
    2. ビューを拡張し、dispatchTouchEvent() メソッドをオーバーライドする。
  4. DM モードのままになることを避けるため、ビューが属するフラグメントまたはアクティビティがインタラクティブでないときは DM モードを終了しなければなりません。
  5. ビューが DM モードであることを視覚的に示す必要があります。

DM モードでマップをパンおよびズームするカスタムビューのサンプルを以下に示します。

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

その他の例については、RotaryPlayground プロジェクトをご覧ください。

ActivityView

ActivityView を使用する場合、以下の条件を満たす必要があります。

  • ActivityView はフォーカス可能にしないでください。
  • (Android 11 QPR3、Android 11 Car。Android 11 ではサポート終了
    ActivityView のコンテンツには、次のように FocusParkingView を最初のフォーカス可能なビューとして含める必要があります。またその app:shouldRestoreFocus 属性は false でなければなりません。
  • ActivityView のコンテンツに android:focusByDefault ビューを含めることはできません。

フォーカス領域が ActivityView にまたがることができない場合を除き、ユーザーから見て ActivityView がナビゲーションに影響しないようにする必要があります。つまり、ActivityView の内側と外側の両方のコンテンツを 1 つのフォーカス領域に入れることはできません。ActivityView に FocusAreas を追加しない場合、ActivityView のビュー階層のルートは暗黙的なフォーカス領域とみなされます。

長押ししたときに動作するボタン

ほとんどのボタンは、クリック時になんらかのアクションが発生します。一部のボタンは、長押しすると動作します。 たとえば、早送りボタンと巻き戻しボタンは、通常はユーザーが長押しすると機能します。このようなボタンでローテーションをサポートするには、次のように KEYCODE_DPAD_CENTER KeyEvents をリッスンします。

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

この例では、mRunnable がアクション(巻き戻しなど)を実行しますが、しばらく遅れてから実行するされるようにスケジューリングします。

タップモード

ユーザーが車内でロータリー コントローラを使用してヘッドユニットを操作するには、ロータリー コントローラを使用する方法と、画面をタップする方法の 2 通りがあります。ロータリー コントローラを使用している場合、フォーカス可能なビューの 1 つがハイライト表示されます。画面にタップしても、フォーカス ハイライトは表示されません。これらの入力モードはいつでも切り替えることができます。

  • ロータリー → タップ。ユーザーが画面にタップすると、フォーカス ハイライトが消えます。
  • タップ → ロータリー。ユーザーが移動またはローテーションを行うか、中央ボタンを押すと、フォーカス ハイライトが表示されます。

戻るボタンとホームボタンは入力モードには影響しません。

ローテーションは、Android に従来からあったタップモードのコンセプトを利用しています。View.isInTouchMode() を使用すると、ユーザーが使用している入力モードを特定できます。OnTouchModeChangeListener を使用すると、変更をリッスンできます。これを使用すると、現在の入力モードに関するユーザー インターフェースをカスタマイズできますが、混乱を招く恐れがあるので、大きな変更は行わないでください。

トラブルシューティング

タップ用に設計されたアプリでは、フォーカス可能なビューがネストされていることは珍しくありません。たとえば、ImageButton の周囲に FrameLayout を配置し、どちらもフォーカス可能にすることができます。これがタップに影響することはありませんが、次のインタラクティブなビューに移動するにはコントローラを 2 回回転させる必要があるため、ロータリーのユーザー エクスペリエンスが低下する可能性があります。優れたユーザー エクスペリエンスを実現するには、両方ではなく、外側のビューと内側のビューのどちらかをフォーカス可能にすることをおすすめします。

ロータリー コントローラからボタンやスイッチが押されたときにフォーカスが失われる場合は、次のいずれかの状況に該当する可能性があります。

  • ボタンが押されたことにより、ボタンまたはスイッチが(短時間または無期限に)無効になっている。いずれの場合も、この問題には 2 つの方法で対処できます。
    • カスタム ステータスで説明されているように、android:enabled のステータスを true のままにして、カスタム ステータスを使用してボタンをグレー表示にします。
    • コンテナを使用してボタンやスイッチを囲い、ボタンやスイッチの代わりにこのコンテナをフォーカス可能にします(クリック リスナーはコンテナ上にある必要があります)
  • ボタンまたはスイッチが置き換わっている最中である。たとえば、ボタンが押されたときや、スイッチが切り替えられたときに実行されるアクションによって、使用可能なアクションの更新がトリガーされ、既存のボタンが新しいボタンに置き換わることがあります。これに対処するには 2 つの方法があります。
    • 新しいボタンやスイッチを作成する代わりに、既存のボタンやスイッチのアイコンやテキストを設定します。
    • 前述のように、ボタンまたはスイッチの周囲にフォーカス可能なコンテナを追加します。

RotaryPlayground

RotaryPlayground は、ロータリーのリファレンス アプリです。これを使用して、ロータリー機能をアプリに統合する方法を学習できます。RotaryPlayground は、エミュレータのビルドと、Android Automotive OS(AAOS)を搭載しているデバイスのビルドに含まれています。

  • RotaryPlayground リポジトリ: packages/apps/Car/tests/RotaryPlayground/
  • バージョン: Android 11 QPR3、Android 11 Car、Android 12

RotaryPlayground アプリの左側に次のタブが表示されます。

  • カード: フォーカス領域間の移動や、フォーカス可能でない領域やテキスト入力のスキップをテストできます。
  • 直接操作: シンプルな直接操作モードまたは高度な直接操作モードをサポートするウィジェットをテストできます。このタブは、アプリ ウィンドウ内で直接操作を行うときに使用します。
  • システム UI 操作: シンプルな直接操作モードのみがサポートされているシステム ウィンドウ内の、直接操作をサポートするウィジェットをテストできます。
  • グリッド: スクロールによる Z パターン ロータリー ナビゲーションをテストできます。
  • 通知: ヘッドアップ通知の開閉をテストできます。
  • スクロール: フォーカス可能なコンテンツとフォーカス不可能なコンテンツを組み合わせてスクロールをテストできます。
  • WebView: WebView 内のリンクの移動をテストできます。
  • カスタム FocusArea: FocusArea のカスタマイズをテストできます。
    • ラップアラウンド
    • android:focusedByDefaultapp:defaultFocus
    • 明示的な移動ターゲット
    • 移動のショートカット
    • フォーカス可能なビューがない FocusArea