在 Wear OS 建立第一個資訊方塊

1. 簡介

資訊方塊會在時間、天氣和計時器之間切換。

請觀看上方的動畫 (資訊方塊示範)。注意:動畫 GIF 只會播放一次。如果您錯過動畫,請重新載入頁面。

Wear OS 資訊方塊可讓使用者輕鬆存取處理各種事務所需的資訊和動作。只要從錶面向上滑動,使用者就能查詢最新天氣預報或啟動計時器。

開發資訊方塊的方式與編寫 Android 應用程式稍有不同。

資訊方塊是系統 UI 的一部分,而不是在其專屬應用程式容器中執行。這表示它與您熟悉的 Android 程式設計概念有所不同,例如「活動」和 XML 版面配置。

相反,我們會用 Service 來描述資訊方塊的版面配置和內容,系統 UI 會隨之視需要算繪資訊方塊。

在本程式碼研究室中,我們會說明如何從頭開始編寫自己的 Wear OS 資訊方塊。

學習目標

  • 建立資訊方塊
  • 在裝置上測試資訊方塊
  • 設計資訊方塊版面配置
  • 加入圖片
  • 新增互動 (輕觸)

建構目標

你會建立一個自訂資訊方塊,顯示距離每日目標所剩的步數。其中包含複雜的版面配置,可協助你瞭解不同的版面配置元素和容器。

完成本程式碼研究室後,您建立的資訊方塊會如下所示:

d1e662b43b49467c.png

必要條件

2. 開始設定

在這個步驟中,您會設定環境並下載範例專案。

您需要準備的項目

  • 最新的 Android Studio 穩定版
  • Wear OS 裝置或模擬器 (剛開始使用嗎?這裡有設定說明。)

下載程式碼

如果您已安裝 Git,只要執行下列指令即可複製這個存放區的程式碼。如要檢查 Git 是否已安裝完成,請在終端機或指令列中輸入 git --version 類型,並確認其可正確執行。

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

如果您沒有 Git,可以點選下方按鈕,以下載這個程式碼研究室的所有程式碼:

您隨時都可變更工具列中的執行設定,以在 Android Studio 中執行任一模組。

b059413b0cf9113a.png

在 Android Studio 中開啟專案

  1. 在「Welcome to Android Studio」(歡迎使用 Android Studio) 視窗中,選取 c01826594f360d94.png「Open an Existing Project」(開啟現有專案)
  2. 選取資料夾 [Download Location]
  3. Android Studio 匯入專案後,請測試是否能在 Wear OS 模擬器或實體裝置上執行 startfinished 模組。
  4. start 模組應如以下螢幕截圖所示,您會在該處執行所有工作。

90315a4e3553335c.png

探索範例程式碼

  • build.gradle 包含基本的應用程式設定。其中包含建立資訊方塊所需的依附元件。
  • main > AndroidManifest.xml 包含將其標示為 Wear OS 應用程式所需的部分。我們會在程式碼研究室中檢查這個檔案。
  • main > GoalsRepository.kt 包含假的存放區類別,該類別會以非同步方式擷取使用者今天設定的隨機步數。
  • main > GoalsTileService.kt 包含用來建立資訊方塊的樣板,我們會在此檔案中完成大部分工作。
  • debug > AndroidManifest.xml 包含 TilePreviewActivity 的活動元素,因此可以預覽資訊方塊。
  • debug > TilesPreviewActivity.kt 包含用來預覽資訊方塊的活動。

3. 新增資訊方塊

首先,請開啟 start 模組中的 GoalsTileService。如您所見,這個類別擴展了 TileService

TileService 是資訊方塊資料庫的一部分,提供了用來編寫資訊方塊的方法:

  • onTileRequest():在系統要求時建立資訊方塊。
  • onResourcesRequest():提供 onTileRequest() 中傳回之資訊方塊所需的圖片。

我們使用協同程式來處理這些方法的非同步性質。如要進一步瞭解協同程式,請參閱 Android 協同程式說明文件

