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

スワイプすると、時刻、天気、タイマーの各タイルが順番に表示されます。

上のアニメーション(タイルのデモ)をご覧ください。注: アニメーション GIF は一度しか再生されません。アニメーションを見逃した場合は、ページを再読み込みしてください。

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

タイルの開発は、アプリの作成とは少し異なります。

タイルは、独自のアプリケーション コンテナで実行されるのではなく、システム UI の一部として実行されます。つまり、アクティビティや XML レイアウトなど、おなじみの Android プログラミングのコンセプトにはアクセスできません。

代わりに、Service を使用してタイルのレイアウトとコンテンツを記述します。これにより、システム UI が必要に応じてタイルのレンダリングを行います。

この Codelab では、独自の Wear OS タイルをゼロから作成する方法を学びます。

学習内容

  • タイルを作成する
  • デバイスでタイルをテストする
  • タイル レイアウトを設計する
  • 画像を追加する
  • インタラクションを追加する(タップ)

作成するアプリの概要

1 日の目標達成に向けて、歩数を表示するカスタム タイルを作成します。複雑なレイアウトが含まれているため、さまざまなレイアウト要素やコンテナについて学ぶことができます。

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 エミュレータまたは物理デバイスで start モジュールと finished モジュールを実行できるかどうかをテストします。
  4. start モジュールは次のスクリーンショットのようになります。すべての作業をこのモジュールで行います。

c72e8870facd8458.png

start コードを確認する

  • build.gradle: 基本的なアプリ構成が含まれています。これにはタイルを作成するために必要な依存関係が含まれます。
  • main > AndroidManifest.xml: このアプリを Wear OS アプリとしてマークするために必要なパーツが含まれています。この Codelab で、このファイルについて説明します。
  • main > GoalsRepository.kt: ユーザーが当日設定したランダムな歩数を非同期で取得する疑似リポジトリ クラスが含まれています。
  • main > GoalsTileService.kt: タイルを作成するためのボイラープレートが含まれています。ほとんどの作業は、このファイルで行います。
  • debug > AndroidManifest.xml: タイルのプレビューを可能にする TilePreviewActivity のアクティビティ要素が含まれています。
  • debug > TilesPreviewActivity.kt: タイルのプレビューに使用するアクティビティが含まれています。

まず、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 は 1 つ以上の TimelineEntry インスタンスで構成され、各インスタンスが時間間隔ごとのレイアウトを記述します。複数の TimelineEntry 値を作成すると、システムはそれらの値を指定された時間間隔でレンダリングします。

fbb666b722376749.png

タイムラインの使用方法について詳しくは、タイムライン ガイドをご覧ください。

この例では、常に 1 つのタイルのみを表示するため、TimelineEntry インスタンスを 1 つだけ宣言しています。次に、setLayout を使用してレイアウトを設定します。

ルートは 1 つ以上の複雑なレイアウトで構成できますが、このステップでは、「Hello, world!」を表示するシンプルな Text レイアウト要素を作成します。

これで完了です。

非常にシンプルなタイルを作成できたので、これを 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() を使用してタイルを作成します。

また、onDestroy()tileManager.close() を使用してクリーンアップを行います。

Wear OS エミュレータまたはデバイス上でアプリを実行してみましょう(start モジュールを選択してください)。次のように、中央に「Hello, world!」と表示されるはずです。

b9976e1073554422.png

基本的なレイアウトを作成したので、次はこれを拡張し、より複雑なタイルを作成しましょう。最終的な目標は、次のレイアウトを作成することです。

c6e1959693cded21.png

ルート レイアウトをボックスに置き換える

「Hello, world!」テキストラベルが含まれる onTileRequest メソッド全体を、次のコードで置き換えます。

ステップ 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 コンテナは、子を重ねて配置します。このボックス内にコンテンツを追加します。この場合、まだ存在していないメソッドを呼び出すことになります。

ここでも、複雑なレイアウトを作成する際のパターンに沿って、メソッドを分割して読みやすくしています。

コードの下部(build() 呼び出しの上)に TODO がいくつか追加されていますが、これについては 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 に関するエラーが表示された場合は、タイル バージョンをクリックしてインポートします)。

このコードでは、目標歩数の達成度を表す ArcLine を含む Arc.Builder を返します。

弧の長さを、達成した目標の割合(度数に変換)として設定し、色と太さも設定します。

次に、アンカータイプを使用して、弧の始点を指定します。アンカーにはさまざまなオプションが用意されています。どのオプションも、Codelab の終了後に自由に試すことができます。

このステップはこれで完了です。

実際の動作を確認しましょう。アプリをもう一度実行すると、次のように表示されます。

ad79daf115d3b6a4.png

列コンテナを追加する

タイルに達成度のインジケーターが表示されるようになったので、これに適切なテキストを追加しましょう。

複数のテキスト フィールド(さらに後で画像)を追加するため、画面中央の Column にアイテムを配置する必要があります。

Column多数あるタイル レイアウト コンテナのひとつであり、子要素を縦に並べて配置できます。

TODO: Add Column containing the rest of the data」を検索して、仮のテキスト コンテンツを以下のコードに置き換えます。TODO: START REPLACE THIS LATER から TODO: 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
            )

コードブロック内のコメントを読み、どのような処理が行われているのかを確認しましょう。

2 つのコンテンツを追加する列を作成していますが、エラーが 2 つ発生しています。

このエラーの原因を考えてみてください。

ここでも、複雑なレイアウトを作成する際のパターンに沿って、メソッドを分割しています(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()

コードブロック内のコメントを読み、どのような処理が行われているのかを確認しましょう。

これは非常に簡単です。2 つの異なる関数を使用して、個別の Text レイアウト要素を作成します。

名前から推測できるように、Text レイアウト要素は、テキスト文字列をレンダリングします(必要に応じてテキストをラップします)。

このクラスで前に定義した定数からフォントサイズを設定し、最後に onTileRequest() で取得したスタイルからフォント スタイルを設定します。

これでテキストができました。タイルの外観を見てみましょう。アプリを実行すると、次のように表示されます。

9eaca483c7e51f38.png

onTileRequest() に画像の名前を追加する

UI の最後の部分として、画像を追加します。

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() メソッド内で画像を指定する方法は 2 つあります。

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 ) で返す特定のドローアブルを設定します。

アプリを実行すると、完成した UI が表示されます。

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 修飾子を追加すると、そのレイアウト要素をタップしたユーザーに反応できます。クリック イベントへの反応として、次の 2 つのアクションを実行できます。

  • LaunchAction: アクティビティを起動します。
  • LoadAction: onTileRequest() を呼び出してタイルを強制的に更新します。

ここでは、クリック可能な修飾子を設定しますが、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

ここまで編集してきたサービスは、マニフェスト内で定義されています。

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>

コードブロックを読み、どのような処理が行われているのかを確認しましょう。

これは通常のサービスに似ていますが、このタイルに固有の要素をいくつか追加しています。

  1. タイル プロバイダをバインドする権限
  2. サービスをタイル プロバイダとして登録するインテント フィルタ
  3. スマートフォンに表示するプレビュー画像を指定する追加のメタデータ

これで、Wear OS のタイルを作成する Codelab は終了です。

次のステップ

Wear OS の他の Codelab をご確認ください。

参考資料