自作ウォッチフェイスにコンプリケーションを実装する

さくっとコンプリケーションを乗せてみたよって記事があっても良いと思ったので書きます。

前提

これが出来ているとする。

f:id:Ginkyo:20180430054030p:plain

Android Studio で新プロジェクトを作成する際に生成できるやつです。

目標

↑をこういう風にすること。

f:id:Ginkyo:20180430054117p:plain

今回は設定画面の作成をせず、とにかく上画像のようにコンプリケーションを置ければ OK とします。

MyWatchFace の変更点

コンプリケーションのためのフィールドを定義する

private static final int LEFT_COMPLICATION_ID = 100;
private static final int RIGHT_COMPLICATION_ID = 101;

private static final int[] COMPLICATION_IDS = {
        LEFT_COMPLICATION_ID, RIGHT_COMPLICATION_ID
};

コンプリケーションを識別するための ID。これからたくさん使う。

Engine の変更点

追加のフィールドを定義する

private SparseArray<ComplicationData> mComplicationDataArray;
private SparseArray<ComplicationDrawable> mComplicationDrawables;

SparseArray<Object>Map<Integer, Object> のように使えるクラスだが、キーには int を使用する。キーのオートボクシングを行わない、データ構造がマッピングごとに余分なエントリ オブジェクトに依存しない、という理由で HashMap よりメモリの効率が良いらしい。*1 エントリ オブジェクトって Map.Entry<K, V> のことかな。

ComplicationData はコンプリケーションのデータの入れ物で、ComplicationDrawable はコンプリケーションを描画する Drawable

onCreate(SurfaceHolder) に追記する

mComplicationDataArray = new SparseArray<>(COMPLICATION_IDS.length);

mComplicationDrawables = new SparseArray<>(COMPLICATION_IDS.length);
// コンプリケーション ID をキーとして、
// ComplicationDrawable インスタンスを SparseArray<> に追加する
mComplicationDrawables.put(
        LEFT_COMPLICATION_ID, new ComplicationDrawable(MyWatchFace.this));
mComplicationDrawables.put(
        RIGHT_COMPLICATION_ID, new ComplicationDrawable(MyWatchFace.this));

// デフォルトのコンプリケーションを設定する
// 今回は設定画面を作らないのでここでの設定が必要
setDefaultSystemComplicationProvider(
        LEFT_COMPLICATION_ID, 
        SystemProviders.NEXT_EVENT, 
        ComplicationData.TYPE_SHORT_TEXT);
setDefaultSystemComplicationProvider(
        RIGHT_COMPLICATION_ID, 
        SystemProviders.WATCH_BATTERY, 
        ComplicationData.TYPE_RANGED_VALUE);

// コンプリケーションに通常モード用とアンビエントモード用の色を設定する
for (int complicationId : COMPLICATION_IDS) {
    ComplicationDrawable complicationDrawable = mComplicationDrawables.get(complicationId);

    complicationDrawable.setBorderColorActive(Color.RED);
    complicationDrawable.setBorderColorAmbient(Color.WHITE);

    complicationDrawable.setRangedValuePrimaryColorActive(Color.RED);
    complicationDrawable.setRangedValuePrimaryColorAmbient(Color.WHITE);
}

// ここで渡したコンプリケーションに対してデータが送られるようになる
setActiveComplications(COMPLICATION_IDS);

ComplicationDrawable インスタンスの作成を XML から行うこともできるが、今回は Java コードのみで行っている。

onPropertiesChanged(Bundle) に追記する

for (int complicationId : COMPLICATION_IDS) {
    ComplicationDrawable complicationDrawable = mComplicationDrawables.get(complicationId);
    complicationDrawable.setLowBitAmbient(mLowBitAmbient);
    complicationDrawable.setBurnInProtection(mBurnInProtection);
}

このメソッドは時計のプロパティ(焼き付き保護、低ビットアンビエントモード)が決定されたときに呼ばれる。

onAmbientModeChanged(boolean) に追記する

