Wear OS で初めてのタイルを作成する

1. はじめに

スマートウォッチのアニメーション。ユーザーがウォッチフェイスをスワイプし、最初のタイルである天気予報、次にタイマーのタイルを表示したあと、スワイプバックする。

Wear OS タイルを使うと、必要な情報やアクションに容易にアクセスできます。ウォッチフェイスをスワイプするだけで、最新の天気予報を確認したり、タイマーを開始したりすることが可能です。

タイルは、独自のアプリコンテナで実行されるのではなく、システム UI の一部として実行されます。Service を使用して、タイルのレイアウトとコンテンツを記述します。これにより、システム UI が必要に応じてタイルのレンダリングを行います。

演習内容

35a459b77a2c9d52.png

メッセージ アプリ用に、最近の会話を表示するタイルを作成します。このサーフェスから、ユーザーは次に示す 3 つの一般的なタスクのいずれかを直接開始できます。

  • 会話を開く
  • 会話を検索する
  • 新しいメッセージを作成する

学習内容

この Codelab では、以下を行う方法を含め、独自の Wear OS タイルを作成する方法を学びます。

  • TileService を作成する
  • デバイスでタイルをテストする
  • Android Studio でタイルの UI をプレビューする
  • タイルの UI を開発する
  • 画像を追加する
  • インタラクションを処理する

前提条件

  • Kotlin に関する基礎知識

2. 設定方法

このステップでは、環境を設定してスターター プロジェクトをダウンロードします。

必要なもの

  • Android Studio Dolphin(2021.3.1)以降
  • Wear OS デバイスまたはエミュレータ

Wear OS の使用に慣れていない場合は、開始する前にこちらのクイックガイドをお読みになることをおすすめします。Wear OS エミュレータのセットアップ手順とシステムの操作方法が記載されています。

コードをダウンロードする

git がインストールされている場合は、以下のコマンドをそのまま実行してこのリポジトリからコードのクローンを作成できます。git がインストールされているかどうかを確認するには、ターミナルまたはコマンドラインで「git –version」と入力し、正しく実行されることを確認します。

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

git がない場合は、次のボタンをクリックして、この Codelab のすべてのコードをダウンロードできます。

Android Studio でプロジェクトを開く

[Welcome to Android Studio] ウィンドウで c01826594f360d94.png [Open an Existing Project] を選択するか、[File] > [Open] を開いて [Download Location] フォルダを選択します。

3. 基本的なタイルを作成する

タイルのエントリ ポイントはタイルサービスです。このステップでは、タイルサービスを登録し、タイルのレイアウトを定義します。

HelloWorldTileService

TileService を実装するクラスは、次の 2 つの関数を指定する必要があります。

  • onResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
  • onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>

1 つ目は文字列 ID を画像リソースにマッピングします。ここで、タイルで使用する画像リソースを指定します。

2 つ目はレイアウトの説明を含むタイルの説明を返します。ここで、タイルのレイアウトと、データがタイルにどのようにバインドされるかを定義します。

start モジュールから HelloWorldTileService.kt を開きますこれから行う変更はすべて、このモジュールに反映されます。また、この Codelab の結果を確認したい場合は、finished モジュールが用意されています。

HelloWorldTileService は、Horologist Tiles ライブラリの Kotlin コルーチンに適したラッパーである CoroutinesTileService を拡張します。Horologist は、Google が開発したライブラリのグループであり、デベロッパーが一般的に必要としている機能でも、Jetpack ではまだ利用できない機能を Wear OS デベロッパー向けに補完することを目的としています。

CoroutinesTileService には、TileService の関数のコルーチン版である suspend 関数が 2 つあります。

  • suspend resourcesRequest(requestParams: ResourcesRequest): Resources
  • suspend tileRequest(requestParams: TileRequest): Tile

コルーチンについて詳しくは、Android での Kotlin コルーチンのドキュメントをご覧ください。

HelloWorldTileServiceまだ完成していません。マニフェストにサービスを登録する必要があります。また、tileLayout の実装を提供する必要もあります。

タイルサービスを登録する

システムが認識できるよう、タイルサービスをマニフェストに登録する必要があります。登録すると、ユーザーが追加できるタイルのリストに表示されます。

