开发应用

以下材料适用于应用程序开发人员。

要使您的应用程序支持旋转,您必须:

  1. FocusParkingView放置在相应的活动布局中。
  2. 确保视图可(或不可)聚焦。
  3. 使用FocusArea来环绕所有可聚焦视图( FocusParkingView除外)。

在设置好开发支持旋转的应用程序的环境后,下面详细介绍了其中的每项任务。

设置旋转控制器

在开始开发支持旋转的应用程序之前,您需要一个旋转控制器或替代品。您有如下所述的选项。

模拟器

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

您还可以使用aosp_car_x86_64-userdebug

要访问模拟旋转控制器:

  1. 点击工具栏底部的三个点:

    访问模拟旋转控制器
    图 1.访问模拟旋转控制器
  2. 在扩展控制窗口中选择汽车旋转

    选择汽车旋转
    图 2.选择汽车旋转

USB键盘

  • 将 USB 键盘插入运行 Android Automotive OS (AAOS) 的设备,在某些情况下,这会阻止屏幕键盘出现。
  • 使用userdebugeng版本。
  • 启用按键事件过滤:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • 请参阅下表查找每个操作对应的键:
    钥匙旋转动作
    逆时针旋转
    顺时针旋转
    A向左微移
    D向右微移
    向上微移
    S向下微移
    F 或逗号中心按钮
    R 或 Esc返回键

亚行命令

您可以使用car_service命令注入旋转输入事件。这些命令可以在运行 Android Automotive OS (AAOS) 的设备或模拟器上运行。

汽车服务命令旋转输入
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汽车 UI 库 (car-ui-library)中的透明视图。 RotaryService使用它来支持旋转控制器导航。 FocusParkingView必须是布局中第一个可聚焦的视图。它必须放置在所有FocusArea之外。每个窗口必须有一个FocusParkingView 。如果您已经在使用包含FocusParkingView car-ui-library 基本布局,则无需添加另一个FocusParkingView 。下面显示的是RotaryPlaygroundFocusParkingView的示例。

<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()的调用。

重点地区

使用FocusAreas将可聚焦视图划分为块,以使导航更容易并与其他应用程序保持一致。例如,如果您的应用程序有一个工具栏,则该工具栏应位于与应用程序其余部分不同的FocusArea中。选项卡栏和其他导航元素也应与应用程序的其余部分分开。大型列表通常应该有自己的FocusArea 。如果没有,用户必须轮流浏览整个列表才能访问某些视图。

FocusArea是 car-ui-library 中LinearLayout的子类。启用此功能后, FocusArea会在其后代之一获得焦点时绘制突出显示。要了解更多信息,请参阅焦点突出显示自定义

在布局文件中创建导航块时,如果您打算使用LinearLayout作为该块的容器,请改用FocusArea 。否则,将该块包装在FocusArea中。

不要FocusArea嵌套在另一个FocusArea中。这样做会导致未定义的导航行为。确保所有可聚焦视图都嵌套在FocusArea中。

RotaryPlaygroundFocusArea的示例如下所示:

<com.android.car.ui.FocusArea
       android:layout_margin="16dp"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">
       <EditText
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:singleLine="true">
       </EditText>
   </com.android.car.ui.FocusArea>

FocusArea工作原理如下:

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

如果布局中不包含任何FocusAreas ,根视图将被视为隐式焦点区域。用户无法在应用程序中进行导航。相反,它们将在所有可聚焦视图之间旋转,这对于对话框来说可能足够了。

焦点区域定制

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

  • android:nextFocusForward允许应用程序开发人员指定焦点区域中的旋转顺序。该属性与用于控制键盘导航的 Tab 键顺序的属性相同。不要使用此属性来创建循环。相反,使用app:wrapAround (见下文)来创建循环。
  • android:focusedByDefault允许应用程序开发人员指定窗口中的默认焦点视图。不要在同一个FocusArea中使用此属性和app:defaultFocus (见下文)。