首先,請建立簡單的「Hello, world」資訊方塊。

在進入正題之前,請注意我們在檔案開頭定義了一些常數,我們會在本程式碼研究室中用到這些常數。只要搜尋「TODO: Review Constants」就能查看這些常數,它們定義包括資源版本到 dp 值 (邊框間距、尺寸等)、sp 值 (文字)、ID 在內的各種內容。

現在我們開始寫程式。

GoalsTileService.kt 中搜尋「TODO: Build a Tile」,然後將整個

onTileRequest() 實作取代為下列程式碼。

步驟 1

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

    // Creates Tile.
    Tile.Builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them via 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()
                                )
                                .build()
                        )
                        .build()
                )
                .build()
        ).build()
}

onTileRequest() 方法預計會傳回 Future,我們使用 serviceScope.future { ... } 將協同程式轉換成 Java ListenableFuture

但除此之外,你只需要在協同程式中建立 Tile,一起試試吧。

onTileRequest() 中,我們使用建構工具模式來建立「Hello, world!」資訊方塊。請詳閱程式碼區塊中的註解,瞭解執行程式碼的相關操作。

首先設定資源版本。此方法在酬載中傳回的資源版本必須onResourcesRequest() 酬載中傳回的資源版本相符,不論是否實際使用任何資源皆是如此。

系統呼叫 onResourcesRequest() 來取得這些圖像時,這種方法可以將這些圖像與正確版本互相比對。

不過,簡單的「Hello, world」資訊方塊中並沒有任何圖像。

接下來,請建立 Timeline

Timeline 包含一或多個 TimelineEntry 執行個體。每個執行個體都描述了特定時間間隔的版面配置。您可以建立多個日後發生的 TimelineEntry 值,之後,系統就會自動算繪這些值。

a318695f73cc337a.png

如要進一步瞭解如何使用時間軸,請參閱時間軸指南

在本例子中,由於我們始終只需要一個資訊方塊,所以只宣告了一個 TimelineEntry 執行個體。然後使用 setLayout 設定版面配置。

根層級可以由一或多個複雜的版面配置所組成,但是在這個步驟中,我們會建立簡單的 Text 版面配置元素,用來顯示「Hello, world!」。

這樣就大功告成了!

現在,我們已建立了非常簡單的資訊方塊,讓我們在 Activity 中預覽。

如要在「活動」中預覽這個資訊方塊,請在應用程式的 debug 來源資料夾中開啟 TilePreviewActivity

搜尋「TODO: Review creation of Tile for Preview」,並在後面加入下列程式碼。

步驟 2

// TODO: Review creation of Tile for Preview.
tileUiClient = TileUiClient(
    context = this,
    component = ComponentName(this, GoalsTileService::class.java),
    parentView = rootLayout
)
tileUiClient.connect()

大部分內容都容易理解。

您建立了 TileUiClient,可用於預覽資訊方塊,並設定內容、元件 (我們一直處理的資訊方塊服務類別) 以及插入資訊方塊的父項檢視畫面。

然後,我們使用 connect() 建立資訊方塊。

你還會發現,我們使用 tileUiClient.close()onDestroy() 中進行一些清理作業。

現在,請在 Wear OS 模擬器或裝置上執行您的應用程式 (請務必選擇 start 模組)。畫面置中位置應會顯示「Hello, world!」:

e8743bcdc89dc459.png

4. 新增 Box 和 Arc

我們的基本版面配置已經可以正常運作,讓我們擴展版面配置,並建立更複雜的資訊方塊。我們的最終版面配置目標如下:

d1e662b43b49467c.png

使用 Box 取代根版面配置

GoalsTileService.kt 檔案中,使用下列程式碼取代 整個 onTileRequest 方法,其中包含「Hello, world!」文字標籤。您可以按照上一個步驟搜尋「TODO: Build a Tile」。

