在 Wear OS 中创建您的第一个卡片

滑动卡片,实现查看时间、天气,启动计时器等功能。

观看上面的动画(卡片演示)。注意:GIF 动画只会播放一次。如果您没看到动画,请重新加载页面。

您可以通过 Wear OS 卡片轻松访问用户完成各种任务所需的信息和操作。只需在表盘上轻轻滑动一下,用户即可查看最新的天气预报或启动定时器。

开发卡片的方式与编写应用有些不同。

卡片作为系统界面的一部分运行,而不是在其专属的应用容器中运行。这意味着它不遵循您可能熟悉的一些 Android 编程概念,如 activity 和 XML 布局。

我们改为使用 Service 来描述卡片的布局和内容。系统界面随后会根据需要渲染卡片。

在此 Codelab 中,您将学习如何从头开始编写自己的 Wear OS 卡片。

学习内容

  • 创建卡片
  • 在设备上测试卡片
  • 设计卡片布局
  • 添加图像
  • 添加互动(点按)

构建内容

您将构建一个自定义卡片,其中会显示距离每日目标所剩的步数。它包含一个复杂的布局,可帮助您了解不同的布局元素和容器。

以下是完成此 Codelab 后,您构建的卡片的样子。

c6e1959693cded21.png

前提条件

在此步骤中,您将设置环境并下载入门级项目

所需条件

  • 最新的稳定版 Android Studio
  • Wear OS 设备或模拟器(第一次使用?这里介绍了设置方法。)

下载代码

如果您已安装 git,则只需运行以下命令即可克隆此代码库中的代码。如需检查是否已安装 git,请在终端或命令行中输入 git --version,并验证其是否正确执行。

git clone https://github.com/googlecodelabs/wear-tiles.git
cd wear-tiles

如果您未安装 git,可以点击下方按钮下载此 Codelab 的全部代码:

下载 ZIP 文件

您可以通过更改工具栏中的运行配置,随时在 Android Studio 中运行其中任一模块。

8a2e49d6d6d2609d.png

在 Android Studio 中打开项目

  1. 在“Welcome to Android Studio”窗口中,选择 1f5145c42df4129a.png Open an Existing Project
  2. 选择文件夹 [Download Location]
  3. Android Studio 导入项目后,请测试您可以在 Wear OS 模拟器或实体设备上运行 startfinished 模块。
  4. start 模块应如以下屏幕截图所示。您将在其中完成所有工作。

c72e8870facd8458.png

探索起始代码

  • build.gradle 中包含基本的应用配置。它包含创建卡片所需的依赖项。
  • main > AndroidManifest.xml 包含将应用标记为 Wear OS 应用的必要代码。我们将在本 Codelab 期间查看此文件。
  • main > GoalsRepository.kt 包含虚构代码库类,用于异步检索用户当天设置的随机步数。
  • main > GoalsTileService.kt 包含用于创建卡片的样板。我们将在此文件中完成大部分工作。
  • debug > AndroidManifest.xml 包含 TilePreviewActivity 的 activity 元素,以便我们可以预览卡片。
  • debug > TilesPreviewActivity.kt 包含我们将用于预览卡片的 activity。

首先,打开 start 模块中的 GoalsTileService。如您所见,此类扩展了 TileProviderService

TileProviderService 是 Tiles 库的一部分,提供用于编写卡片的方法:

  • onTileRequest() - 在系统请求卡片时予以创建。
  • onResourcesRequest() - 提供 onTileRequest() 中返回的卡片所需的任何图像。

我们使用协程来异步处理这些方法。如需详细了解协程,请参阅 Android 协程文档

首先,我们创建简单的“Hello, world”卡片。

在进入正题之前,请注意我们在文件的开头定义了一些常量,我们将在整个 Codelab 中用到这些常量。如需查看这些常量,您可以搜索“TODO: Review Constants”。它们定义了包括从资源版本到 dp 值(内边距、尺寸等)、sp 值(文本)、标识符等在内的诸多内容。

接下来我们开始编码。

GoalsTileService.kt 中,搜索“TODO: Build a Tile”,并将整个

onTileRequest() 实现替换为以下代码。

第 1 步

// TODO: Build a Tile.
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    Tile.builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them using onResourcesRequest() and match them with this version number.
        .setResourcesVersion(RESOURCES_VERSION)

        // Creates a timeline to hold one or more tile entries for a specific time periods.
        .setTimeline(
            Timeline.builder().addTimelineEntry(
                TimelineEntry.builder().setLayout(
                    Layout.builder().setRoot(
                        Text.builder().setText("Hello, world!")
                    )
                )
            )
        ).build()
}