FocusArea还定义了一些属性来自定义旋转导航。无法使用这些属性自定义隐式焦点区域。

  1. Android 11 QPR3、Android 11 汽车、Android 12
    app:defaultFocus可用于指定可聚焦后代视图的 ID,当用户轻移到此FocusArea时,该子视图应该聚焦。
  2. Android 11 QPR3、Android 11 汽车、Android 12
    app:defaultFocusOverridesHistory可以设置为true以使上面指定的视图获得焦点,即使历史记录表明此FocusArea中的另一个视图已获得焦点。
  3. 安卓12
    使用app:nudgeLeftShortcutapp:nudgeRightShortcutapp:nudgeUpShortcutapp:nudgeDownShortcut指定可聚焦后代视图的 ID,当用户向给定方向微移时应聚焦该子视图。要了解更多信息,请参阅下面的微移快捷方式内容。

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

  4. Android 11 QPR3、Android 11 汽车、Android 12
    要使旋转能够在此FocusArea中环绕,可以将app:wrapAround设置为true 。当视图排列成圆形或椭圆形时,最常使用这种方式。
  5. Android 11 QPR3、Android 11 汽车、Android 12
    要调整此FocusArea中突出显示的填充,请使用app:highlightPaddingStartapp:highlightPaddingEndapp:highlightPaddingTopapp:highlightPaddingBottomapp:highlightPaddingHorizontalapp:highlightPaddingVertical
  6. Android 11 QPR3、Android 11 汽车、Android 12
    要调整此FocusArea的感知边界以查找微移目标,请使用app:startBoundOffsetapp:endBoundOffsetapp:topBoundOffsetapp:bottomBoundOffsetapp:horizontalBoundOffsetapp:verticalBoundOffset
  7. Android 11 QPR3、Android 11 汽车、Android 12
    要显式指定给定方向上相邻FocusArea (或多个区域)的 ID,请使用app:nudgeLeftapp:nudgeRightapp:nudgeUpapp:nudgeDown 。当默认使用的几何搜索找不到所需的目标时,请使用此选项。

轻推通常在焦点区域之间导航。但使用微移快捷方式时,微移有时会首先在FocusArea内导航,因此用户可能需要微移两次才能导航到下一个FocusArea 。当FocusArea包含一个长列表后跟一个Floating Action Button时,微移快捷键非常有用,如下例所示:

微移快捷方式
图 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 汽车、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_ENCODERAXIS_VSCROLLAXIS_HSCROLL监听 MotionEvent,以指示滚动距离和方向(通过标志)。

当在CarUiRecyclerView上启用旋转滚动并且用户旋转到不存在可聚焦视图的区域时,滚动条会从灰色变为蓝色,就好像指示滚动条已获得焦点一样。如果您愿意,您可以实现类似的效果。

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

直接操控模式

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

通过以下两种方式之一实施 DM。如果您只需要处理旋转并且要操作的视图适当地响应ACTION_SCROLL_FORWARDACTION_SCROLL_BACKWARD AccessibilityEvent ,请使用简单的机制。否则,请使用高级机制。

简单机制是系统窗口中唯一的选择;应用程序可以使用任一机制。

机制简单

Android 11 QPR3、Android 11 汽车、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不应该是可聚焦的。
  • Android 11 QPR3、Android 11 汽车、Android 11 中已弃用
    ActivityView的内容必须包含FocusParkingView作为第一个可聚焦视图,并且其app:shouldRestoreFocus属性必须为false
  • ActivityView的内容不应有android:focusByDefault视图。

对于用户来说,除了焦点区域不能跨越 ActivityView 之外,ActivityView 不应该对导航产生任何影响。换句话说,您不能拥有一个在ActivityView内部外部都包含内容的焦点区域。如果您没有向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执行一个操作(例如倒带)并安排自己在延迟后运行。

触摸模式

用户可以通过两种方式使用旋转控制器与汽车主机进行交互:使用旋转控制器或触摸屏幕。使用旋转控制器时,可聚焦视图之一会突出显示。触摸屏幕时,不会出现焦点突出显示。用户可以随时在这些输入模式之间切换:

  • 旋转→触摸。当用户触摸屏幕时,焦点突出显示就会消失。
  • 触摸 → 旋转。当用户轻推、旋转或按下中心按钮时,会出现焦点突出显示。