步驟 3

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

    // Retrieves progress value to populate the Tile.
    val goalProgress = GoalsRepository.getGoalProgress()
    // Retrieves device parameters to later retrieve font styles for any text in the Tile.
    val deviceParams = requestParams.deviceParameters!!

    // Creates Tile.
    Tile.Builder()
        // If there are any graphics/images defined in the Tile's layout, the system will
        // retrieve them via 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, deviceParams)
                                )
                                .build()
                        )
                        .build()
                )
                .build()
        ).build()
}

請詳閱程式碼區塊中的註解,瞭解執行程式碼的相關操作。我們尚未定義 layout(),所以不必擔心出錯。

在該方法的第一個程式碼中,我們會先從 (虛構) 存放區擷取實際目標進度。

我們也會擷取傳遞至 onTileRequest()deviceParameters,稍後用來建立文字標籤的字型樣式。

下一段程式碼同樣是建立資訊方塊。您會發現,所有的程式碼都大致相同!

事實上,我們只變更一行內容,將根設為 layout(goalProgress, deviceParameters)

如先前所述,方法名稱下方會顯示一條紅線,因為該方法並不存在!

我們通常會將版面配置分解為數個邏輯片段,並在各自的方法中定義這些邏輯,以避免出現深層巢狀程式碼。接下來,我們要定義版面配置方法!

搜尋「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, deviceParameters: DeviceParameters) =
        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(deviceParameters).build())
                    .build()
            )
            // TODO: END REPLACE THIS LATER

            .build()

請詳閱程式碼區塊中的註解,瞭解執行程式碼的相關操作。我們尚未定義 progressArc(),所以不必擔心該方法呼叫會出錯。

我們的版面配置方法含有簡易的 Box 容器,這是多種資訊方塊版面配置容器之一,可讓您排列子項元素。對於 Box 容器,子項會逐人配置。在該 Box 內新增一些內容。在本例子中,我們呼叫的方法並不存在!

我們會再次遵循分解複雜版面配置的方法,讓程式碼易於閱讀。

您可能會發現,靠近程式碼底部 (build() 呼叫上方) 還有一些其他 TODO。稍後,我們會在本程式碼研究室中探這些內容。

現在,我們要修正該錯誤並定義本機方法,最終宣告我們要尋找的弧線。

建立 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)
                .build()
        )
        // 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,其中 ArcLine 表示距離步驟目標的進度。

我們將長度設為已完成進度占目標的百分比 (已轉換為角度),並設定其顏色和粗細。

接下來,請指定我們希望的弧線起點,以及錨點類型。錨點有多種不同選項,您可以在程式碼研究室結束後隨意嘗試。

很好,我們已完成這個部分!

請觀看實際使用教學。再次執行應用程式,畫面應如下所示:

59c0d71b82348c93.png

5. 新增自訂文字和圖片

新增欄容器

現在資訊方塊設有一個不錯的進度指標,接下來請新增適當的文字。

由於我們要新增多個文字欄位 (之後還要新增圖片),因此我們希望這些項目顯示在畫面中央的 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] via local function.
                    .addContent(
                        currentStepsText(goalProgress.current.toString(), deviceParameters)
                    )
                    // Adds a [Text] via local function.
                    .addContent(
                        totalStepsText(
                            resources.getString(R.string.goal, goalProgress.goal),
                            deviceParameters
                        )
                    )
                    // TODO: Add Spacer and Image representations of our step graphic.
                    // DO LATER
                    .build()
            )

請詳閱程式碼區塊中的註解,瞭解執行程式碼的相關操作。

我們要建立一欄,並在其中新增兩段內容。我們現在收到兩個錯誤!

您認為問題出在哪裡?

我們再次採用分解複雜版面配置的方法 (我們可以忽略 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, deviceParameters: DeviceParameters) = Text.Builder()
        .setText(current)
        .setFontStyle(FontStyles.display2(deviceParameters).build())
        .build()

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

請詳閱程式碼區塊中的註解,瞭解執行程式碼的相關操作。

這段程式碼非常容易理解,我們會使用兩個不同的函式,來建立單獨的 Text 版面配置元素。