for (int complicationId : COMPLICATION_IDS) {
    mComplicationDrawables.get(complicationId).setInAmbientMode(mAmbient);
}

このメソッドはアンビエントモードが開始または終了したときに呼ばれる。

onTapCommand(int, int, int, long) に挿入する

case TAP_TYPE_TAP:
    // The user has completed the tap gesture.
    // TODO: Add code to handle the tap gesture.
    Toast.makeText(getApplicationContext(), R.string.message, Toast.LENGTH_SHORT)
            .show();

    // 追加ここから
    for (int complicationId : COMPLICATION_IDS) {
        // タップ位置がコンプリケーションの境界の内側なら、
        // タップアクションをコンプリケーションに送信する
        boolean successfulTap = mComplicationDrawables.get(complicationId).onTap(x, y);
        if (successfulTap) {
            return;
        }
    }
    // 追加ここまで

    break;

このメソッドはタップまたはタッチに関連するイベントが発生したときに呼ばれる。

onDraw(Canvas, Rect) に追記する

for (int complicationId : COMPLICATION_IDS) {
    mComplicationDrawables.get(complicationId).draw(canvas, now);
}

このメソッドはウォッチフェイスを描画する。invalidate() でこれの実行をスケジュールできる。

onComplicationDataUpdate(int, ComplicationData) を作成する

@Override
public void onComplicationDataUpdate(int watchFaceComplicationId, ComplicationData data) {
    mComplicationDataArray.put(watchFaceComplicationId, data);
    mComplicationDrawables.get(watchFaceComplicationId).setComplicationData(data);
}

このメソッドは新しいコンプリケーションデータを受信したときに呼ばれる。

onSurfaceChanged(SurfaceHolder, int, int, int) を作成する

@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    super.onSurfaceChanged(holder, format, width, height);

    int sizeOfComplication = width / 4;
    int midpointOfScreen = width / 2;

    int horizontalOffset = (midpointOfScreen - sizeOfComplication) / 2;
    int verticalOffset = midpointOfScreen - (sizeOfComplication / 2);

    mComplicationDrawables.get(LEFT_COMPLICATION_ID).setBounds(
            horizontalOffset,
            verticalOffset,
            (horizontalOffset + sizeOfComplication),
            (verticalOffset + sizeOfComplication)
    );

    mComplicationDrawables.get(RIGHT_COMPLICATION_ID).setBounds(
            (midpointOfScreen + horizontalOffset),
            verticalOffset,
            (midpointOfScreen + horizontalOffset + sizeOfComplication),
            (verticalOffset + sizeOfComplication)
    );
}

ここではコンプリケーションを配置する位置と範囲の計算を行っている。

サンプルアプリ(後述)のコメントによると、円形または正方形のコンプリケーションは画面幅の少なくとも四分の一、横長の長方形のコンプリケーションは画面幅の少なくとも三分の二を使用することが勧められている。

マニフェストComplicationHelperActivity を追加する

<activity android:name="android.support.wearable.complications.ComplicationHelperActivity"/>

コンプリケーションで「次の予定」を表示するには、ユーザがウォッチフェイスに権限を与える必要がある。ComplicationHelperActivityマニフェストに追加しておくだけで、権限を要求するダイアログが実装される。(とても楽で良い)

実行してみる

f:id:Ginkyo:20180430080000p:plain

まだ権限を与えていないので「次の予定」は表示されていない。

アンビエントモードではこんな感じ。

f:id:Ginkyo:20180430080033p:plain

「次の予定」コンプリケーションをタップすると、

f:id:Ginkyo:20180430080054p:plain

ダイアログが出るので了承する。

f:id:Ginkyo:20180430054117p:plain

予定ないんか~い!

最後に

ここではコンプリケーションを 2 個置くための最低限の実装しかしていません。実用的なことを学ぶにはサンプルアプリとドキュメントを見るのが良いと思います。というか僕も初心者なのでこれから学んでいくつもりです。

参考

以下、重要っぽいクラス達。