返回和主页按钮对输入模式没有影响。

旋转搭载了 Android 现有的触摸模式概念。您可以使用View.isInTouchMode()来确定用户正在使用哪种输入模式。您可以使用OnTouchModeChangeListener来监听更改。虽然这可用于针对当前输入模式自定义用户界面,但请避免任何重大更改,因为它们可能会令人不安。

故障排除

在专为触摸设计的应用程序中,通常具有嵌套的可聚焦视图。例如, ImageButton周围可能有一个FrameLayout ,两者都是可聚焦的。这对触摸没有任何损害,但可能会导致旋转的用户体验不佳,因为用户必须旋转控制器两次才能移动到下一个交互式视图。为了获得良好的用户体验,Google 建议您将外部视图或内部视图设置为可聚焦的,但不要同时设置两者。

如果通过旋转控制器按下按钮或开关时失去焦点,则可能存在以下情况之一:

  • 由于按下按钮,按钮或开关被禁用(短暂或无限期)。无论哪种情况,都有两种方法可以解决此问题:
    • android:enabled状态保留为true并使用自定义状态使按钮或开关变灰,如自定义状态中所述。
    • 使用容器包围按钮或开关,并使容器(而不是按钮或开关)可聚焦。 (点击侦听器必须位于容器上。)
  • 正在更换按钮或开关。例如,按下按钮或切换开关时采取的操作可能会触发可用操作的刷新,从而导致新按钮替换现有按钮。有两种方法可以解决这个问题:
    • 设置现有按钮或开关的图标和/或文本,而不是创建新按钮或开关。
    • 如上所述,在按钮或开关周围添加一个可聚焦的容器。

扶轮游乐场

RotaryPlayground是旋转的参考应用程序。使用它来了解如何将旋转功能集成到您的应用程序中。 RotaryPlayground包含在模拟器构建以及运行 Android Automotive OS (AAOS) 的设备构建中。

  • RotaryPlayground存储库: packages/apps/Car/tests/RotaryPlayground/
  • 版本:Android 11 QPR3、Android 11 汽车和 Android 12

RotaryPlayground应用程序在左侧显示以下选项卡:

  • 牌。测试围绕焦点区域导航,跳过不可聚焦的元素和文本输入。
  • 直接操纵。测试支持简单和高级直接操作模式的小部件。此选项卡专门用于在应用程序窗口内直接操作。
  • 系统 UI 操作。测试支持在仅支持简单直接操作模式的系统窗口中直接操作的小部件。
  • 网格。通过滚动测试 z 模式旋转导航。
  • 通知。测试推送通知的进出。
  • 滚动。测试滚动可聚焦和不可聚焦内容的组合。
  • 网页视图。测试通过WebView中的链接进行导航。
  • 自定义FocusArea测试FocusArea定制:
    • 环绕式。
    • android:focusedByDefaultapp:defaultFocus
    • 明确的微调目标。
    • 微移快捷方式。
    • FocusArea没有可聚焦视图。
,

以下材料适用于应用程序开发人员。

要使您的应用程序支持旋转,您必须:

  1. FocusParkingView放置在相应的活动布局中。
  2. 确保视图可(或不可)聚焦。
  3. 使用FocusArea来环绕所有可聚焦视图( FocusParkingView除外)。

在设置好开发支持旋转的应用程序的环境后,下面详细介绍了其中的每项任务。

设置旋转控制器

在开始开发支持旋转的应用程序之前,您需要一个旋转控制器或替代品。您有如下所述的选项。

模拟器

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

您还可以使用aosp_car_x86_64-userdebug

要访问模拟旋转控制器:

  1. 点击工具栏底部的三个点:

    访问模拟旋转控制器
    图 1.访问模拟旋转控制器
  2. 在扩展控制窗口中选择汽车旋转

    选择汽车旋转
    图 2.选择汽车旋转

