开发应用

以下材料面向应用开发者提供。

如需让您的应用支持旋转输入,您必须:

  1. 将一个 FocusParkingView 放入相应的 activity 布局中。
  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. 在“Extended controls”窗口中选择 Car rotary

    选择“Car rotary”
    图 2. 选择“Car rotary”

USB 键盘

  • 将 USB 键盘插入搭载 Android Automotive OS (AAOS) 的设备中。在某些情况下,这会导致屏幕键盘无法显示。
  • 使用 userdebugeng build。
  • 启用按键事件过滤功能:
    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-library) 中的一个透明视图。RotaryService 使用此视图来支持旋控器导航。FocusParkingView 必须是布局中的第一个可聚焦视图。它必须放置在所有 FocusArea 之外。每个窗口都必须有一个 FocusParkingView。如果您已经在使用 car-ui-library 基本布局(其中已包含一个 FocusParkingView),就无需添加其他 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 会重新聚焦该窗口中的某个视图,从而导致两个窗口同时聚焦。为每个窗口添加一个 FocusParkingView 就可以解决此问题。此视图是透明的,其默认焦点突出显示标志已停用,因此无论此视图是否聚焦,对用户都不可见。此视图可以获得焦点,使 RotaryService 能够将焦点“停”在它上面,从而移除焦点突出显示标志。
  2. 如果当前窗口中只有一个 FocusArea,那么在该 FocusArea 中旋转旋控器会导致 RotaryService 将焦点从右侧视图移至左侧视图(反之亦然)。为每个窗口添加该视图就可以解决此问题。当 RotaryService 确定焦点目标是 FocusParkingView 时,就可以确定即将发生循环,届时它需要通过不移动焦点来避免循环。
  3. 当旋转控件启动应用时,Android 会聚焦第一个可聚焦视图,该视图始终是 FocusParkingViewFocusParkingView 会确定要聚焦的最佳视图,然后应用焦点。

可聚焦视图

RotaryService 基于 Android 框架现有的视图焦点概念,这一概念可以追溯到手机有实体键盘和方向键的时候。现有的 android:nextFocusForward 属性已改为用于旋转输入(请参阅 FocusArea 自定义),但 android:nextFocusLeftandroid:nextFocusRightandroid:nextFocusUpandroid:nextFocusDown 没有更改用途。

RotaryService 只能聚焦于可聚焦视图。有一些视图(例如 Button)通常可聚焦。另一些视图(例如 TextViewViewGroup)通常不可聚焦。可点击视图自动具有可聚焦特性,并且视图在包含点击监听器时自动具有可点击特性。如果此自动逻辑导致视图具有所需的可聚焦性,您就无需显式设置视图的可聚焦性。如果自动逻辑并未让视图具有所需的可聚焦性,请将 android:focusable 属性设置为 truefalse,或者使用 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. 如需让该按钮显示为已停用,请在视图的背景可绘制对象中使用 app:state_rotary_enabled 而非 android:state_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 中。

下面显示了一个 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 的工作原理如下:

  1. 处理旋转和微移操作时,RotaryService 会在视图层次结构中查找 FocusArea 的实例。
  2. 收到旋转事件时,RotaryService 会将焦点移到同一个 FocusArea 中可以获得焦点的另一个视图上。
  3. 收到微移事件时,RotaryService 会将焦点移到另一个(通常是相邻的)FocusArea 中可以获得焦点的另一个视图上。

如果未在布局中添加任何 FocusAreas,根视图就会被视为隐式聚焦区域。用户无法通过微移在应用中导航,而是要旋转遍所有可聚焦视图,这对于对话框而言可能已经足够了。

FocusArea 自定义

您可以使用两个标准的视图属性来自定义旋转导航:

  • android:nextFocusForward:应用开发者可以用其指定聚焦区域中的旋转顺序。此属性正是用于控制键盘导航的 Tab 键顺序的属性。请勿使用此属性创建循环。相反,请使用 app:wrapAround(见下文)创建循环。
  • android:focusedByDefault:应用开发者可以用其指定窗口中的默认焦点视图。请勿在同一个 FocusArea 中使用此属性和 app:defaultFocus(见下文)。