<application> 要素内に <service> を追加します。

start/src/main/AndroidManifest.xml

<service
    android:name="com.example.wear.tiles.hello.HelloWorldTileService"
    android:icon="@drawable/ic_waving_hand_24"
    android:label="@string/hello_tile_label"
    android:description="@string/hello_tile_description"
    android:exported="true"
    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_hello" />
</service>

タイルが初めて読み込まれたとき、またはタイルの読み込み中にエラーが発生した場合、アイコンとラベルが(プレースホルダとして)使用されます。最後のメタデータは、ユーザーがタイルを追加するときにカルーセルに表示されるプレビュー画像を定義します。

タイルのレイアウトを定義する

HelloWorldTileService には、本文が TODO()tileLayout という関数があります。これを、タイルのレイアウトを定義してデータをバインドする実装に置き換えてみましょう。

start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt

private fun tileLayout(): LayoutElement {
    val text = getString(R.string.hello_tile_body)
    return LayoutElementBuilders.Box.Builder()
        .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
        .setWidth(DimensionBuilders.expand())
        .setHeight(DimensionBuilders.expand())
        .addContent(
            LayoutElementBuilders.Text.Builder()
                .setText(text)
                .build()
        )
        .build()
}

Text 要素を作成して Box 内に設定し、基本的な配置ができるようにします。

これで、初めての Wear OS タイルを作成できました。このタイルをインストールして、どのように表示されるかを確認してみましょう。

4. デバイスでタイルをテストする

実行構成のプルダウンで開始モジュールを選択した状態で、デバイスまたはエミュレータにアプリ(start モジュール)をインストールし、ユーザーのようにタイルを手動でインストールすることもできます。

今回は代わりに、Android Studio Dolphin で導入された機能である Direct Surface Launch を使用して、Android Studio からタイルを直接起動するための新しい実行構成を作成しましょう。上部パネルのプルダウンで、[Edit Configurations...] を選択します。

Android Studio の上部パネルにある実行構成のプルダウン。[Edit configurations...] メニューがハイライト表示されている。

新しい構成を追加するボタンをクリックし、[Wear OS Tile] を選択します。わかりやすい名前を追加して、Tiles_Code_Lab.start モジュールと HelloWorldTileService タイルを選択します。

[OK] を押して終了します。

[Edit configurations...] メニューの画面。HelloTile という Wear OS タイルを構成している。

Direct Surface Launch を使用すると、Wear OS エミュレータまたは実機でタイルをすばやくテストできます。「HelloTile」を実行してみましょう。次のスクリーンショットのように表示されます。

黒色の背景に白色の文字で「Time to create a tile!」と表示された丸い時計

5. メッセージ タイルを作成する

5 つの円形ボタンが 2×3 のピラミッド状に表示された円形のスマートウォッチ。1 番目と 3 番目のボタンには紫色のイニシャル、2 番目と 4 番目のボタンにはプロフィール写真、最後のボタンには検索アイコンが表示されている。ボタンの下には、黒色で「New」と書かれた紫色のコンパクト チップがある。

これから作成するメッセージ タイルは、より実際のタイルに近いものです。HelloWorld の例とは異なり、ローカル リポジトリからデータを読み込み、ネットワークから表示する画像を取得し、アプリを開くための操作をタイルから直接処理します。

MessagingTileService

MessagingTileService は、前述の CoroutinesTileService クラスを拡張します。

前の例との主な違いは、リポジトリからデータを監視し、ネットワークから画像データを取得している点です。

長時間かかる可能性のある処理(ネットワーク呼び出しなど)では、タイルサービスの関数のタイムアウトが比較的短いため、WorkManager などを使用するほうが適しています。この Codelab では WorkManager の紹介は行いません。実際に使ってみるには、こちらの Codelab をご覧ください。

MessagingTileRenderer

MessagingTileRenderer は、TileRenderer クラスを拡張します(Horologist Tiles からの別の抽象化)。これは完全に同期的です。状態はレンダラ関数に渡されるため、テストや Android Studio プレビューで使用しやすくなります。

次のステップでは、タイルの Android Studio プレビューを追加する方法について説明します。

6. プレビュー関数を追加する