USB键盘

  • 将 USB 键盘插入运行 Android Automotive OS (AAOS) 的设备,在某些情况下,这会阻止屏幕键盘出现。
  • 使用userdebugeng版本。
  • 启用按键事件过滤:
    adb shell settings put secure android.car.ROTARY_KEY_EVENT_FILTER 1
    
  • 请参阅下表查找每个操作对应的键:
    钥匙旋转动作
    逆时针旋转
    顺时针旋转
    A向左微移
    D向右微移
    向上微移
    S向下微移
    F 或逗号中心按钮
    R 或 Esc返回键

亚行命令

您可以使用car_service命令注入旋转输入事件。这些命令可以在运行 Android Automotive OS (AAOS) 的设备或模拟器上运行。

汽车服务命令旋转输入
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汽车 UI 库 (car-ui-library)中的透明视图。 RotaryService使用它来支持旋转控制器导航。 FocusParkingView必须是布局中第一个可聚焦的视图。它必须放置在所有FocusArea之外。每个窗口必须有一个FocusParkingView 。如果您已经在使用包含FocusParkingView car-ui-library 基本布局,则无需添加另一个FocusParkingView 。下面显示的是RotaryPlaygroundFocusParkingView的示例。

<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()的调用。

重点地区

使用FocusAreas将可聚焦视图划分为块,以使导航更容易并与其他应用程序保持一致。例如,如果您的应用程序有一个工具栏,则该工具栏应位于与应用程序其余部分不同的FocusArea中。选项卡栏和其他导航元素也应与应用程序的其余部分分开。大型列表通常应该有自己的FocusArea 。如果没有,用户必须轮流浏览整个列表才能访问某些视图。

FocusArea是 car-ui-library 中LinearLayout的子类。启用此功能后, FocusArea会在其后代之一获得焦点时绘制突出显示。要了解更多信息,请参阅焦点突出显示自定义

在布局文件中创建导航块时,如果您打算使用LinearLayout作为该块的容器,请改用FocusArea 。否则,将该块包装在FocusArea中。

不要FocusArea嵌套在另一个FocusArea中。这样做会导致未定义的导航行为。确保所有可聚焦视图都嵌套在FocusArea中。

RotaryPlaygroundFocusArea的示例如下所示:

<com.android.car.ui.FocusArea
       android:layout_margin="16dp"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">
       <EditText
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:singleLine="true">
       </EditText>
   </com.android.car.ui.FocusArea>

FocusArea工作原理如下:

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

如果您在布局中不包含任何FocusAreas ,则将根视图视为隐性焦点区域。用户无法在应用程序中推动导航。取而代之的是,它们将旋转所有可集中的视图,这可能足以进行对话。

Focusarea定制

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

  • android:nextFocusForward允许应用程序开发人员在焦点区域中指定旋转顺序。这是用于控制键盘导航的选项卡顺序的相同属性。请勿使用此属性来创建循环。而是使用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汽车,在Android 12中弃用app:nudgeShortcut and app:nudgeShortcutDirection仅支持一个Nudge快捷方式。

  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包含一个长列表,然后是浮动动作按钮,如下示例:

轻微捷径
图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 s,则应改用CarUiRecyclerView s。这样可以确保您的UI与其他人一致,因为OEM的自定义适用于所有CarUiRecyclerView s。

如果列表中的元素都是可集中的,那么您不需要做任何其他事情。旋转导航将焦点通过列表中的元素和列表卷轴移动,以使新聚焦元素可见。

Android 11 QPR3,Android 11 Car,Android 12
如果混合了聚焦和不可调节的元素,或者所有元素都无法关注,则可以启用旋转滚动,这使用户可以使用旋转控制器逐渐浏览列表,而无需跳过无法调用的项目。要启用旋转滚动,请将app:rotaryScrollEnabled属性为true

Android 11 QPR3,Android 11 Car,Android 12
您可以在CarUiUtils中使用setRotaryScrollEnabled()方法在包括AV CarUiRecyclerView在内的任何可滚动视图中启用旋转滚动。如果这样做,则需要:

  • 使可滚动视图可集中到可以集中精力上,以便何时看到其专注的后代观点何时可见,
  • 通过调用setDefaultFocusHighlightEnabled(false) ,禁用默认焦点亮点在可滚动视图上
  • 通过调用setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS)请确保可滚动视图在其后代之前关注。
  • 使用SOURCE_ROTARY_ENCODERAXIS_VSCROLLAXIS_HSCROLL聆听MotionEvents,以指示滚动和方向的距离(通过符号)。