onTileRequest() 方法预计会返回 Future,我们使用 serviceScope.future { ... } 将协程转换为 Java ListenableFuture

除此之外,您只需在协程中创建 Tile,一起来试试吧。

onTileRequest() 中,我们使用构建器模式来创建“Hello, world!”卡片。仔细阅读代码块中的注解,理解代码执行的相关操作。

首先,我们设置一个资源版本。此方法在载荷中返回的资源版本必须onResourcesRequest() 在载荷中返回的资源版本相匹配,无论实际是否使用了任何资源都是如此。

当系统调用 onResourcesRequest() 来获取这些图形时,这种方法可以将图形与相应正确版本进行匹配。

不过,对于这个简单的“Hello, world”卡片,我们不提供任何图形。

接下来,我们创建一个 Timeline

Timeline 中包含一个或多个 TimelineEntry 实例。每个实例都描述特定时间间隔要展示的相应布局。您可以创建代表未来时间的多个 TimelineEntry 值,系统将会在相应未来时间渲染相应布局。

fbb666b722376749.png

您可以在时间轴指南中详细了解如何使用时间轴。

在本例中,我们只声明了一个 TimelineEntry 实例,因为我们始终只需要一个卡片。然后我们使用 setLayout 设置布局。

根布局可以由一个或多个复杂布局构成,但在这一步中,我们将创建一个简单的 Text 布局元素,用于显示“Hello, world!”。

大功告成!

现在我们已创建一个非常简单的卡片,接下来我们在 Activity 中预览该卡片。

如需在 activity 中预览此卡片,请在应用的 debug 源文件夹中打开 TilePreviewActivity

搜索“TODO: Review creation of Tile for Preview”并在其后添加以下代码。

第 2 步

// TODO: Review creation of Tile for Preview.
tileManager = TileManager(
    context = this,
    component = ComponentName(this, GoalsTileService::class.java),
    parentView = rootLayout
)
tileManager.create()

这一步大多数内容非常容易理解。

您创建了 TileManager,将其用于预览卡片并设置上下文、组件(我们一直在开发的卡片服务类)以及要在其中插入卡片的父视图。

然后,我们使用 create() 创建卡片。

您还会看到我们会用 tileManager.close()onDestroy() 中做一些清理工作。

现在,在 Wear OS 模拟器或设备上运行您的应用(请务必选择 start 模块)。您应该会看到屏幕内居中显示着“Hello, world!”:

b9976e1073554422.png

我们构建的基本布局已能正常运行,现在我们对其进行扩展,创建更复杂的卡片。我们的最终目标布局如下所示:

c6e1959693cded21.png

使用 Box 替换根布局

使用以下代码替换整个 onTileRequest 方法(包含“Hello, world!”文本标签)。

第 3 步

// TODO: Build a Tile.
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    // Retrieves progress value to populate the Tile.
    val goalProgress = GoalsRepository.getGoalProgress()
    // Retrieves font styles for any text in the Tile.
    val fontStyles = FontStyles.withDeviceParameters(requestParams.deviceParameters)

    // Creates Tile.
    Tile.builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them using onResourcesRequest() and match them with this version number.
        .setResourcesVersion(RESOURCES_VERSION)
        // Creates a timeline to hold one or more tile entries for a specific time periods.
        .setTimeline(
            Timeline.builder().addTimelineEntry(
                TimelineEntry.builder().setLayout(
                    Layout.builder().setRoot(
                        // Creates the root [Box] [LayoutElement]
                        layout(goalProgress, fontStyles)
                    )
                )
            )
        ).build()
}

仔细阅读代码块中的注解,理解代码执行的相关操作。我们尚未定义 layout(),因此无需担心出错。

在该方法的第一段代码中,我们首先从(虚构)代码库中检索实际目标进度。

我们还检索传递到 onTileRequest()deviceParameters,稍后我们将使用它来构建文本标签的字体样式。

下一段代码和之前创建卡片的代码一样。您可能会注意到,几乎所有代码都相同!

事实上,我们仅更改了一行内容,将根布局设置为 layout(goalProgress, fontStyles)

如前所述,您应该会在方法名称下方看到一条红线,因为相应方法不存在!

通常我们会将布局分解为若干个逻辑代码段,并在各自的方法中定义相应逻辑,以防止代码深度嵌套。接下来,我们将定义布局方法!

搜索“TODO: Create root Box layout and content”并添加以下代码。