Horologist Tile の TileLayoutPreview(および同様のもの)を使用して、Android Studio でタイル UI をプレビューできます。UI 開発時のフィードバック ループが短縮され、反復処理がはるかに速くなります。

このプレビューの表示には、Jetpack Compose のツールを使用します。そのため、以下のプレビュー関数には @Composable アノテーションが表示されます。詳しくは、コンポーザブルのプレビューをご覧ください。ただし、この Codelab を完了する必要はありません。

ファイルの最後に MessagingTileRenderer のコンポーザブルのプレビューを追加します。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    TileLayoutPreview(
        state = MessagingTileState(MessagingRepo.knownContacts),
        resourceState = emptyMap(),
        renderer = MessagingTileRenderer(LocalContext.current)
    )
}

なお、コンポーズ可能な関数は TileLayoutPreview を使用します。タイル レイアウトを直接プレビューすることはできません。

「分割」エディタモードを使用すると、タイルのプレビューが表示されます。

Android Studio の分割画面ビュー。左側にプレビュー コード、右側にタイルの画像が表示されている。

MessagingTileState で人工的なデータを渡しており、リソースの状態が(まだ)ないため、空のマップを渡すことができます。

次のステップでは、Tiles Material を使用してレイアウトを更新します。

7. Tiles Material を追加する

Tiles Material にはビルド済みのマテリアル コンポーネントとレイアウトが用意されているため、Wear OS 向けの最新のマテリアル デザインを採用したタイルを作成できます。

Tiles Material の依存関係を build.gradle ファイルに追加します。

start/build.gradle

implementation "androidx.wear.tiles:tiles-material:$tilesVersion"

設計の複雑さによっては、同じファイル内のトップレベルの関数を使用してレンダラとレイアウト コードを同じ場所に配置し、UI の論理ユニットをカプセル化すると便利です。

レンダラ ファイルの下部にボタンのコードを追加し、プレビューも追加します。

start/src/main/java/MessagingTileRenderer.kt