您可能已經猜到,Text 版面配置元素會轉譯為文字串 (可選擇是否換行)。

我們根據此類別先前定義的常數來設定字型大小,最後再根據從 onTileRequest() 擷取的樣式來設定字型樣式。

我們已有一些文字,一起看看資訊方塊現在的樣子。執行資訊方塊,畫面應如下所示。

72e01eef5526877a.png

6. 加入圖片

將圖片名稱新增至 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).build())
                    // Adds an [Image] via 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)
                            .build()
                    )
                    .setBackground(
                        Background.Builder()
                            .setCorner(Corner.Builder().setRadius(BUTTON_RADIUS).build())
                            .setColor(argb(ContextCompat.getColor(this, R.color.primaryDark)))
                            .build()
                    )
                    // TODO: Add click (START)
                    // DO LATER
                    // TODO: Add click (END)
                    .build()
            )
            .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()
                    )
                    .build()
            )
            .build()
    }

請詳閱程式碼區塊,瞭解執行程式碼的相關操作。

您應該還記得,我們在用 onTileRequest() 的第一步時,已經設定了資源版本號碼。

在這一步,我們在資源建構工具中設定相同的資源版本號碼,以比對資料方塊與正確的資源。

接下來,我們要使用 addIdToImageMapping() 方法,將我們建立的所有 Image 版面配置元素對應至實際圖片。您會發現,我們使用與先前一樣的常數名稱 (ID_IMAGE_START_RUN);現在要設定 .setResourceId(R.drawable.ic_run) 中需要傳回的特定可繪項目。

現在,請執行應用程式,您應會看到完成的使用者介面!

d1e662b43b49467c.png

7. 新增點擊監聽器

最後一步是為資訊方塊新增點擊動作。這樣做可以在 Wear OS 應用程式中開啟 Activity,以程式碼研究室為例,此操作只會觸發資訊方塊本身的更新。

找出「TODO: Add click (START)」,並以下列程式碼取代 DO LATER 註解。

請注意,請勿移除原始程式碼結尾的 build() 呼叫。

步驟 11

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

請詳閱程式碼區塊,瞭解執行程式碼的相關操作。

Clickable 修飾詞新增到版面配置元素,即可在使用者輕觸該版面配置元素時做出回應。做為點擊事件的回應,您可以執行下列兩項操作:

在本例子中,我們設定一個可點擊的修飾詞,但使用 LoadAction 透過簡單的 ActionBuilders.LoadAction.builder() 程式碼行來重新整理資訊方塊本身。這會觸發對 onTileRequest() 的呼叫,但會傳遞先前設定的 ID,即 ID_CLICK_START_RUN

如有需要,我們可以傳遞至 onTileRequest()最後一個可點擊 ID,並根據該 ID 算繪不同的資訊方塊。程式碼應如下所示:

// 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...
    }
}

在本例子中,我們不會這麼做。我們只要重新整理資訊方塊 (並從模擬資料庫中提取新的值)。

如要進一步瞭解與資訊方塊互動的其他選項,請參閱我們的指南

現在請再次執行資訊方塊。點選按鈕時,您就會看到步數值發生變化!

b2bb82d690d34c0.png

8. 查看資訊清單

您或許已經猜到,我們在資訊清單中的某處定義了一直在編輯的服務。

AndroidManifest.xml 檔案中找出「TODO: Review service」,畫面應會顯示下方程式碼。

您無須執行任何步驟,只需查看現有的程式碼即可。

<!-- 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>

請詳閱程式碼區塊,瞭解執行程式碼的相關操作。

這與常規服務類似,但我們為資訊方塊新增了幾項特定元素:

  1. 綁定資訊方塊人供應商的權限
  2. 將服務註冊為資訊方塊供應商的意圖篩選器
  3. 指定預覽可在手機上檢視的額外中繼資料

9. 恭喜

恭喜!你已瞭解如何建構適用於 Wear OS 的資訊方塊!

後續步驟

查看其他 Wear OS 程式碼研究室:

其他資訊