第 4 步

    // TODO: Create root Box layout and content.
    // Creates a simple [Box] container that lays out its children one over the other. In our
    // case, an [Arc] that shows progress on top of a [Column] that includes the current steps
    // [Text], the total steps [Text], a [Spacer], and a running icon [Image].
    private fun layout(goalProgress: GoalProgress, fontStyles: FontStyles) =
        Box.builder()
            // Sets width and height to expand and take up entire Tile space.
            .setWidth(expand())
            .setHeight(expand())

            // Adds an [Arc] via local function.
            .addContent(progressArc(goalProgress.percentage))

            // TODO: Add Column containing the rest of the data.
            // TODO: START REPLACE THIS LATER
            .addContent(
                Text.builder()
                    .setText("REPLACE ME!")
                    .setFontStyle(fontStyles.display3())
            )
            // TODO: END REPLACE THIS LATER

            .build()

仔细阅读代码块中的注解,理解代码执行的相关操作。我们尚未定义 progressArc(),因此您不必担心针对该方法或 setRoot() 的调用出现任何错误。

我们的布局方法包含一个简单 Box 容器,该容器控制其子元素的布局方式。在该 Box 中,我们添加了一些内容。在本例中,我们调用了尚不存在的方法!

我们将再次遵循分解复杂布局的方法,以便代码更具可读性。

您可能会注意到,靠近代码底部还有一些其他 TODO 注解(位于 build() 调用上方)。稍后在此 Codelab 中,我们会探讨这些内容。

现在,我们将修复此错误并定义我们的本地方法,用于最终声明我们要实现的弧线。

创建 ArcLine

首先,我们需要围绕屏幕边缘创建一个弧线,随着用户步数的增加,弧线会相应变长。

Tiles API 提供多种 Arc 容器选项。我们将使用 ArcLine,沿着 Arc 渲染一条曲线。

现在,我们来定义该函数,以便消除前面提及的烦人错误。

找到“TODO: Create a function that constructs an Arc representation of the current step progress”并将其替换为以下代码。

第 5 步

    // TODO: Create a function that constructs an Arc representation of the current step progress.
    // Creates an [Arc] representing current progress towards steps goal.
    private fun progressArc(percentage: Float) = Arc.builder()
        .addContent(
            ArcLine.builder()
                // Uses degrees() helper to build an [AngularDimension] which represents progress.
                .setLength(degrees(percentage * ARC_TOTAL_DEGREES))
                .setColor(argb(ContextCompat.getColor(this, R.color.primary)))
                .setThickness(PROGRESS_BAR_THICKNESS)
        )
        // Element will start at 12 o'clock or 0 degree position in the circle.
        .setAnchorAngle(degrees(0.0f))
        // Aligns the contents of this container relative to anchor angle above.
        // ARC_ANCHOR_START - Anchors at the start of the elements. This will cause elements
        // added to an arc to begin at the given anchor_angle, and sweep around to the right.
        .setAnchorType(ARC_ANCHOR_START)
        .build()

仔细阅读代码块中的注解,理解代码执行的相关操作。(注意:您可能会看到 degrees()argb()ContextCompat 都报错。如果确实如此,请直接点击相应代码导入卡片版本。)

在这段代码中,我们会返回 Arc.Builder,其中的 ArcLine 代表距离步数目标的进度。

我们将长度设置为已完成进度占目标的百分比(转换为角度),并设置颜色和粗细。

接下来,指定我们期望的弧线起始位置,以及相应锚点类型。锚点有很多不同的选项,在 Codelab 后,您可以随意尝试所有这些选项。

太棒了,我们已完成本部分的操作!

我们来看看实际用例。再次运行应用后,您应该会看到如下内容:

ad79daf115d3b6a4.png

添加 Column 容器

现在我们的卡片有了不错的进度指示器,接下来我们添加一些合适的文本。

由于我们要添加多个文本字段(稍后还要添加图像),因此我们希望这些项位于屏幕中央的 Column 下。

Column诸多卡片布局容器中的一种,它允许我们垂直排列其中的子元素。

找到“TODO: Add Column containing the rest of the data”并将临时文本内容替换为以下代码。这些代码就是从 TODO: START REPLACE THIS LATERTODO: END REPLACE THIS LATER 所需的所有内容。

请注意,不要移除原始代码块末尾的 build() 调用。

