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

1. はじめに

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

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

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

演習内容

35a459b77a2c9d52.png

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

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

学習内容

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

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

前提条件

  • Kotlin に関する基礎知識

2. 設定方法

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

必要なもの

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

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

git がインストールされている場合は、以下のコマンドをそのまま実行してこのリポジトリからコードのクローンを作成できます。

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

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

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

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

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

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

HelloWorldTileService

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

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

最初のメソッドは Resources オブジェクトを返します。これは文字列 ID をタイルで使用する画像リソースにマッピングします。

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

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

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

SuspendingTileService には、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 モジュールを選択した状態で、デバイスまたはエミュレータにアプリ(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 は、前述の SuspendingTileService クラスを拡張します。

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

MessagingTileRenderer

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

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

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

Jetpack の Tiles ライブラリのバージョン 1.4 でリリースされたタイル プレビュー機能(現在はアルファ版)を使用して、Android Studio でタイル UI をプレビューできます。これにより、UI 開発時のフィードバック ループが短縮され、開発速度が向上します。

ファイルの最後に MessagingTileRenderer のタイル プレビューを追加します。

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

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

@Composable アノテーションは提供されません。タイルはコンポーズ可能な関数と同じプレビュー UI を使用しますが、Compose は利用せず、コンポーズ可能ではありません。

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

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

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

7. Tiles Material を追加する

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

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

start/build.gradle

implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"

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

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()

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

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)
    .setResponsiveContentInsetEnabled(true)
    .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()

96fee80361af2c0f.png

MultiButtonLayout は最大 7 個のボタンをサポートしており、適切な間隔でレイアウトします。

messagingTileLayout() 関数に、PrimaryLayout の「メイン」のチップとして「新しい」CompactChip を追加しましょう。

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()
    )

2041bdca8a46458b.png

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

8. 画像を追加する

大まかに言うと、タイルはレイアウト要素(文字列 ID でリソースを参照)とリソースそのもの(画像など)の 2 つのもので構成されます。

ローカル画像を表示するのは容易です。Android のドローアブル リソースを直接使用することはできませんが、Horologist が提供する簡易関数を使用して、必要な形式に簡単に変換できます。次に、addIdToImageMapping 関数を使用して、画像をリソース ID に関連付けます。次に例を示します。

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: List<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

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

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

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

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

2 つの変更を加える必要があります。まず、Resources オブジェクトを返す関数 previewResources() を作成します。

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

private fun previewResources() = Resources.Builder()
    .addIdToImageMapping(ID_IC_SEARCH, drawableResToImageResource(R.drawable.ic_search_24))
    .addIdToImageMapping(knownContacts[1].imageResourceId(), drawableResToImageResource(R.drawable.ali))
    .addIdToImageMapping(knownContacts[2].imageResourceId(), drawableResToImageResource(R.drawable.taylor))
    .build()

次に、messagingTileLayoutPreview() を更新して、リソースを渡すようにします。

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

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData({ previewResources() }) { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

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

3142b42717407059.png

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

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

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

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

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

LoadAction

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

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

これをクリックするとサービスで onTileRequest が呼び出されるため(SuspendingTileService では 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 を渡しています。これは NoOps 実装で、ボタンがクリックされても何も実行されません。

実際のクリック アクションを渡すように messagingTileLayout() を更新しましょう。

  1. 新しいパラメータ searchButtonClickableModifiersBuilders.Clickable タイプ)を追加します。
  2. これを既存の 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")
}

タイルを実行し(「hello」タイルではなく「メッセージ」タイルを実行してください)、検索ボタンをクリックします。MainActivity が開き、検索ボタンがクリックされたことを確認するテキストが表示されます。

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

10. 完了

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

次のステップ

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