FocusArea 还定义了一些用于自定义旋转导航的属性。隐式聚焦区域无法使用这些属性进行自定义。

  1. (Android 11 QPR3、Android 11 Car、Android 12)
    可以使用 app:defaultFocus 指定可聚焦后代视图的 ID,当用户微移到此 FocusArea 时,应聚焦于该视图。
  2. (Android 11 QPR3、Android 11 Car、Android 12)
    可将 app:defaultFocusOverridesHistory 设置为 true,这样,即使历史记录表明已聚焦于此 FocusArea 中的另一个视图,上面指定的视图也会获得焦点。
  3. (Android 12)
    使用 app:nudgeLeftShortcutapp:nudgeRightShortcutapp:nudgeUpShortcutapp:nudgeDownShortcut 指定可聚焦后代视图的 ID,当用户朝给定方向微移时,应聚焦于该视图。如需了解详情,请参阅下文微移快捷方式部分的内容。

    (Android 11 QPR3、Android 11 Car,在 Android 12 中已废弃)app:nudgeShortcutapp:nudgeShortcutDirection 仅支持一个微移快捷方式。

  4. (Android 11 QPR3、Android 11 Car、Android 12)
    如需让旋转操作能够在此 FocusArea 中循环,可以将 app:wrapAround 设置为 true当视图排列成圆形或椭圆形时,这是最常用的方式。
  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当默认使用的几何图形搜索找不到所需目标时,请使用此方式。

微移通常是在 FocusArea 之间导航。但在使用微移快捷方式时,微移有时会先在 FocusArea 中进行导航,因此用户可能需要微移两次才能导航到下一个 FocusArea。微移快捷方式适用于 FocusArea 包含长列表后跟悬浮操作按钮的情况,如以下示例所示:

微移快捷方式
图 3. 微移快捷方式

如果没有微移快捷方式,用户就必须旋转完整个列表才能访问 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 会替换这些资源,以便与其指定的默认焦点突出显示标志保持一致。这可以确保当用户在具有自定义焦点突出显示标志的视图和具有默认焦点突出显示标志的视图之间导航时,焦点突出显示颜色和描边宽度等不会发生变化。最后一项是用于触摸操作的涟漪效果。用于粗体资源的默认值如下所示:

粗体资源的默认值
图 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

如果列表中的元素都是可聚焦元素,您无需执行任何其他操作。旋转导航会顺着列表中的元素移动焦点,列表通过滚动使新聚焦的元素可见。

(Android 11 QPR3、Android 11 Car、Android 12)
如果既有可聚焦元素又有不可聚焦元素,或者所有元素均不可聚焦,您可以启用旋转滚动,这样用户就可以使用旋控器逐项滚动浏览列表,而不会跳过不可聚焦的列表项。如需启用旋转滚动,请将 app:rotaryScrollEnabled 属性设置为 true

(Android 11 QPR3、Android 11 Car、Android 12)
您可以在包括 CarUiRecyclerView 在内的任何可滚动视图中,在 CarUiUtils 中使用 setRotaryScrollEnabled() 方法启用旋转滚动。如果采用这种做法,您需要执行以下操作:

  • 将可滚动视图设置为可聚焦,以便在其所有可聚焦后代视图均不可见时对其聚焦。
  • 通过调用 setDefaultFocusHighlightEnabled(false),停用可滚动视图上的默认焦点突出显示标志,这样可滚动视图就不会显示为已聚焦。
  • 通过调用 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS),确保可滚动视图在其后代之前获得焦点。
  • 使用 SOURCE_ROTARY_ENCODER 加上 AXIS_VSCROLLAXIS_HSCROLL 来监听 MotionEvent,以便指示滚动距离和方向(通过符号)。

如果对 CarUiRecyclerView 启用了旋转滚动,并且用户旋转到没有可聚焦视图的区域,滚动条会从灰色变为蓝色,就好像表示滚动条已获得焦点。如果需要,您可以实现类似的效果。

除了来源不同之外,这些 MotionEvent 与鼠标滚轮生成的 MotionEvent 相同。

直接操作模式

通常情况下,微移和旋转操作会在界面中导航,并在居中的按钮按下时执行操作,但有时并非如此。例如,如果用户想要调整闹钟音量,他们可能会使用旋控器导航到音量滑块,按居中的按钮,旋转旋控器调整闹钟音量,然后按返回按钮返回到导航。这种模式称为直接操作 (DM) 模式。在此模式下,旋控器用于直接与视图互动,而非用于导航。

您可以采用以下两种方式之一来实现 DM。如果您只需要处理旋转,并且您要操作的视图会对 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_FORWARDACTION_SCROLL_BACKWARD,并在用户按返回按钮时退出 DM 模式。简单机制会在进入和退出 DM 模式时切换视图的选中状态。

如需提供视觉提示来提醒用户处于 DM 模式,请让视图在选中后显示不同的效果。例如,在 android:state_selectedtrue 时更改背景。