第 6 步

            // TODO: Add Column containing the rest of the data.
            // Adds a [Column] containing the two [Text] objects, a [Spacer], and a [Image].
            .addContent(
                Column.builder()
                    // Adds a [Text] using local function.
                    .addContent(
                        currentStepsText(goalProgress.current.toString(), fontStyles)
                    )
                    // Adds a [Text] using local function.
                    .addContent(
                        totalStepsText(
                            resources.getString(R.string.goal, goalProgress.goal),
                            fontStyles
                        )
                    )
                    // TODO: Add Spacer and Image representations of our step graphic.
                    // DO LATER
            )

仔细阅读代码块中的注解,理解代码执行的相关操作。

我们将创建一个列,并在其中添加两部分内容。现在,我们会收到两个错误!

能想到是哪里出问题了吗?

我们再次遵循分解复杂布局的方法(并且可以忽略 DO LATER TODO)。

我们来一起修正这些错误。

添加文本元素

找到“TODO: Create functions that construct/stylize Text representations of the step count & goal”并在其下方添加以下代码。

第 7 步

    // TODO: Create functions that construct/stylize Text representations of the step count & goal.
    // Creates a [Text] with current step count and stylizes it.
    private fun currentStepsText(current: String, fontStyles: FontStyles) = Text.builder()
        .setText(current)
        .setFontStyle(fontStyles.display2())
        .build()

    // Creates a [Text] with total step count goal and stylizes it.
    private fun totalStepsText(goal: String, fontStyles: FontStyles) = Text.builder()
        .setText(goal)
        .setFontStyle(fontStyles.title3())
        .build()

仔细阅读代码块中的注解,理解代码执行的相关操作。

这段代码非常易于理解。我们使用两个不同的函数来创建单独的 Text 布局元素。

您可能已经猜到,Text 布局元素会渲染文本字符串(可选择是否换行)。

我们根据此类中前面定义的常量设置字号,最后,根据我们在 onTileRequest() 中检索到的样式设置字体样式。

现在,我们能渲染一些文字。一起来看看我们的卡片现在的样子。运行函数,您应该会看到如下内容。

9eaca483c7e51f38.png

将图像名称添加到 onTileRequest() 中

在界面设计的最后部分,我们将添加一个图像。

找到“TODO: Add Spacer and Image representations of our step graphic”,并将 DO LATER 替换为以下代码(确保不要删除末尾的“)”字符)。

另请注意,不要移除原始代码块末尾的 build() 调用。

第 8 步

                    // TODO: Add Spacer and Image representations of our step graphic.
                    // Adds a [Spacer].
                    .addContent(Spacer.builder().setHeight(VERTICAL_SPACING_HEIGHT))
                    // Adds an [Image] using local function.
                    .addContent(startRunButton())

仔细阅读代码块中的注解,理解代码执行的相关操作。

首先,我们添加一个 Spacer 布局元素,指定元素之间的内边距,在本例中为与后面的图像的间距。

接下来,我们将为 Image 布局元素添加内容,以渲染图像,但根据错误您可以猜到,我们会在单独的本地函数中定义上述语句。

找到“TODO: Create a function that constructs/stylizes a clickable Image of a running icon”并将其替换为以下代码。

第 9 步

    // TODO: Create a function that constructs/stylizes a clickable Image of a running icon.
    // Creates a running icon [Image] that's also a button to refresh the tile.
    private fun startRunButton() =
        Image.builder()
            .setWidth(BUTTON_SIZE)
            .setHeight(BUTTON_SIZE)
            .setResourceId(ID_IMAGE_START_RUN)
            .setModifiers(
                Modifiers.builder()
                    .setPadding(
                        Padding.builder()
                            .setStart(BUTTON_PADDING)
                            .setEnd(BUTTON_PADDING)
                            .setTop(BUTTON_PADDING)
                            .setBottom(BUTTON_PADDING)
                    )
                    .setBackground(
                        Background.builder()
                            .setCorner(Corner.builder().setRadius(BUTTON_RADIUS))
                            .setColor(argb(ContextCompat.getColor(this, R.color.primaryDark)))
                    )
                    // TODO: Add click (START)
                    // DO LATER
                    // TODO: Add click (END)
            )
            .build()

仔细阅读代码块,理解代码执行的相关操作。

与以往一样,我们暂时忽略 DO LATER TODO

大部分代码非常容易理解。我们使用类的顶部定义的常量设置了各种维度和样式。如需详细了解修饰符,请点击此处

需要重点关注的是 .setResourceId(ID_IMAGE_START_RUN) 调用。

我们将把图像名称设置为在类的顶部定义的常量。

最后,我们需要将相应常数名称映射到应用中的实际图像。

将图像映射关系添加到 onResourcesRequest() 中

