1. はじめに
Wear OS タイルを使うと、必要な情報やアクションに容易にアクセスできます。ウォッチフェイスをスワイプするだけで、最新の天気予報を確認したり、タイマーを開始したりすることが可能です。
タイルは、独自のアプリコンテナで実行されるのではなく、システム UI の一部として実行されます。Service を使用して、タイルのレイアウトとコンテンツを記述します。これにより、システム UI が必要に応じてタイルのレンダリングを行います。
演習内容
メッセージ アプリ用に、最近の会話を表示するタイルを作成します。このサーフェスから、ユーザーは次の 3 つの一般的なタスクに移動できます。
- 会話を開く
- 会話を検索する
- 新しいメッセージを作成する
学習内容
この Codelab では、以下を行う方法を含め、独自の Wear OS タイルを作成する方法を学びます。
TileService
を作成する- デバイスでタイルをテストする
- Android Studio でタイルの UI をプレビューする
- タイルの UI を開発する
- 画像を追加する
- インタラクションを処理する
前提条件
- Kotlin に関する基礎知識
2. 設定方法
このステップでは、環境を設定してスターター プロジェクトをダウンロードします。
必要なもの
- Android Studio Koala Feature Drop | 2024.1.2 Canary 1 以降
- Wear OS デバイスまたはエミュレータ
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] ウィンドウで [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...] を選択します。
新しい構成を追加するボタンをクリックし、[Wear OS Tile] を選択します。わかりやすい名前を追加して、Tiles_Code_Lab.start
モジュールと HelloWorldTileService
タイルを選択します。
[OK] を押して終了します。
Direct Surface Launch を使用すると、Wear OS エミュレータまたは実機でタイルをすばやくテストできます。「HelloTile」を実行してみましょう。次のスクリーンショットのように表示されます。
5. メッセージ タイルを作成する
これから作成するメッセージ タイルは、より実際のタイルに近いものです。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] エディタモードを使用すると、タイルのプレビューが表示されます。
次のステップでは、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 のレイアウトを使用することで、目的の外観をすばやく実現できます。
ここでは PrimaryLayout
と MultiButtonLayout
を使用して、4 つの連絡先と検索ボタンを配置します。次のレイアウトを使用して、MessagingTileRenderer
の messagingTileLayout()
関数を更新します。
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()
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()
)
次のステップでは、画像の欠落を修正します。
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
)
}
}
プレビューを更新すると、画像が次のように表示されます。
次のステップでは、各要素のクリックを処理します。
9. インタラクションを処理する
タイルでできる便利なこととして、クリティカル ユーザー ジャーニーのショートカットの提供が挙げられます。単にアプリを起動するアプリ ランチャーとは異なり、アプリ内の特定の画面に状況依存のショートカットを表示するスペースがあります。
これまで、チップと各ボタンに emptyClickable
を使用してきました。これはインタラクティブではないプレビューには適していますが、要素にアクションを追加する方法を見てみましょう。
「ActionBuilders」クラスの 2 つのビルダーは、クリック可能なアクション LoadAction
と LaunchAction
を定義します。
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
で、検索ボタンのクリック可能なアクションを更新してみましょう。
検索ボタンは、MessagingTileRenderer
の searchLayout()
関数で定義されます。この関数はすでにパラメータとして Clickable
を受け取っていますが、今のところ emptyClickable
を渡しています。これは NoOps 実装で、ボタンがクリックされても何も実行されません。
実際のクリック アクションを渡すように messagingTileLayout()
を更新しましょう。
- 新しいパラメータ
searchButtonClickable
(ModifiersBuilders.Clickable
タイプ)を追加します。 - これを既存の
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 タイルのガイド、設計ガイドラインで、さらに詳しい情報をご確認ください。