高级机制

由应用确定 RotaryService 何时进入和退出 DM 模式。为了提供一致的用户体验,在 DM 视图获得焦点时,按居中的按钮应进入 DM 模式,按返回按钮应退出 DM 模式。如果居中的按钮和/或微移未使用,可将其作为退出 DM 模式的替代方式。对于 Google 地图等应用,可以使用代表 DM 的按钮进入 DM 模式。

如需支持高级 DM 模式,视图必须满足以下条件:

  1. (Android 11 QPR3、Android 11 Car、Android 12)必须监听 KEYCODE_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 模式中,必须在视图所属的 fragment 或 activity 不具有互动性时退出 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 内部和外部的内容。如果未在 ActivityView 中添加任何 FocusArea,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 负责执行操作(例如快退),并安排自身在一定延迟后运行。

触摸模式

用户使用旋控器与汽车中的车机进行互动可以采用两种方式:使用旋控器或轻触屏幕。使用旋控器时,其中一个可聚焦视图会突出显示。轻触屏幕时,不会显示焦点突出显示标志。用户可以随时在这两种输入模式之间切换:

  • 旋转 → 触摸。当用户轻触屏幕时,焦点突出显示标志会消失。
  • 触摸 → 旋转。当用户微移、旋转或按居中的按钮时,焦点突出显示标志会出现。

返回按钮和主屏幕按钮对输入模式没有任何影响。

旋转借用了 Android 现有的触摸模式概念。您可以使用 View.isInTouchMode() 来确定用户正在使用的输入模式。您可以使用 OnTouchModeChangeListener 来监听更改。尽管您可以用这种方式为当前输入模式自定义界面,但请避免做出任何重大更改,否则可能会令用户感到困惑。

问题排查

在专为触摸操作设计的应用中,通常采用嵌套的可聚焦视图。例如,ImageButton 外面可能环绕着一个 FrameLayout,二者皆可聚焦。这对触摸没有什么害处,但会导致旋转操作的用户体验不佳,因为用户必须旋转旋控器两次才能移到下一个互动式视图。为了提供良好的用户体验,Google 建议您将外部视图或内部视图设置为可聚焦,但不要将二者同时设置为可聚焦。

如果按钮或开关在通过旋控器按下时失去焦点,可能属于以下某一种情况:

  • 该按钮或开关因为按钮被按下而(短暂或无限期)停用。无论是上述哪一种情况,您都可以通过两种方式解决此问题:
    • 保留 android:enabled 状态为 true,并使用自定义状态让按钮或开关灰显,如自定义状态中所述。
    • 使用一个容器来环绕按钮或开关,并将该容器(而不是按钮或开关)设置为可聚焦。(点击监听器必须位于容器上。)
  • 该按钮或开关被替换。例如,在按钮按下或开关切换时执行的操作可能触发了可用操作的刷新,导致新按钮取代了现有按钮。您可以通过两种方式解决此问题:
    • 不创建新的按钮或开关,而是设置现有按钮或开关的图标和/或文字。
    • 如上所述,添加一个可聚焦容器来环绕按钮或开关。

RotaryPlayground

RotaryPlayground 是旋转参考应用。您可以使用它来了解如何将旋转功能集成到您的应用中。RotaryPlayground 包含在模拟器 build 和搭载 Android Automotive OS (AAOS) 的设备的 build 中。

  • RotaryPlayground 代码库:packages/apps/Car/tests/RotaryPlayground/
  • 版本:Android 11 QPR3、Android 11 Car 和 Android 12

RotaryPlayground 应用在左侧显示了以下标签页:

  • Cards:测试在聚焦区域之间导航(跳过不可聚焦元素和文本输入)。
  • Direct Manipulation:测试支持简单和高级直接操作模式的微件。此标签页专门用于应用窗口中的直接操作。
  • Sys UI Manipulation:测试支持系统窗口(在此窗口中,仅支持简单的直接操作模式)中的直接操作的微件。
  • Grid:测试通过滚动进行 Z 型旋转导航。
  • Notification:测试移入和移出浮动通知。
  • Scroll:测试在既有可聚焦内容又有不可聚焦内容的情况下进行滚动浏览的效果。
  • WebView:测试在 WebView 中的链接之间导航。
  • Custom FocusArea:测试 FocusArea 自定义:
    • 循环。
    • android:focusedByDefaultapp:defaultFocus
    • .
    • 显式微移目标。
    • 微移快捷方式。
    • 没有可聚焦视图的 FocusArea