private fun searchLayout(
    context: Context,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(context.getString(R.string.tile_messaging_search))
    .setIconContent(MessagingTileRenderer.ID_IC_SEARCH)
    .setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
    .build()

@IconSizePreview
@Composable
private fun SearchButtonPreview() {
    LayoutElementPreview(
        searchLayout(
            context = LocalContext.current,
            clickable = emptyClickable
        )
    ) {
        addIdToImageMapping(
            MessagingTileRenderer.ID_IC_SEARCH,
            drawableResToImageResource(R.drawable.ic_search_24)
        )
    }
}

LayoutElementPreviewTileLayoutPreview に似ていますが、ボタン、チップ、ラベルのような、個々のコンポーネントに使用されます。末尾のラムダでリソース ID の(画像リソースへの)マッピングを指定するため、ここでは ID_IC_SEARCH を検索画像リソースにマッピングしています。

「分割」エディタモードを使用すると、検索ボタンのプレビューが表示されます。

縦に積み重ねられたプレビュー。上はタイル、下は検索アイコンボタン。

連絡先レイアウトの作成と同様のことも行えます。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun contactLayout(
    context: Context,
    contact: Contact,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(contact.name)
    .apply {
        if (contact.avatarUrl != null) {
            setImageContent(contact.imageResourceId())
        } else {
            setTextContent(contact.initials)
            setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
        }
    }
    .build()

Tiles Material に含まれるものはコンポーネントだけではありません。列と行をネストして使用する代わりに Tiles Material のレイアウトを使用することで、目的の外観をすばやく実現できます。

ここでは PrimaryLayoutMultiButtonLayout を使用して、4 つの連絡先と検索ボタンを配置します。次のレイアウトを使用して、MessagingTileRenderermessagingTileLayout() 関数を更新します。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState
) = PrimaryLayout.Builder(deviceParameters)
    .setContent(
        MultiButtonLayout.Builder()
            .apply {
                // In a PrimaryLayout with a compact chip at the bottom, we can fit 5 buttons.
                // We're only taking the first 4 contacts so that we can fit a Search button too.
                state.contacts.take(4).forEach { contact ->
                    addButtonContent(
                        contactLayout(
                            context = context,
                            contact = contact,
                            clickable = emptyClickable
                        )
                    )
                }
            }
            .addButtonContent(searchLayout(context, emptyClickable))
            .build()
    )
    .build()

5 つのボタンを 2x3 のピラミッド状に配置したタイルのプレビュー。2 番目と 3 番目のボタンは青色で塗りつぶされた円であり、画像がないことを示している。

MultiButtonLayout は最大 7 個のボタンをサポートし、適切な間隔でレイアウトします。messagingTileLayout() 関数の PrimaryLayout ビルダーで、PrimaryLayout に「New」チップも追加してみましょう。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

.setPrimaryChipContent(
    CompactChip.Builder(
        /* context = */ context,
        /* text = */ context.getString(R.string.tile_messaging_create_new),
        /* clickable = */ emptyClickable,
        /* deviceParameters = */ deviceParameters
    )
        .setChipColors(ChipColors.primaryChipColors(MessagingTileTheme.colors))
        .build()
)

5 つのボタンとその下に「New」と書かれたコンパクト チップが配置されたタイルのプレビュー

次のステップでは、画像の欠落を修正します。

8. 画像を追加する

タイルにローカル画像を表示するのは簡単です。文字列 ID(レイアウトで使用)から画像へのマッピングを提供し、Horologist Tile の簡易関数を使用してドローアブルを読み込み、画像リソースに変換します。この例については、SearchButtonPreview をご覧ください。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

addIdToImageMapping(
    ID_IC_SEARCH,
    drawableResToImageResource(R.drawable.ic_search_24)
)

メッセージ タイルでは、(ローカル リソースだけでなく)ネットワークから画像を読み込む必要もあります。そのため、Kotlin コルーチン ベースの画像ローダーである Coil を使用します。

このコードはすでに作成されています。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileService.kt

override suspend fun resourcesRequest(requestParams: ResourcesRequest): Resources {
    val avatars = imageLoader.fetchAvatarsFromNetwork(
        context = this@MessagingTileService,
        requestParams = requestParams,
        tileState = latestTileState()
    )
    return renderer.produceRequestedResources(avatars, requestParams)
}

タイルレンダラは完全に同期的であるため、タイルサービスがネットワークからビットマップを取得します。前のように、画像のサイズによっては WorkManager を使用して事前に画像を取得する方が適切な場合もありますが、この Codelab では画像を直接取得します。

avatars マップ(Contact から Bitmap)を、リソースの「状態」としてレンダラに渡します。これで、レンダラがビットマップをタイルの画像リソースに変換できるようになりました。

このコードもすでに作成されています。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
    resourceState: Map<Contact, Bitmap>,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    resourceIds: MutableList<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

    resourceState.forEach { (contact, bitmap) ->
        addIdToImageMapping(
            /* id = */ contact.imageResourceId(),
            /* image = */ bitmap.toImageResource()
        )
    }
}

では、サービスがビットマップを取得し、レンダラがそのビットマップを画像リソースに変換しているのであれば、タイルに画像が表示されないのはなぜでしょうか。

実際には、インターネットにアクセスできるデバイスでタイルを実行すると、画像は読み込まれます。この問題はプレビューでのみ生じます。理由は、emptyMap()resourceState に渡しているためです。

実際のタイルでは、ネットワークからビットマップを取得して別の連絡先にマッピングしますが、プレビューとテストでは、ネットワークに接続する必要はまったくありません。

MessagingTileRendererPreview() を更新して、ビットマップを必要とする 2 つの連絡先にビットマップを提供するようにします。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    val state = MessagingTileState(MessagingRepo.knownContacts)
    val context = LocalContext.current
    TileLayoutPreview(
        state = state,
        resourceState = mapOf(
            state.contacts[1] to (context.getDrawable(R.drawable.ali) as BitmapDrawable).bitmap,
            state.contacts[2] to (context.getDrawable(R.drawable.taylor) as BitmapDrawable).bitmap,
        ),
        renderer = MessagingTileRenderer(context)
    )
}

プレビューを更新すると、画像が次のように表示されます。

5 つのボタンが配置されたプレビュー。青色の円だった 2 つのボタンに今回は写真が表示されている。

次のステップでは、各要素のクリックを処理します。

9. インタラクションを処理する

タイルでできる便利なこととして、クリティカル ユーザー ジャーニーのショートカットの提供が挙げられます。単にアプリを起動するアプリ ランチャーとは異なり、アプリ内の特定の画面に状況依存のショートカットを表示するスペースがあります。

