以下は、アプリ デベロッパー向けの資料です。
アプリがロータリーをサポートするには、次のことを行う必要があります。
- それぞれのアクティビティ レイアウトに
FocusParkingView
を配置する。 - フォーカス可能な(またはフォーカス可能でない)ビューを確認する。
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
を使用することもできます。
エミュレートしたロータリー コントローラにアクセスするには:
- ツールバーの下部にあるその他アイコンをタップします。
- 拡張コントロール ウィンドウで [車両のロータリー] を選択します。
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
が必要な理由は次のとおりです。
- Android では、別のウィンドウにフォーカスが設定されても、フォーカスは自動的にクリアされません。前のウィンドウのフォーカスを削除しようとしても、Android はそのウィンドウのビューにリフォーカスします。これにより、2 つのウィンドウが同時にフォーカスされることになります。この問題は、各ウィンドウに
FocusParkingView
を追加することで解決できます。このビューは透明で、デフォルトでフォーカス ハイライトが無効になっているため、フォーカスされているかどうかにかかわらずユーザーには表示されません。RotaryService
が存在すると、それにフォーカスが移動し、パーキングされるため、フォーカスのハイライトを削除できるようになります。 - 現在のウィンドウに
FocusArea
が 1 つしかない場合、FocusArea
内でコントローラを回すと、RotaryService
はフォーカスを右側のビューから左側のビューに移動します(またはその逆)。このビューを各ウィンドウに追加すると、問題を解決できます。RotaryService
は、フォーカス ターゲットがFocusParkingView
であると判断した場合、ラップアラウンドが間もなく発生するものと判断し、その時点でフォーカスを移動しないことによりラップアラウンドを回避できます。 - ロータリー コントロールがアプリを起動すると、Android は最初のフォーカス可能なビュー(必ず
FocusParkingView
)にフォーカスします。FocusParkingView
は、フォーカスする最適なビューを判断し、フォーカスを適用します。
フォーカス可能なビュー
RotaryService
は、Android フレームワークの従来のビュー フォーカスのコンセプトをベースに構築されています。これはスマートフォンに物理キーボードと D-pad が搭載されていた時代のコンセプトです。
既存の android:nextFocusForward
属性はロータリー用に再利用されていますが(FocusArea のカスタマイズを参照)、android:nextFocusLeft
、android:nextFocusRight
、android:nextFocusUp
、android:nextFocusDown
は再利用されていません。
RotaryService
は、フォーカス可能なビューにのみフォーカスします。Button
などの一部のビューは、通常はフォーカス可能です。他のビュー(TextView
や ViewGroup
など)は、通常はフォーカス可能ではありません。クリック可能なビューは自動的にフォーカス可能となり、クリック リスナーがあるビューは自動的にクリック可能となります。この自動的なロジックで目的とするフォーカス可能性を実現できているのであれば、ビューのフォーカス可能性を明示的に設定する必要はありません。自動的なロジックで目的とするフォーカス可能性を実現できていない場合は、android:focusable
属性を true
または false
に設定するか、プログラムで View.setFocusable(boolean)
を使用してビューのフォーカス可能性を設定します。RotaryService
がフォーカスするには、ビューが次の要件を満たしている必要があります。
- フォーカス可能である
- 有効である
- 表示されている
- 幅と高さにゼロ以外の値が指定されている
ビューがこれらの要件のいずれかを満たしていない場合(フォーカス可能だが無効なボタンなど)、ユーザーがロータリー コントロールを使用してそれにフォーカスすることはできません。無効になっているビューにフォーカスする場合は、android:state_enabled
ではなく、カスタム ステータスを使用して、無効とみなすように Android に指示せずにビューの表示方法を制御することを検討してください。タップしたときに、ビューが無効になっている理由をアプリからユーザーに通知できます。次のセクションでは、その方法について説明します。
カスタム ステータス
カスタム ステータスを追加する方法:
- ビューにカスタム属性を追加します。たとえば
state_rotary_enabled
カスタム ステータスをCustomView
ビュークラスに追加する場合は、次のようにします。<declare-styleable name="CustomView"> <attr name="state_rotary_enabled" format="boolean" /> </declare-styleable>
- このステータスを追跡するには、ビューにインスタンス変数とアクセサ メソッドを追加します。
private boolean mRotaryEnabled; public boolean getRotaryEnabled() { return mRotaryEnabled; } public void setRotaryEnabled(boolean rotaryEnabled) { mRotaryEnabled = rotaryEnabled; }
- 次のようにして、ビューの作成時に属性の値を読み取ります。
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomView); mRotaryEnabled = a.getBoolean(R.styleable.CustomView_state_rotary_enabled);
- ビュークラスで
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; }
- ビューのクリック ハンドラがそのステータスに応じて異なる動作をするようにします。たとえば、クリック ハンドラが何もしない場合もあれば、
mRotaryEnabled
がfalse
の場合にトーストをポップアップする場合もあるようにします。 - ボタンを無効の状態で表示するには、ビューの背景ドローアブルで、
android:state_enabled
ではなくapp:state_rotary_enabled
を使用します。 まだない場合は、以下を追加する必要があります。xmlns:app="http://schemas.android.com/apk/res-auto"
- ビューがいずれかのレイアウトで無効になる場合は、
android:enabled="false"
をapp:state_rotary_enabled="false"
に置き換え、上記のようにapp
名前空間を追加します。 - ビューをプログラムによって無効にする場合は、
setEnabled()
の呼び出しをsetRotaryEnabled()
の呼び出しに置き換えます。
FocusArea
FocusAreas
を使用してフォーカス可能なビューをブロックに分割することで、ナビゲーションを容易にし、他のアプリとの一貫性を持たせることができます。たとえば、アプリにツールバーがある場合、ツールバーはアプリの他の部分とは別の FocusArea
にある必要があります。タブバーなどのナビゲーション要素も、アプリの他の部分から分離する必要があります。通常、大規模なリストには独自の FocusArea
が必要です。これがない場合、ユーザーは一部のビューにアクセスするためにリスト全体を順番にローテーションする必要があります。
FocusArea
は、car-ui-library の LinearLayout
のサブクラスです。
この機能を有効にした場合、FocusArea
の子のいずれかがフォーカスされると、ハイライトが描画されます。詳しくは、フォーカス ハイライトのカスタマイズをご覧ください。
レイアウト ファイル内にナビゲーション ブロックを作成する際に、そのブロックのコンテナとして LinearLayout
の使用を考えている場合は、代わりに FocusArea
を使用してください。それ以外の場合は、FocusArea
でブロックをラップします。
FocusArea
を別の FocusArea
にネストしないでください。
この操作を行った場合、未定義のナビゲーション動作の原因となります。フォーカス可能なすべてのビューが FocusArea
内にネストされていることを確認します。
RotaryPlayground
の FocusArea
の例を次に示します。
<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
は次のように機能します。
- ローテーション アクションと移動アクションを処理する場合、
RotaryService
はビュー階層内でFocusArea
のインスタンスを探します。 RotaryService
はローテーション イベントを受け取ると、同じFocusArea
でフォーカスを取得可能な別のビューにフォーカスを移動させます。RotaryService
は移動イベントを受け取ると、別の(通常は隣の)FocusArea
でフォーカスを取得可能な別のビューにフォーカスを移動します。
レイアウトに FocusAreas
を含めない場合、ルートビューは暗黙的なフォーカス領域として扱われます。この場合、ユーザーはアプリ内でローテーションすることができません。代わりに、フォーカス可能なすべてのビューが順に表示されます。これはダイアログに適している場合があります。
FocusArea のカスタマイズ
ロータリー ナビゲーションをカスタマイズするには、2 つの標準のビュー属性を使用できます。
android:nextFocusForward
を使用すると、アプリ デベロッパーはフォーカス エリア内でローテーション順序を指定できます。これはキーボード ナビゲーションのタブの順序を制御する場合と同じ属性です。この属性は、ループの作成には使用しないでください。 ループを作成するには、代わりにapp:wrapAround
(下記参照)を使用してください。- アプリ デベロッパーは、
android:focusedByDefault
を使用して、ウィンドウ内でデフォルトのフォーカス ビューを指定できます。この属性とapp:defaultFocus
(下記参照)は同じFocusArea
内で使用しないでください。
FocusArea
では、ロータリー ナビゲーションをカスタマイズする属性も定義します。
暗黙的なフォーカス領域は、次の属性ではカスタマイズできません。
- (Android 11 QPR3、Android 11 Car、Android 12)
app:defaultFocus
を使用することで、フォーカス可能な子孫ビューの ID を指定できます。このビューは、ユーザーがこのFocusArea
に移動した時点でフォーカスされます。 - (Android 11 QPR3、Android 11 Car、Android 12)
app:defaultFocusOverridesHistory
をtrue
に設定することで、このFocusArea
の別のビューがフォーカスされていたことを示す履歴があっても、上記で指定したビューがフォーカスされるようにすることができます。 - (Android 12)
app:nudgeLeftShortcut
、app:nudgeRightShortcut
、app:nudgeUpShortcut
、app:nudgeDownShortcut
を使用することで、フォーカス可能な子孫ビューの ID を指定します。このビューには、ユーザーが特定の方向に移動した時点でフォーカスします。詳しくは、以下の移動のショートカットをご覧ください。(Android 11 QPR3、Android 11 Car。Android 12 ではサポート終了)
app:nudgeShortcut
とapp:nudgeShortcutDirection
は 1 つの移動ショートカットのみをサポートします。 - (Android 11 QPR3、Android 11 Car、Android 12)
このFocusArea
でラップアラウンドするためにローテーションを許可する場合、app:wrapAround
をtrue
に設定します。通常は、ビューを円または楕円に配置する場合に使用します。 - (Android 11 QPR3、Android 11 Car、Android 12)
このFocusArea
でハイライトのパディングを調整する場合、app:highlightPaddingStart
、app:highlightPaddingEnd
、app:highlightPaddingTop
、app:highlightPaddingBottom
、app:highlightPaddingHorizontal
、app:highlightPaddingVertical
を使用します。 - (Android 11 QPR3、Android 11 Car、Android 12)
このFocusArea
の認識済みの境界を調整して移動のターゲットを探すには、app:startBoundOffset
、app:endBoundOffset
、app:topBoundOffset
、app:bottomBoundOffset
、app:horizontalBoundOffset
、app:verticalBoundOffset
を使用します。 - (Android 11 QPR3、Android 11 Car、Android 12)
特定の方向に隣接するFocusArea
(またはエリア)の ID を明示的に指定する場合は、app:nudgeLeft
、app:nudgeRight
、app:nudgeUp
、app:nudgeDown
を使用します。これは、デフォルトで使用されているジオメトリ検索によって目的のターゲットが見つからない場合に使用します。
通常、移動では FocusAreas 間を移動します。しかし、移動ショートカットを使用すると最初に FocusArea
内で移動することがあり、その場合ユーザーは 2 回移動して次の FocusArea
に移動することが必要となる場合があります。移動ショートカットは、次の例に示すように、FocusArea
に長いリストがあり、その後にフローティング操作ボタンがある場合に便利です。
移動ショートカットがない場合、ユーザーはリスト全体でローテーションして 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 は、これらをオーバライドすることで、自身が指定したデフォルトのフォーカス ハイライトと一貫性を持たせることができます。これにより、カスタムのフォーカス ハイライトを持つビューとデフォルトのフォーカス ハイライトを持つビューの間でユーザーが移動を行っても、フォーカスのハイライトの色やストローク幅などが変化しなくなります。最後の項目は、タップに使用されるリップルです。太字のリソースに使用されるデフォルト値は次のように表示されます。
また、ボタンに単色の背景色が割り当てられているときに、ユーザーの注意を引くためにカスタム フォーカス ハイライトが呼び出されると、次の例のようになります。この場合、フォーカス ハイライトが見づらくなります。この場合は、セカンダリ カラーを使用してカスタム フォーカス ハイライトを指定します。
- (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)
CarUiUtils
で setRotaryScrollEnabled()
を使用すると、avCarUiRecyclerView
などのスクロール可能な任意のビューでロータリーのスクロールを有効にできます。これを行うには、次の操作を行う必要があります。
- スクロール可能なビューをフォーカス可能にして、フォーカス可能な子孫ビューがどれも表示されていないときにこのビューがフォーカスされるようにします。
setDefaultFocusHighlightEnabled(false)
を呼び出して、スクロール可能なビューでデフォルトのフォーカス ハイライト表示を無効にして、スクロール可能なビューがフォーカスされた状態で表示されないようにします。setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS)
を呼び出して、スクロール可能なビューが子孫より前にフォーカスされるようにします。SOURCE_ROTARY_ENCODER
と、AXIS_VSCROLL
またはAXIS_HSCROLL
のいずれかを使用して MotionEvents をリッスンし、スクロールする距離と方向(符号で表します)を指定します。
CarUiRecyclerView
でロータリーのスクロールが有効で、ユーザーがフォーカス可能なビューがない領域にローテーションすると、スクロールバーがフォーカスされている場合と同様に、スクロールバーがグレーから青色に変わります。必要であれば、同様の効果を実装できます。
MotionEvents は、マウスのスクロール ホイールで生成されるイベントと同様ですが、ソースは異なります。
直接操作モード
通常、移動とローテーションではユーザー インターフェース内の移動が発生し、中央ボタンの押下ではアクションが実行されます。ただし、常にそうであるとは限りません。たとえば、アラームの音量を調整する場合は、ロータリー コントローラを使用して音量スライダーに移動し、中央のボタンを押してからコントローラをローテーションさせ、アラームの音量を調整します。戻るボタンを押すとナビゲーションに戻ります。これは、直接操作(DM)モードと呼ばれます。このモードでは、ロータリー コントローラはナビゲーションにではなく、ビューを直接操作するために使用されます。
DM を実装する方法は 2 通りあります。ローテーションの処理のみが必要で、操作対象のビューが ACTION_SCROLL_FORWARD
とACTION_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_selected
が true
のときに背景を変更します。
高度なメカニズム
RotaryService
が DM モードを開始する時点と終了する時点は、アプリが決定します。一貫したユーザー エクスペリエンスを確保するため、DM ビューをフォーカスした状態で中央ボタンを押すと DM モードに移行し、戻るボタンを押すと DM モードが終了するようにします。中央のボタンや移動を使用しない場合は、別の方法で DM モードを終了できます。マップなどのアプリの場合、DM を表すボタンを使用して DM モードに移行できるようにします。
高度な DM モードをサポートするには、ビューが以下の条件を満たす必要があります。
- (Android 11 QPR3、Android 11 Car、Android 12)
KEYCODE_DPAD_CENTER
イベントをリッスンして DM モードに移行し、KEYCODE_BACK
イベントをリッスンして DM を終了できる必要があります。いずれの場合もDirectManipulationHelper.enableDirectManipulationMode()
を呼び出します。 これらのイベントをリッスンするには、次のいずれかを行います。OnKeyListener
を登録する。
または
- ビューを拡張し、
dispatchKeyEvent()
メソッドをオーバーライドする。
- ビューが移動を処理する場合は、移動イベント(
KEYCODE_DPAD_UP
、KEYCODE_DPAD_DOWN
、KEYCODE_DPAD_LEFT
、KEYCODE_DPAD_RIGHT
)をリッスンする必要があります。 - ビューがローテーションを処理する場合は、
MotionEvent
をリッスンし、AXIS_SCROLL
でローテーション数を取得する必要があります。これを行うには、いくつかの方法があります。OnGenericMotionListener
を登録する。- ビューを拡張し、
dispatchTouchEvent()
メソッドをオーバーライドする。
- DM モードのままになることを避けるため、ビューが属するフラグメントまたはアクティビティがインタラクティブでないときは DM モードを終了しなければなりません。
- ビューが 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:focusedByDefault
とapp:defaultFocus
- 明示的な移動ターゲット
- 移動のショートカット
- フォーカス可能なビューがない
FocusArea