以下材料適用於應用程序開發人員。
要使您的應用支持輪播,您必須:
- 在相應的活動佈局中放置一個
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 鍵盤
- 將 USB 鍵盤插入運行 Android Automotive OS (AAOS) 的設備,在某些情況下,這可能會阻止屏幕鍵盤出現。
- 使用
userdebug
或eng
build。 - 啟用關鍵事件過濾:
adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
- 請參閱下表以找到每個操作的對應鍵:
鑰匙 旋轉動作 問 逆時針旋轉 乙 順時針旋轉 一個 向左輕推 D 向右輕推 W 向上輕推 小號 向下輕推 F 或逗號 中心按鈕 R 或 Esc 返回鍵
亞行命令
您可以使用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
是Car UI 庫 (car-ui-library)中的一個透明視圖。 RotaryService
使用它來支持旋轉控制器導航。 FocusParkingView
必須是佈局中的第一個可聚焦視圖。它必須放在所有FocusArea
之外。每個窗口必須有一個FocusParkingView
。如果您已經在使用包含FocusParkingView
的 car-ui-library 基本佈局,則無需添加另一個FocusParkingView
。下面顯示的是FocusParkingView
中的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>
以下是您需要FocusParkingView
的原因:
- 在另一個窗口中設置焦點時,Android 不會自動清除焦點。如果您嘗試清除前一個窗口中的焦點,Android 會重新聚焦該窗口中的視圖,這會導致兩個窗口同時被聚焦。給每個窗口添加一個
FocusParkingView
可以解決這個問題。這個視圖是透明的,並且它的默認焦點突出顯示被禁用,因此無論是否聚焦,它對用戶都是不可見的。它可以獲取焦點,以便RotaryService
可以將焦點停在其上以移除焦點突出顯示。 - 如果當前窗口中只有一個
FocusArea
,則旋轉RotaryService
FocusArea
焦點從右側視圖移動到左側視圖(反之亦然)。將此視圖添加到每個窗口可以解決此問題。當RotaryService
確定焦點目標是FocusParkingView
時,它可以確定即將發生環繞,此時它通過不移動焦點來避免環繞。 - 當旋轉控件啟動應用程序時,Android 會聚焦第一個可聚焦視圖,它始終是
FocusParkingView
。FocusParkingView
確定要關注的最佳視圖,然後應用焦點。
可聚焦的視圖
RotaryService
建立在 Android 框架現有的視圖焦點概念之上,可以追溯到手機有物理鍵盤和方向鍵的時候。現有的android:nextFocusForward
屬性重新用於旋轉(請參閱FocusArea 自定義),但android:nextFocusLeft
、 android:nextFocusRight
、 android:nextFocusUp
和android:nextFocusDown
不是。
RotaryService
只關注可聚焦的視圖。某些視圖,例如Button
,通常是可聚焦的。其他的,例如TextView
s 和ViewGroup
s,通常不是。可點擊的視圖會自動獲得焦點,並且當視圖有點擊監聽器時會自動點擊。如果此自動邏輯導致所需的可聚焦性,則無需顯式設置視圖的可聚焦性。如果自動邏輯沒有產生所需的可聚焦性,請將android:focusable
focusable 屬性設置為true
或false
,或者使用View.setFocusable(boolean)
以編程方式設置視圖的可聚焦性。要讓RotaryService
專注於它,視圖必須滿足以下要求:
- 可聚焦
- 啟用
- 可見的
- 寬度和高度具有非零值
如果視圖不滿足所有這些要求,例如可聚焦但禁用的 Button,則用戶無法使用旋轉控件對其進行聚焦。如果您想關注禁用的視圖,請考慮使用自定義狀態而不是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
時,單擊處理程序可能什麼都不做,或者它可能會彈出一個 toast。 - 要使按鈕顯示為禁用,請在視圖的背景可繪製對像中使用
app:state_rotary_enabled
而不是android:state_enabled
。如果您還沒有,則需要添加:xmlns:app="http://schemas.android.com/apk/res-auto"
- 如果您的視圖在任何佈局中被禁用,請將
android:enabled="false"
替換為app:state_rotary_enabled="false"
,然後添加app
命名空間,如上所述。 - 如果您的視圖以編程方式被禁用,請將對
setEnabled()
的調用替換為對setRotaryEnabled()
的調用。
重點地區
使用FocusAreas
將可聚焦視圖劃分為塊,以使導航更容易並與其他應用程序保持一致。例如,如果您的應用程序有一個工具欄,則該工具欄應該與您的應用程序的其餘部分位於一個單獨的FocusArea
中。標籤欄和其他導航元素也應該與應用程序的其餘部分分開。大型列表通常應該有自己的FocusArea
。如果沒有,用戶必須輪流瀏覽整個列表才能訪問某些視圖。
FocusArea
是 car-ui-library 中LinearLayout
的子類。啟用此功能後, FocusArea
將在其後代之一被聚焦時突出顯示。要了解更多信息,請參閱Focus 突出顯示自定義。
在佈局文件中創建導航塊時,如果您打算使用LinearLayout
作為該塊的容器,請改用FocusArea
。否則,將塊包裝在FocusArea
中。
不要將FocusArea嵌套在另一個FocusArea
FocusArea
。這樣做會導致未定義的導航行為。確保所有可聚焦的視圖都嵌套在FocusArea
中。
FocusArea
中的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
工作原理如下:
- 在處理旋轉和微調動作時,
RotaryService
在視圖層次結構中查找FocusArea
的實例。 - 當接收到一個旋轉事件時,
RotaryService
將焦點移動到另一個可以在同一個FocusArea
中獲得焦點的 View。 - 當接收到一個微調事件時,
RotaryService
將焦點移動到另一個視圖,該視圖可以在另一個(通常是相鄰的)FocusArea
中獲得焦點。
如果您的佈局中不包含任何FocusAreas
,則根視圖將被視為隱式焦點區域。用戶無法輕推以在應用程序中導航。相反,它們將在所有可聚焦的視圖中旋轉,這對於對話框來說可能已經足夠了。
焦點區域定制
兩個標準視圖屬性可用於自定義旋轉導航:
-
android:nextFocusForward
允許應用程序開發人員指定焦點區域中的旋轉順序。這與用於控制鍵盤導航的 Tab 順序的屬性相同。不要使用此屬性來創建循環。相反,使用app:wrapAround
(見下文)創建一個循環。 -
android:focusedByDefault
允許應用開發者在窗口中指定默認的焦點視圖。不要在同一個FocusArea
中使用此屬性和app:defaultFocus
(見下文)。
FocusArea
還定義了一些屬性來自定義旋轉導航。無法使用這些屬性自定義隱式焦點區域。
- ( Android 11 QPR3、Android 11 汽車、Android 12 )
app:defaultFocus
可用於指定可聚焦的後代視圖的 ID,當用戶輕推到此FocusArea
時,它應該被聚焦。 - ( Android 11 QPR3、Android 11 汽車、Android 12 )
app:defaultFocusOverridesHistory
可以設置為true
以使上面指定的視圖獲得焦點,即使歷史表明此FocusArea
中的另一個視圖已被關注。 - (安卓12 )
使用app:nudgeLeftShortcut
、app:nudgeRightShortcut
、app:nudgeUpShortcut
和app:nudgeDownShortcut
來指定可聚焦後代視圖的 ID,當用戶向給定方向輕推時,它應該被聚焦。要了解更多信息,請參閱下面的微調快捷方式的內容。( Android 11 QPR3、Android 11 Car,在 Android 12 中已棄用)
app:nudgeShortcut
和app:nudgeShortcutDirection
僅支持一個輕推快捷方式。 - ( Android 11 QPR3、Android 11 汽車、Android 12 )
要在此FocusArea
中啟用旋轉環繞,可以將app:wrapAround
設置為true
。這通常在視圖以圓形或橢圓形排列時使用。 - ( Android 11 QPR3、Android 11 汽車、Android 12 )
要調整此FocusArea
中突出顯示的填充,請使用app:highlightPaddingStart
、app:highlightPaddingEnd
、app:highlightPaddingTop
、app:highlightPaddingBottom
、app:highlightPaddingHorizontal
ntal 和app:highlightPaddingVertical
。 - ( Android 11 QPR3、Android 11 汽車、Android 12 )
要調整此FocusArea
的感知邊界以找到微調目標,請使用app:startBoundOffset
、app:endBoundOffset
、app:topBoundOffset
、 app:bottomBoundOffset 、app:horizontalBoundOffset
app:bottomBoundOffset
和app:verticalBoundOffset
。 - ( Android 11 QPR3、Android 11 汽車、Android 12 )
要明確指定給定方向上相鄰FocusArea
(或多個區域)的 ID,請使用app:nudgeLeft
、app:nudgeRight
、app:nudgeUp
和app:nudgeDown
。當默認使用的幾何搜索未找到所需目標時使用此選項。
輕推通常在 FocusAreas 之間導航。但是對於輕推快捷方式,輕推有時會首先在FocusArea
內導航,因此用戶可能需要輕推兩次才能導航到下一個FocusArea
。當FocusArea
包含一個長列表後跟一個Floating Action Button時,微調快捷鍵很有用,如下例所示:
如果沒有輕推快捷方式,用戶將不得不旋轉整個列表才能到達 FAB。
焦點高亮定制
如上所述, RotaryService
建立在 Android 框架現有的視圖焦點概念之上。當用戶旋轉和輕推時, RotaryService
會移動焦點,聚焦一個視圖並取消聚焦另一個視圖。在 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 汽車、Android 12 )
car_ui_rotary_focus_fill_secondary_color
car_ui_rotary_focus_stroke_secondary_color
- (安卓12 )
car_ui_rotary_focus_pressed_fill_secondary_color
car_ui_rotary_focus_pressed_stroke_secondary_color
例如:
專注,不按 | 專注,按下 |
旋轉滾動
如果您的應用程序使用RecyclerView
,您應該改用CarUiRecyclerView
。這可確保您的 UI 與其他 UI 一致,因為 OEM 的自定義適用於所有CarUiRecyclerView
。
如果列表中的元素都是可聚焦的,則無需執行任何其他操作。旋轉導航通過列表中的元素移動焦點,並且列表滾動以使新獲得焦點的元素可見。
( Android 11 QPR3、Android 11 汽車、Android 12 )
如果混合了可聚焦和不可聚焦的元素,或者如果所有元素都不可聚焦,則可以啟用旋轉滾動,這允許用戶使用旋轉控制器逐漸滾動列表,而不會跳過不可聚焦的項目。要啟用旋轉滾動,請將app:rotaryScrollEnabled
屬性設置為true
。
( Android 11 QPR3、Android 11 汽車、Android 12 )
您可以使用CarUiUtils
中的setRotaryScrollEnabled()
方法在任何可滾動視圖中啟用旋轉滾動,包括 av CarUiRecyclerView
。如果這樣做,您需要:
- 使可滾動視圖可聚焦,以便在其可聚焦的後代視圖都不可見時聚焦,
- 通過調用
setDefaultFocusHighlightEnabled(false)
禁用可滾動視圖上的默認焦點突出顯示,以便可滾動視圖看起來沒有焦點, - 通過調用
setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS)
確保可滾動視圖在其後代之前聚焦。 - 使用
SOURCE_ROTARY_ENCODER
和AXIS_VSCROLL
或AXIS_HSCROLL
以指示滾動距離和方向(通過標誌)。
當在CarUiRecyclerView
上啟用旋轉滾動並且用戶旋轉到不存在可聚焦視圖的區域時,滾動條會從灰色變為藍色,好像表示滾動條已聚焦。如果你願意,你可以實現類似的效果。
除了源之外,MotionEvents 與鼠標滾輪生成的相同。
直接操作模式
通常,輕推和旋轉在用戶界面中導航,而按下中心按鈕會採取行動,儘管情況並非總是如此。例如,如果用戶想要調整鬧鐘音量,他們可能會使用旋轉控制器導航到音量滑塊,按中心按鈕,旋轉控制器以調整鬧鐘音量,然後按返回按鈕返回導航.這稱為直接操作 (DM)模式。在這種模式下,旋轉控制器用於直接與視圖交互而不是導航。
以兩種方式之一實施 DM。如果您只需要處理旋轉並且要操作的視圖適當地響應ACTION_SCROLL_FORWARD
和ACTION_SCROLL_BACKWARD
AccessibilityEvent
,請使用簡單的機制。否則,使用高級機制。
簡單機制是系統窗口中的唯一選擇;應用程序可以使用任何一種機制。
簡單的機制
( Android 11 QPR3、Android 11 汽車、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 視圖的情況下按下 Center 按鈕應進入 DM 模式,而 Back 按鈕應退出 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 模式,當視圖所屬的 Fragment 或 Activity 不是交互式時,必須退出 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
不應該是可聚焦的。 - ( Android 11 QPR3、Android 11 Car,在 Android 11 中已棄用)
ActivityView
的內容必須包含一個FocusParkingView
作為第一個焦點視圖,並且它的app:shouldRestoreFocus
屬性必須是false
。 -
ActivityView
的內容應該沒有android:focusByDefault
視圖。
對於用戶來說,ActivityViews 應該對導航沒有影響,只是焦點區域不能跨越 ActivityViews。換句話說,您不能在ActivityView
內部和外部擁有一個包含內容的焦點區域。如果您不向ActivityView
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
採取行動(例如倒帶)並安排自己在延遲後運行。
觸控模式
用戶可以通過兩種方式使用旋轉控制器與汽車中的主機交互,通過使用旋轉控制器或通過觸摸屏幕。使用旋轉控制器時,將突出顯示其中一個可聚焦視圖。觸摸屏幕時,不會出現焦點突出顯示。用戶可以隨時在這些輸入模式之間切換:
- 旋轉→觸摸。當用戶觸摸屏幕時,焦點突出顯示消失。
- 觸摸 → 旋轉。當用戶輕推、旋轉或按下中心按鈕時,會出現焦點突出顯示。
Back 和 Home 按鈕對輸入模式沒有影響。
旋轉搭載 Android 現有的觸控模式概念。您可以使用View.isInTouchMode()
來確定用戶正在使用哪種輸入模式。您可以使用OnTouchModeChangeListener
來監聽變化。雖然這可用於為當前輸入模式自定義用戶界面,但請避免任何重大更改,因為它們可能會令人不安。
故障排除
在為觸摸設計的應用程序中,嵌套可聚焦視圖並不少見。例如, ImageButton
周圍可能有一個FrameLayout
,兩者都是可聚焦的。這對觸摸沒有任何危害,但會導致用戶對旋轉的體驗不佳,因為用戶必須旋轉控制器兩次才能移動到下一個交互式視圖。為了獲得良好的用戶體驗,Google 建議您將外部視圖或內部視圖設為可聚焦,但不能同時設置兩者。
如果通過旋轉控制器按下按鈕或開關時失去焦點,可能會出現以下情況之一:
- 由於按下按鈕,按鈕或開關被禁用(短暫或無限期)。無論哪種情況,有兩種方法可以解決這個問題:
- 將
android:enabled
狀態保留為true
並使用自定義狀態使按鈕或開關變灰,如Custom State中所述。 - 使用容器圍繞按鈕或開關,並使容器而不是按鈕或開關成為焦點。 (點擊監聽器必須在容器上。)
- 將
- 正在更換按鈕或開關。例如,按下按鈕或切換開關時採取的操作可能會觸發可用操作的刷新,從而導致新按鈕替換現有按鈕。有兩種方法可以解決這個問題:
- 不要創建新按鈕或開關,而是設置現有按鈕或開關的圖標和/或文本。
- 如上所述,在按鈕或開關周圍添加一個可聚焦的容器。
旋轉遊樂場
RotaryPlayground
是一個旋轉的參考應用程序。使用它來了解如何將旋轉功能集成到您的應用程序中。 RotaryPlayground
包含在模擬器構建和運行 Android Automotive OS (AAOS) 的設備的構建中。
-
RotaryPlayground
存儲庫:packages/apps/Car/tests/RotaryPlayground/
- 版本:Android 11 QPR3、Android 11 Car 和 Android 12
RotaryPlayground
應用程序在左側顯示以下選項卡:
- 牌。測試在焦點區域周圍導航,跳過無法聚焦的元素和文本輸入。
- 直接操縱。測試支持簡單和高級直接操作模式的小部件。此選項卡專門用於在應用程序窗口中進行直接操作。
- 系統用戶界面操作。在僅支持簡單直接操作模式的系統窗口中測試支持直接操作的小部件。
- 網格。使用滾動測試 z 模式旋轉導航。
- 通知。測試輕推進出平視通知。
- 滾動。測試滾動瀏覽可聚焦和不可聚焦的內容。
- 網絡視圖。測試通過
WebView
中的鏈接導航。 - 自定義
FocusArea
。測試FocusArea
自定義:- 環繞。
-
android:focusedByDefault
和app:defaultFocus
. - 明確的微調目標。
- 輕推快捷方式。
- 沒有可聚焦視圖的
FocusArea
。
以下材料適用於應用程序開發人員。
要使您的應用支持輪播,您必須:
- 在相應的活動佈局中放置一個
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 鍵盤
- 將 USB 鍵盤插入運行 Android Automotive OS (AAOS) 的設備,在某些情況下,這可能會阻止屏幕鍵盤出現。
- 使用
userdebug
或eng
build。 - 啟用關鍵事件過濾:
adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
- 請參閱下表以找到每個操作的對應鍵:
鑰匙 旋轉動作 問 逆時針旋轉 乙 順時針旋轉 一個 向左輕推 D 向右輕推 W 向上輕推 小號 向下輕推 F 或逗號 中心按鈕 R 或 Esc 返回鍵
亞行命令
您可以使用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
是Car UI 庫 (car-ui-library)中的一個透明視圖。 RotaryService
使用它來支持旋轉控制器導航。 FocusParkingView
必須是佈局中的第一個可聚焦視圖。它必須放在所有FocusArea
之外。每個窗口必須有一個FocusParkingView
。如果您已經在使用包含FocusParkingView
的 car-ui-library 基本佈局,則無需添加另一個FocusParkingView
。下面顯示的是FocusParkingView
中的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>
以下是您需要FocusParkingView
的原因:
- 在另一個窗口中設置焦點時,Android 不會自動清除焦點。如果您嘗試清除前一個窗口中的焦點,Android 會重新聚焦該窗口中的視圖,這會導致兩個窗口同時被聚焦。給每個窗口添加一個
FocusParkingView
可以解決這個問題。這個視圖是透明的,並且它的默認焦點突出顯示被禁用,因此無論是否聚焦,它對用戶都是不可見的。它可以獲取焦點,以便RotaryService
可以將焦點停在其上以移除焦點突出顯示。 - 如果當前窗口中只有一個
FocusArea
,則旋轉RotaryService
FocusArea
焦點從右側視圖移動到左側視圖(反之亦然)。將此視圖添加到每個窗口可以解決此問題。當RotaryService
確定焦點目標是FocusParkingView
時,它可以確定即將發生環繞,此時它通過不移動焦點來避免環繞。 - 當旋轉控件啟動應用程序時,Android 會聚焦第一個可聚焦視圖,它始終是
FocusParkingView
。FocusParkingView
確定要關注的最佳視圖,然後應用焦點。
可聚焦的視圖
RotaryService
建立在 Android 框架現有的視圖焦點概念之上,可以追溯到手機有物理鍵盤和方向鍵的時候。現有的android:nextFocusForward
屬性重新用於旋轉(請參閱FocusArea 自定義),但android:nextFocusLeft
、 android:nextFocusRight
、 android:nextFocusUp
和android:nextFocusDown
不是。
RotaryService
只關注可聚焦的視圖。某些視圖,例如Button
,通常是可聚焦的。其他的,例如TextView
s 和ViewGroup
s,通常不是。可點擊的視圖會自動獲得焦點,並且當視圖有點擊監聽器時會自動點擊。如果此自動邏輯導致所需的可聚焦性,則無需顯式設置視圖的可聚焦性。如果自動邏輯沒有產生所需的可聚焦性,請將android:focusable
focusable 屬性設置為true
或false
,或者使用View.setFocusable(boolean)
以編程方式設置視圖的可聚焦性。要讓RotaryService
專注於它,視圖必須滿足以下要求:
- 可聚焦
- 啟用
- 可見的
- 寬度和高度具有非零值
如果視圖不滿足所有這些要求,例如可聚焦但禁用的 Button,則用戶無法使用旋轉控件對其進行聚焦。如果您想關注禁用的視圖,請考慮使用自定義狀態而不是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
時,單擊處理程序可能什麼都不做,或者它可能會彈出一個 toast。 - 要使按鈕顯示為禁用,請在視圖的背景可繪製對像中使用
app:state_rotary_enabled
而不是android:state_enabled
。如果您還沒有,則需要添加:xmlns:app="http://schemas.android.com/apk/res-auto"
- 如果您的視圖在任何佈局中被禁用,請將
android:enabled="false"
替換為app:state_rotary_enabled="false"
,然後添加app
命名空間,如上所述。 - 如果您的視圖以編程方式被禁用,請將對
setEnabled()
的調用替換為對setRotaryEnabled()
的調用。
重點地區
使用FocusAreas
將可聚焦視圖劃分為塊,以使導航更容易並與其他應用程序保持一致。例如,如果您的應用程序有一個工具欄,則該工具欄應該與您的應用程序的其餘部分位於一個單獨的FocusArea
中。標籤欄和其他導航元素也應該與應用程序的其餘部分分開。大型列表通常應該有自己的FocusArea
。如果沒有,用戶必須輪流瀏覽整個列表才能訪問某些視圖。
FocusArea
是 car-ui-library 中LinearLayout
的子類。啟用此功能後, FocusArea
將在其後代之一被聚焦時突出顯示。要了解更多信息,請參閱Focus 突出顯示自定義。
在佈局文件中創建導航塊時,如果您打算使用LinearLayout
作為該塊的容器,請改用FocusArea
。否則,將塊包裝在FocusArea
中。
不要將FocusArea嵌套在另一個FocusArea
FocusArea
。這樣做會導致未定義的導航行為。確保所有可聚焦的視圖都嵌套在FocusArea
中。
FocusArea
中的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
works as follows:
- When handling rotate and nudge actions,
RotaryService
looks for instances ofFocusArea
in the view hierarchy. - When receiving a rotation event,
RotaryService
moves focus to another View that can take focus in the sameFocusArea
. - 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, useapp: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 andapp:defaultFocus
(see below) in the sameFocusArea
.
FocusArea
also defines some attributes to customize rotary navigation. Implicit focus areas can't be customized with these attributes.
- ( 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 thisFocusArea
. - ( Android 11 QPR3, Android 11 Car, Android 12 )
app:defaultFocusOverridesHistory
can be set totrue
to make the view specified above take focus even if with history to indicate another view in thisFocusArea
had been focused on. - ( Android 12 )
Useapp:nudgeLeftShortcut
,app:nudgeRightShortcut
,app:nudgeUpShortcut
, andapp: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
andapp:nudgeShortcutDirection
supported only one nudge shortcut. - ( Android 11 QPR3, Android 11 Car, Android 12 )
To enable rotation to wrap around in thisFocusArea
,app:wrapAround
can be set totrue
. This is most typically used when views are arranged in a circle or oval. - ( Android 11 QPR3, Android 11 Car, Android 12 )
To adjust the padding of the highlight in thisFocusArea
, useapp:highlightPaddingStart
,app:highlightPaddingEnd
,app:highlightPaddingTop
,app:highlightPaddingBottom
,app:highlightPaddingHorizontal
, andapp:highlightPaddingVertical
. - ( Android 11 QPR3, Android 11 Car, Android 12 )
To adjust the perceived bounds of thisFocusArea
to find a nudge target, useapp:startBoundOffset
,app:endBoundOffset
,app:topBoundOffset
,app:bottomBoundOffset
,app:horizontalBoundOffset
, andapp:verticalBoundOffset
. - ( Android 11 QPR3, Android 11 Car, Android 12 )
To explicitly specify the ID of an adjacentFocusArea
(or areas) in the given directions, useapp:nudgeLeft
,app:nudgeRight
,app:nudgeUp
, andapp: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:
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:
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:
- ( 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
例如:
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 eitherAXIS_VSCROLL
orAXIS_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:
- ( Android 11 QPR3, Android 11 Car, Android 12 ) MUST listen for a
KEYCODE_DPAD_CENTER
event to enter DM mode and listen for aKEYCODE_BACK
event to exit DM mode, callingDirectManipulationHelper.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.
- Register an
- SHOULD listen for nudge events (
KEYCODE_DPAD_UP
,KEYCODE_DPAD_DOWN
,KEYCODE_DPAD_LEFT
, orKEYCODE_DPAD_RIGHT
) if the view should handle nudges. - SHOULD listen to
MotionEvent
s and get rotation count inAXIS_SCROLL
if the view wants to handle rotation. There are several ways to do this:- Register an
OnGenericMotionListener
. - Extend the view and override its
dispatchTouchEvent()
method.
- Register an
- To avoid being stuck in DM mode, MUST exit DM mode when the Fragment or Activity the view belongs to is not interactive.
- 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 theActivityView
MUST contain aFocusParkingView
as the first focusable view, and itsapp:shouldRestoreFocus
attribute MUST befalse
. - The contents of the
ActivityView
should have noandroid: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 astrue
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.)
- Leave the
- 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
. TestFocusArea
customization:- Wrap-around.
-
android:focusedByDefault
andapp:defaultFocus
. - Explicit nudge targets.
- Nudge shortcuts.
-
FocusArea
with no focusable views.