これまで、チップと各ボタンに emptyClickable を使用してきました。これはインタラクティブではないプレビューには適していますが、要素にアクションを追加する方法を見てみましょう。

「ActionBuilders」クラスの 2 つのビルダーは、クリック可能なアクション LoadActionLaunchAction を定義します。

LoadAction

LoadAction は、ユーザーが要素をクリックしたときにタイルサービスでロジック(カウンタの増加など)を実行する場合に使用します。

.setClickable(
    Clickable.Builder()
        .setId(ID_CLICK_INCREMENT_COUNTER)
        .setOnClick(ActionBuilders.LoadAction.Builder().build())
        .build()
    )
)

これをクリックするとサービスで onTileRequest が呼び出されるため(CoroutinesTileService では tileRequest)、タイル UI を更新する良い機会となります。

override suspend fun tileRequest(requestParams: TileRequest): Tile {
    if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
        // increment counter
    }
    // return an updated tile
}

LaunchAction

LaunchAction はアクティビティの起動に使用できます。MessagingTileRenderer で、検索ボタンのクリック可能なアクションを更新してみましょう。

検索ボタンは、MessagingTileRenderersearchLayout() 関数で定義されます。この関数はすでにパラメータとして Clickable を受け取っていますが、今のところ emptyClickable を渡しています。これは no-op 実装で、ボタンがクリックされても何も実行されません。

実際のクリック アクションを渡すように messagingTileLayout() を更新しましょう。searchButtonClickable パラメータを追加して、searchLayout() に渡します。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState,
    searchButtonClickable: ModifiersBuilders.Clickable
...
    .addButtonContent(searchLayout(context, searchButtonClickable))

また、新しいパラメータ(searchButtonClickable)を追加したので、messagingTileLayout を呼び出す場所である renderTile も更新する必要があります。launchActivityClickable() 関数を使用して新しいクリック可能な要素を作成し、openSearch() ActionBuilder をアクションとして渡します。

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun renderTile(
    state: MessagingTileState,
    deviceParameters: DeviceParametersBuilders.DeviceParameters
): LayoutElementBuilders.LayoutElement {
    return messagingTileLayout(
        context = context,
        deviceParameters = deviceParameters,
        state = state,
        searchButtonClickable = launchActivityClickable("search_button", openSearch())
    )
}

launchActivityClickable を開いて、これらの関数(すでに定義されている関数)の仕組みを確認します。

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun launchActivityClickable(
    clickableId: String,
    androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
    .setId(clickableId)
    .setOnClick(
        ActionBuilders.LaunchAction.Builder()
            .setAndroidActivity(androidActivity)
            .build()
    )
    .build()

これは LoadAction によく似ています。主な違いは setAndroidActivity を呼び出すことです。同じファイルに、さまざまな ActionBuilder.AndroidActivity の例があります。

このクリック可能なアクションに使用している openSearch については、setMessagingActivity を呼び出し、文字列エクストラを渡して、それがどのボタンのクリックなのかを識別します。

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun openSearch() = ActionBuilders.AndroidActivity.Builder()
    .setMessagingActivity()
    .addKeyToExtraMapping(
        MainActivity.EXTRA_JOURNEY,
        ActionBuilders.stringExtra(MainActivity.EXTRA_JOURNEY_SEARCH)
    )
    .build()

...

internal fun ActionBuilders.AndroidActivity.Builder.setMessagingActivity(): ActionBuilders.AndroidActivity.Builder {
    return setPackageName("com.example.wear.tiles")
        .setClassName("com.example.wear.tiles.messaging.MainActivity")
}

タイルを実行し、検索ボタンをクリックします。MainActivity が開き、検索ボタンがクリックされたことを確認するためのテキストが表示されます。

他のアクションの追加も同様です。ClickableActions には、必要な関数が含まれています。ヒントが必要な場合は、finished モジュールの MessagingTileRenderer をご確認ください。

10. 完了

お疲れさまでした。Wear OS のタイルの作成方法を学習しました。

次のステップ

GitHub の Golden Tiles の実装Wear OS タイルのガイドから、さらに詳しい情報をご確認ください。