当在CarUiRecyclerView上启用旋转滚动,并且用户旋转到没有可聚焦视图的区域时,滚动条会从灰色变为蓝色,好像是指向滚动条的聚焦。如果愿意,您可以实现类似的效果。

动态因素与鼠标上的滚动轮生成的动态相同,除了源外。

直接操纵模式

通常,轻推和旋转在用户界面中导航,而中央按钮按下采取行动,尽管并非总是如此。例如,如果用户想调整警报量,他们可能会使用旋转控制器导航到卷滑块,按中间按钮,旋转控制器以调整警报量,然后按下后面的按钮返回导航。这称为直接操纵(DM)模式。在此模式下,旋转控制器用于直接与视图交互,而不是导航。

以两种方式之一实施DM。如果您只需要处理旋转,并且要操纵的视图响应ACTION_SCROLL_FORWARDACTION_SCROLL_BACKWARD AccessibilityEvent s适当地使用,请使用简单的机制。否则,使用高级机制。

简单的机制是系统窗口中唯一的选择。应用程序可以使用任何一种机制。

简单的机制

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 DirectManipulationHelper.enableDirectManipulationMode()呼叫DM Mode,在每种情况下都呼叫DM模式。要聆听这些事件,请执行以下操作之一:
    • 注册一个OnKeyListener
    • 或者,
    • 扩展视图,然后覆盖其dispatchKeyEvent()方法。
  2. 如果视图应处理裸露,应收听Nudge事件( KEYCODE_DPAD_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFTKEYCODE_DPAD_RIGHT )。
  3. 如果要处理旋转,应听取MotionEvent s并在AXIS_SCROLL中获得旋转计数。做这件事有很多种方法:
    1. 注册OnGenericMotionListener
    2. 扩展视图并覆盖其dispatchTouchEvent()方法。
  4. 为避免陷入DM模式,当视图所属于的片段或活动不是交互式时,必须退出DM模式。
  5. 应提供一个视觉提示,以表明视图处于DM模式。

一个自定义视图的示例,该视图使用DM模式泛滥和缩小地图如下:

/** Whether this view is in DM mode. */
private boolean mInDirectManipulationMode;