卡片无法访问应用的任何资源。这意味着您无法将 Android 图像 ID 传递给图像布局元素,期望应用对其进行解析。因此,您需要替换 onResourcesRequest() 方法并手动提供任何资源。

onResourcesRequest() 方法中,您可以通过以下两种方式提供图像:

使用 setAndroidResourceByResId() 将我们之前用于图像的名称映射到实际图像。

找到“TODO: Supply resources (graphics) for the Tile”,并将整个现有方法替换为以下代码。

第 10 步

    // TODO: Supply resources (graphics) for the Tile.
    override fun onResourcesRequest(requestParams: ResourcesRequest) = serviceScope.future {
        Resources.builder()
            .setVersion(RESOURCES_VERSION)
            .addIdToImageMapping(
                ID_IMAGE_START_RUN,
                ImageResource.builder()
                    .setAndroidResourceByResid(
                        AndroidImageResourceByResId.builder()
                            .setResourceId(R.drawable.ic_run)
                    )
            )
            .build()
    }

仔细阅读代码块,理解代码执行的相关操作。

您可能还记得,我们在开始使用 onTileRequest() 时设置了资源版本号。

这一步中,我们在资源构建器中设置同个资源版本号,将卡片与正确的资源相匹配。

接下来,我们需要使用 addIdToImageMapping() 方法,将我们创建的任何 Image 布局元素映射到实际图像。您可以看到,我们使用前面用到的同个常量名称 ID_IMAGE_START_RUN,现在我们将设置要在 .setResourceId(R.drawable.ic_run 中返回的具体可绘制资源)

现在,运行应用,您应该看到已完成的界面!

c6e1959693cded21.png

最后一步是为卡片添加点击操作。此操作可以在 Wear OS 应用中打开 Activity,在此 Codelab 中,操作将触发对卡片本身的更新。

找到“TODO: Add click (START)”并将 DO LATER 注解替换为以下代码。

请注意,不要移除原始代码块末尾的 build() 调用。

第 11 步

                    // TODO: Add click (START)
                    .setClickable(
                        Clickable.builder()
                            .setId(ID_CLICK_START_RUN)
                            .setOnClick(ActionBuilders.LoadAction.builder())
                    )
                    // TODO: Add click (END)

仔细阅读代码块,理解代码执行的相关操作。

向布局元素添加 Clickable 修饰符,您可以针对用户点按该布局元素的操作设置响应。点击事件的响应支持您执行两项操作:

在本例中,我们设置一个可点击修饰符,但使用 LoadAction 通过简单的代码行 ActionBuilders.LoadAction.builder() 来刷新卡片本身。这会触发对 onTileRequest() 的调用,但会传递我们设置的 ID,即 ID_CLICK_START_RUN

如果有必要,我们可以检查传递给 onTileRequest()上一个可点击 ID,并根据该 ID 渲染不同的卡片。此时的代码如下所示:

第 12 步

// Example of getting the clickable Id
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {

    if (requestParams.state.lastClickableId == ID_CLICK_START_RUN) {
        // Create start run tile...
    } else {
        // Create default tile...
    }
}

在本例中,我们不会这样做。我们只需刷新卡片(并从模拟数据库中拉取一个新值即可)。

如需了解与卡片互动的其他选项,请参阅我们的指南

现在再次运行卡片。点击按钮时,您会看到步数值发生变化!

e15bba88abc0d832.png

您可能已经猜到,我们在清单中的某处定义了 service,我们前面一直在修改该组件。

找到“TODO: Review service”,您应该会看到以下代码。

您无需执行任何步骤,只需查看现有代码即可。

第 12 步

<!-- TODO: Review service -->
<service
   android:name="com.example.wear.tiles.GoalsTileService"
   android:label="@string/fitness_tile_label"
   android:description="@string/tile_description"
   android:icon="@drawable/ic_run"
   android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
   <intent-filter>
       <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
   </intent-filter>

   <!-- The tile preview shown when configuring tiles on your phone -->
   <meta-data
       android:name="androidx.wear.tiles.PREVIEW"
       android:resource="@drawable/tile_goals" />
</service>

仔细阅读代码块,理解代码执行的相关操作。

这与常规 service 类似,但我们为卡片添加了几个特定元素:

  1. 用于绑定卡片提供程序的权限
  2. 将 service 注册为卡片提供程序的 intent 过滤器
  3. 用于指定可在手机上查看的预览图像的其他元数据

恭喜!您已了解如何构建适用于 Wear OS 的卡片!

后续操作

查看其他 Wear OS Codelab:

深入阅读