开发应用

以下材料适用于应用开发者。

为了使您的应用支持旋转,您必须

  1. 在相应的 Activity 布局中放置 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. 点击工具栏底部的三个点

    Access emulated rotary controller
    图 1. 访问模拟的旋转控制器
  2. 在扩展控件窗口中选择 车载旋转

    Select Car rotary
    图 2. 选择车载旋转

USB 键盘

  • 将 USB 键盘插入运行 Android Automotive OS (AAOS) 的设备。在某些情况下,这会阻止屏幕键盘出现。
  • 使用 userdebugeng 版本。
  • 启用按键事件过滤
    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

FocusParkingViewCar UI 库 (car-ui-library) 中的一个透明视图。RotaryService 使用它来支持旋转控制器导航。FocusParkingView 必须是布局中第一个可聚焦的视图。它必须放置在所有 FocusArea 之外。每个窗口必须有一个 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 会重新聚焦该窗口中的视图,从而导致两个窗口同时聚焦。向每个窗口添加 FocusParkingView 可以解决此问题。此视图是透明的,并且其默认焦点高亮效果已禁用,因此无论是否聚焦,用户都看不到它。它可以获取焦点,以便 RotaryService 可以将焦点停放在它上面以移除焦点高亮效果。
  2. 如果当前窗口中只有一个 FocusArea,则在 FocusArea 中旋转控制器会导致 RotaryService 将焦点从右侧的视图移动到左侧的视图(反之亦然)。向每个窗口添加此视图可以解决此问题。当 RotaryService 确定焦点目标是 FocusParkingView 时,它可以确定即将发生环绕,此时它会避免环绕而不移动焦点。
  3. 当旋转控件启动应用时,Android 会聚焦第一个可聚焦的视图,该视图始终是 FocusParkingViewFocusParkingView 确定要聚焦的最佳视图,然后应用焦点。

可聚焦视图

RotaryService 基于 Android 框架现有的视图焦点概念构建,该概念可以追溯到手机具有物理键盘和 D-pad 的时代。现有的 android:nextFocusForward 属性被重新用于旋转(请参阅FocusArea 自定义),但 android:nextFocusLeftandroid:nextFocusRightandroid:nextFocusUpandroid:nextFocusDown 则不是。

RotaryService 仅聚焦于可聚焦的视图。某些视图(如 Button)通常是可聚焦的。其他视图(如 TextViewViewGroup)通常不可聚焦。可点击的视图会自动变为可聚焦,当视图具有点击监听器时,会自动变为可点击。如果此自动逻辑产生所需的聚焦能力,则无需显式设置视图的聚焦能力。如果自动逻辑未产生所需的聚焦能力,请将 android:focusable 属性设置为 truefalse,或者以编程方式使用 View.setFocusable(boolean) 设置视图的聚焦能力。为了使 RotaryService 聚焦于视图,视图必须满足以下要求

  • 可聚焦
  • 已启用
  • 可见
  • 宽度和高度值不为零

如果视图不满足所有这些要求(例如,可聚焦但禁用的按钮),则用户无法使用旋转控件聚焦于它。如果您希望聚焦于禁用的视图,请考虑使用自定义状态而不是 android:state_enabled 来控制视图的显示方式,而无需指示 Android 应将其视为已禁用。您的应用可以在用户点按视图时告知用户视图禁用的原因。下一部分介绍如何执行此操作。

自定义状态

要添加自定义状态

  1. 要向您的视图添加自定义属性。例如,要向 CustomView 视图类添加 state_rotary_enabled 自定义状态,请使用
    <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 时,点击处理程序可能不执行任何操作,或者可能会弹出 Toast。
  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

使用 FocusArea 将可聚焦视图划分为多个块,以简化导航并与其他应用保持一致。例如,如果您的应用具有工具栏,则工具栏应与应用的其余部分位于单独的 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 中可以获取焦点的另一个视图。

如果您的布局中未包含任何 FocusArea,则根视图将被视为隐式焦点区域。用户无法轻推以在应用中导航。相反,他们将旋转浏览所有可聚焦视图,这对于对话框可能已足够。

FocusArea 自定义

可以使用两个标准 View 属性来自定义旋转导航

  • 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 包含一个长列表,后跟一个浮动操作按钮(如下例所示)时,轻推快捷方式非常有用

Nudge shortcut
图 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 会覆盖这些资源,以使其与他们指定的默认焦点高亮效果保持一致。这可确保当用户在具有自定义焦点高亮效果的视图和具有默认焦点高亮效果的视图之间导航时,焦点高亮效果颜色、笔划宽度等不会改变。最后一项是用于触摸的波纹效果。用于粗体资源的默认值如下所示

Default values for bold resources
图 4. 粗体资源的默认值

此外,当按钮被赋予纯色背景颜色以引起用户的注意时,也需要自定义焦点高亮效果,如下例所示。这可能会使焦点高亮效果难以看清。在这种情况下,请使用辅助颜色指定自定义焦点高亮效果

Solid background color
  • 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
已聚焦,未按下 已聚焦,已按下

旋转滚动

如果您的应用使用 RecyclerView,您应该改用 CarUiRecyclerView。这可确保您的 UI 与其他 UI 保持一致,因为 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 与鼠标上的滚轮生成的事件相同,只是来源不同。

直接操控模式

通常,轻推和旋转会在用户界面中导航,而中心按钮按下会执行操作,尽管并非总是如此。例如,如果用户想要调整闹钟音量,他们可以使用旋转控制器导航到音量滑块,按下中心按钮,旋转控制器以调整闹钟音量,然后按下返回按钮以返回导航。这被称为直接操控 (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 模式的替代方法。对于地图等应用,可以使用表示 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 KeyEvent

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 包含在模拟器版本和运行 Android Automotive OS (AAOS) 的设备的版本中。

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

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

  • 卡片。测试在焦点区域周围导航、跳过不可聚焦的元素和文本输入。
  • 直接操控。测试支持简单和高级直接操控模式的小部件。此标签专门用于应用窗口内的直接操控。
  • 系统界面操控。测试支持系统窗口中直接操控的小部件,其中仅支持简单直接操控模式。
  • 网格。测试带有滚动的 Z 字形旋转导航。
  • 通知。测试在浮动通知中和浮动通知外轻推。
  • 滚动。测试滚动浏览可聚焦和不可聚焦内容混合的内容。
  • WebView。测试在 WebView 中导航链接。
  • 自定义 FocusArea测试 FocusArea 自定义:
    • 环绕。
    • android:focusedByDefaultapp:defaultFocus
    • .
    • 显式轻推目标。
    • 轻推快捷方式。
    • 没有可聚焦视图的 FocusArea