/** Initializes the view. Called by the constructors. */ private void init() { setOnKeyListener((view, keyCode, keyEvent) -> { boolean isActionUp = keyEvent.getAction() == KeyEvent.ACTION_UP; switch (keyCode) { // Always consume KEYCODE_DPAD_CENTER and KEYCODE_BACK events. case KeyEvent.KEYCODE_DPAD_CENTER: if (!mInDirectManipulationMode && isActionUp) { mInDirectManipulationMode = true; DirectManipulationHelper.enableDirectManipulationMode(this, true); setSelected(true); // visually indicate DM mode } return true; case KeyEvent.KEYCODE_BACK: if (mInDirectManipulationMode && isActionUp) { mInDirectManipulationMode = false; DirectManipulationHelper.enableDirectManipulationMode(this, false); setSelected(false); } return true; // Consume controller nudge events only when in DM mode. // When in DM mode, nudges pan the map. case KeyEvent.KEYCODE_DPAD_UP: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(0f, -10f); return true; case KeyEvent.KEYCODE_DPAD_DOWN: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(0f, 10f); return true; case KeyEvent.KEYCODE_DPAD_LEFT: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(-10f, 0f); return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (!mInDirectManipulationMode) return false; if (isActionUp) pan(10f, 0f); return true; // Don't consume other key events. default: return false; } });
// When in DM mode, rotation zooms the map. setOnGenericMotionListener(((view, motionEvent) -> { if (!mInDirectManipulationMode) return false; float scroll = motionEvent.getAxisValue(MotionEvent.AXIS_SCROLL); zoom(10 * scroll); return true; })); }
@Override public void onPause() { if (mInDirectManipulationMode) { // To ensure that the user doesn't get stuck in DM mode, disable DM mode // when the fragment is not interactive (e.g., a dialog shows up). mInDirectManipulationMode = false; DirectManipulationHelper.enableDirectManipulationMode(this, false); } super.onPause(); }

可以在RotaryPlayground项目中找到更多示例。

ActivityView

使用ActivityView时:

  • ActivityView不应集中注意力。
  • Android 11 QPR3,Android 11汽车,在Android 11中弃用
    ActivityView的内容必须包含FocusParkingView作为第一个焦点视图,其app:shouldRestoreFocus属性必须为false
  • ActivityView的内容不应具有android:focusByDefault视图。

对于用户,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采取了行动(例如倒带),并安排在延迟后运行。

触摸模式

用户可以使用旋转控制器通过使用旋转控制器或通过触摸屏幕来通过两种方式与Head单位进行交互。使用旋转控制器时,突出显示了一个可聚焦视图之一。触摸屏幕时,不会出现焦点。用户可以随时在这些输入模式之间切换:

  • 旋转→触摸。当用户触摸屏幕时,焦点亮点会消失。
  • 触摸→旋转。当用户轻推,旋转或按下中心按钮时,将出现焦点重点。

背面和主页按钮对输入模式没有影响。

Android现有的触摸模式概念上的旋转式背包。您可以使用View.isInTouchMode()来确定用户正在使用的输入模式。您可以使用OnTouchModeChangeListener来收听更改。虽然这可以用来自定义当前输入模式的用户界面,但要避免任何重大更改,因为它们可能会令人不安。

故障排除

在设计用于触摸的应用程序中,通常具有可嵌套的聚焦视图。例如, ImageButton周围可能会有一个FrameLayout ,这两者都是可集中的。这对触摸没有损害,但它可能会导致旋转的用户体验差,因为用户必须两次旋转控制器才能移动到下一个交互式视图。为了获得良好的用户体验,Google建议您使外视图或内部视图可聚焦,但不能两者兼而有之。

如果按下旋转控制器时按钮或开关失去焦点,则可能适用以下条件之一:

  • 由于按下按钮,该按钮或开关被禁用(短暂或无限期)。无论哪种情况,都有两种解决方案:
    • android:enabled状态启用为true ,并使用自定义状态按下按钮或开关,如自定义状态所述。
    • 使用容器包围按钮或开关,然后使容器可聚焦而不是按钮或开关。 (点击侦听器必须在容器上。)
  • 按钮或开关正在替换。例如,按下按钮或切换开关时采取的操作可能会触发可用的动作,从而导致新按钮替换现有按钮。有两种解决方案的方法:
    • 而不是创建新按钮或开关,而是设置现有按钮或开关的图标和/或文本。
    • 如上所述,在按钮或开关周围添加一个可聚焦容器。

旋转式播放

RotaryPlayground是用于旋转的参考应用程序。使用它来学习如何将旋转功能集成到应用程序中。 RotaryPlayground包含在模拟器构建中,并在运行Android Automotive OS(AAOS)的设备中包含。

  • RotaryPlayground库: packages/apps/Car/tests/RotaryPlayground/
  • 版本:Android 11 QPR3,Android 11车和Android 12

RotaryPlayground应用显示左侧的以下选项卡:

  • 牌。测试围绕焦点区域导航,跳过无法调用的元素和文本输入。
  • 直接操纵。支持简单和高级直接操纵模式的测试小部件。此选项卡专门用于应用程序窗口中的直接操作。
  • 系统UI操纵。测试小部件支持在系统窗口中仅支持简单直接操纵模式的直接操作的小部件。
  • 网格。用滚动测试Z-Pattern旋转导航。
  • 通知。测试裸露的通知进出。
  • 滚动。测试滚动浏览焦点和不可调节内容的混合。
  • WebView。通过WebView中的链接进行测试。
  • 自定义FocusArea测试FocusArea定制:
    • 环绕。
    • android:focusedByDefault and app:defaultFocus
    • 明确的轻推目标。
    • 轻捷捷径。
    • FocusArea无视图。