Watch Faceを作る その他Tips

2015/12/7 2916hit

Android WearのWatch Faceを作る 目次
連載Watch Faceを作ろう最終回はWatch Faceを作る上でよくあるTipsを集めました。

1.丸型と四角で処理を変えるには

Android Wearには丸型の画面を搭載したモデルと、四角い画面を搭載したモデルが有ります。
丸い画面か四角い画面かを判別したいときはEngineのonApplyWindowInsetsをOverrideして引数として渡されたWindowInsetsのisRound()を呼びます。
丸い画面の場合true、四角い画面の場合falseが返ります。

@Override
public void onApplyWindowInsets(WindowInsets insets) {
super.onApplyWindowInsets(insets);
boolean isRound = insets.isRound();
}


設定画面などでレイアウトを丸型と四角で切り替えたい時はstubを使用します。

<android.support.wearable.view.WatchViewStub
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rectLayout="@layout/rect_layout"
app:roundLayout="@layout/round_layout"/>


あとは、layoutフォルダにrect_layoutとround_layoutをそれぞれ作成します。
これによりstubは四角い画面ではrect_layoutに、丸い画面ではround_layoutに置き換わります。
丸い画面内にある四角い領域を切り取りたい場合は、BoxInsetLayoutも使用可能です。

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent">
</android.support.wearable.view.BoxInsetLayout>

このViewの子要素は、四角い画面の場合は画面いっぱいが選択されれるのに対して、丸型の場合は、中央の正方形部分が切り取られます。
この場合、丸型の液晶は切り取られる部分が余白となるので、実質的にゆとりあるデザインになるのに対して、四角の場合は全画面が選択されるので窮屈な画面となる時があります。
BoxInsetLayoutにpaddingを設定することで、四角い画面でも丸い画面でも余白をつけることが出来ます。
この余白は、丸型の画面では切り取られる余白とどちらか大きなほうが適用されます。

<android.support.wearable.view.BoxInsetLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:padding="15dp">
</android.support.wearable.view.BoxInsetLayout>



中央以外で針を回転させるには

画像で描かれた針の描画は針を水平移動してから中央地点を基準に回転させています。
同様に所定の位置に針を動かした後、基準にしたい位置を元に回転させます。
所定の位置への移動は「デザインした画像」=グラフィックソフト上での画像を元にmScaleを掛けた値にmBackgroundLeftあるいはmBackgroundTopを足します。
このため、針の画像は通常、mScaleでリサイズされたBitmap、水平移動するためのX座標、Y座標、回転中央市のX座標、Y座標を持つことになります。
これらは定形処理のため、クラスにまとめてしまうというのも手です。
例えば次のようなクラスにすることが出来ます。

package org.firespeed.myapplication;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;

/**
* Created by kenz on 2015/12/06.
*/
public class Hand {
private Bitmap mScaledBitmap;
private final int mBitmapId;
private final float mTop;
private final float mLeft;
private final float mCenterX;
private final float mCenterY;
private final float mScale;

private static Bitmap createScaledBitmap(Resources resources, int id, float scale) {
Bitmap original = ((BitmapDrawable) (resources.getDrawable(id))).getBitmap();
Bitmap scaled = Bitmap.createScaledBitmap(original, (int) (original.getWidth() * scale),
(int) (original.getHeight() * scale), true);
return scaled;
}

public Hand(Resources resources, int bitmapId, float scale, float backgroundTop, float backgroundLeft, float left, float top, float centerX, float centerY) {
mScaledBitmap = createScaledBitmap(resources, bitmapId, scale);
mLeft = left * scale + backgroundLeft;
mTop = top * scale + backgroundTop;
mCenterX = centerX * scale + backgroundLeft;
mCenterY = centerY * scale + backgroundTop;
mBitmapId = bitmapId;
mScale = scale;
}


public void rescaleBitmap(Resources resources) {
mScaledBitmap = createScaledBitmap(resources, mBitmapId, mScale);
}

public void draw(Canvas canvas, Paint paint, Matrix matrix, float rotate) {
matrix.setTranslate(mLeft, mTop);
matrix.postRotate(rotate, mCenterX, mCenterY);
canvas.drawBitmap(mScaledBitmap, matrix, paint);
}

public boolean isRecycled() {
return mScaledBitmap == null || mScaledBitmap.isRecycled();
}
}


前回作った時針、分針をこのクラスに置き換える場合はMyWatchFace.javaを次のように書き換えます。
mHourとmMinuteをBitmapからHourに置き換えます。

private Bitmap mHour; // 削除
private Bitmap mMinute; // 削除
private Hour mHour; // 追加
private Hour mMinute; // 追加

個別に保存していた座標を削除します。

private float mHourLeft;
private float mHourTop;
private float mMinuteLeft;
private float mMinuteTop;

onDraw()にて縦横の座標を求めていた部分をHandのインスタンス生成に置き換えます。
Handはコンストラクター内で座標計算を行います。
// 以下削除

mHourLeft = 244f * mScale + mBackgroundLeft; // 削除
mHourTop = 80f * mScale + mBackgroundTop; // 削除
mMinuteLeft = 242f * mScale + mBackgroundLeft; // 削除
mMinuteTop = 54f * mScale + mBackgroundTop; // 削除

// 以下追加
Resources resources = getResources();
mHour = new Hand(resources, R.drawable.hour, mScale, mBackgroundTop, mBackgroundLeft, 244f, 80f, 256f, 256f);
mMinute = new Hand(resources, R.drawable.minute, mScale, mBackgroundTop, mBackgroundLeft, 242f, 54f, 256f, 256f);


画像が破棄された時に備えたロジックもメソッドを呼ぶだけになります。

if (isSizeChanged
|| mBackground == null || mBackground.isRecycled()
|| mHour == null || mHour.isRecycled()
|| mMinute == null || mMinute.isRecycled()) {
Resources resources = getResources();
mHour = createSaledBitmap(resources, R.drawable.hour); // 削除
mMinute = createSaledBitmap(resources, R.drawable.minute); // 削除
mHour.rescaleBitmap(resources); // 追加
mMinute.rescaleBitmap(resources); // 追加
mBackground = createScaledBitmap(resources, R.drawable.background);
}


描画について、Matrixの処理もHandクラス内で行っているためCanvasとPaintとMatrix、そして角度を渡すだけです。

float hourRotate = (mCalendar.get(Calendar.HOUR) + mCalendar.get(Calendar.MINUTE) / 60f) * 30;
mMatrix.setTranslate(mHourLeft, mHourTop); // 削除
mMatrix.postRotate(hourRotate, mCenterX, mCenterY); // 削除
canvas.drawBitmap(mHour, mMatrix, mBitmapPaint); // 削除
mHour.draw(canvas,mBitmapPaint, mMatrix, hourRotate); // 追加
float minuteRotate = (mCalendar.get(Calendar.MINUTE) + mCalendar.get(Calendar.SECOND) / 60f) * 6;
mMatrix.setTranslate(mMinuteLeft, mMinuteTop); // 削除
mMatrix.postRotate(minuteRotate, mCenterX, mCenterY); // 削除
canvas.drawBitmap(mMinute, mMatrix, mBitmapPaint); // 削除
mMinute.draw(canvas,mBitmapPaint, mMatrix, minuteRotate); // 追加

このようにクラスにまとめておくことで、針の数が増えても闇雲にMyWatchFaceを複雑にせずに実装することが出来ます。

Ambient Modeで画像を使う方法

Ambient Modeで画像を使う場合、本来であれば白黒で作るのが望ましいです。
白黒の画像を作ったらHandクラスを書き換えてAmbientモード時は白黒のBitmapとなるように修正します。

package org.firespeed.myapplication;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;

/**
* Created by kenz on 2015/12/06.
*/
public class Hand {
private Bitmap mScaledBitmap;
private Bitmap mAmbientBitmap;
private final int mBitmapId;
private final int mAmbientBitmapId;
private final float mTop;
private final float mLeft;
private final float mCenterX;
private final float mCenterY;
private final float mScale;

private static Bitmap createScaledBitmap(Resources resources, int id, float scale) {
Bitmap original = ((BitmapDrawable) (resources.getDrawable(id))).getBitmap();
Bitmap scaled = Bitmap.createScaledBitmap(original, (int) (original.getWidth() * scale),
(int) (original.getHeight() * scale), true);
return scaled;
}

public Hand(Resources resources, int bitmapId, int ambientBitmapId, float scale, float backgroundTop, float backgroundLeft, float left, float top, float centerX, float centerY) {
mScaledBitmap = createScaledBitmap(resources, bitmapId, scale);
if(bitmapId == ambientBitmapId) {
mAmbientBitmap = mScaledBitmap;
}else{
mAmbientBitmap = createScaledBitmap(resources, ambientBitmapId, scale);
}
mLeft = left * scale + backgroundLeft;
mTop = top * scale + backgroundTop;
mCenterX = centerX * scale + backgroundLeft;
mCenterY = centerY * scale + backgroundTop;
mBitmapId = bitmapId;
mAmbientBitmapId = ambientBitmapId;
mScale = scale;
}

public void rescaleBitmap(Resources resources) {
mScaledBitmap = createScaledBitmap(resources, mBitmapId, mScale);
if(mBitmapId == mAmbientBitmapId){
mAmbientBitmap = mScaledBitmap;
}else{
mAmbientBitmap = createScaledBitmap(resources, mAmbientBitmapId, mScale);
}
}

public void draw(Canvas canvas, Paint paint, Matrix matrix, float rotate, boolean isAmbient) {
paint.setFilterBitmap(!isAmbient);
matrix.setTranslate(mLeft, mTop);
matrix.postRotate(rotate, mCenterX, mCenterY);
canvas.drawBitmap(isAmbient?mAmbientBitmap:mScaledBitmap, matrix, paint);
}

public boolean isRecycled() {
return mScaledBitmap == null || mScaledBitmap.isRecycled() || mAmbientBitmap == null || mAmbientBitmap.isRecycled();
}
}

Handのインスタンス生成時に白黒のリソースIDを渡すように処理を追加します。

mHour = new Hand(resources, R.drawable.hour, R.drawable.hourWb, mScale, mBackgroundTop, mBackgroundLeft, 244f, 80f, 256f, 256f);
mMinute = new Hand(resources, R.drawable.minute,R.drawable.minuteWb, mScale, mBackgroundTop, mBackgroundLeft, 242f, 54f, 256f, 256f);

draw()にAmbientかどうかを渡すようにします。

float hourRotate = (mCalendar.get(Calendar.HOUR) + mCalendar.get(Calendar.MINUTE) / 60f) * 30;
mHour.draw(canvas,mBitmapPaint, mMatrix, hourRotate, mAmbient);
float minuteRotate = (mCalendar.get(Calendar.MINUTE) + mCalendar.get(Calendar.SECOND) / 60f) * 6;
mMinute.draw(canvas,mBitmapPaint, mMatrix, minuteRotate, mAmbient);


AmbientModeに入るまでの時間を調整する

※この設定はAndroidWearのバッテリー持ちに大きな影響をおよぼす場合があります。
使用する場合はデフォルトをオフとして、設定で有効にできるようにすることをおすすめします。
また、この方法は非推奨の機能を使用しています。将来的なバージョンアップで使用できなくなる可能性があります。

アニメーションをみせるWatchFaceではAmbientModeへ入る時間が短すぎることが有ります。
AmbientModeに入る時間はPowerManager.WakeLockのacquire()を使って指定することが出来ます。
acquire()の引数としてAmbientModeへ入るまでの時間をミリ秒で渡します。
もし、引数を指定しなかった場合無制限となります。 必ずreleaseを読んで明示的にWakeLockを解除してください。


private static final String WAKE_LOCK_TAG = "my_watch_tag";
private static final long WAKE_LOCK_TIME = 20000l; // ambient modeに入る時間(ミリ秒)
private void setWakeLock() { ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, WAKE_LOCK_TAG).acquire(WAKE_LOCK_TIME);
}


setWakeLock()をAmbientModeから出た時と画面を表示した時にそれぞれ呼び出します。
AmgientModeから出た時

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
if (mAmbient != inAmbientMode) {
mAmbient = inAmbientMode;
if(mAmbient) {
setWakeLock(); // 追加
mConfig.connect();
}else{
mConfig.disconnect();
}
if (mLowBitAmbient) {
mDrawPaint.setAntiAlias(!inAmbientMode);
mBitmapPaint.setFilterBitmap(!inAmbientMode);
}
invalidate();
}
// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}

画面が表示された時

@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);

if (visible) {
registerReceiver();
// Update time zone in case it changed while we weren't visible.
mCalendar.setTimeZone(TimeZone.getDefault());
mConfig.connect();
setWakeLock(); // 追加
} else {
unregisterReceiver();
mConfig.disconnect();
}

// Whether the timer should be running depends on whether we're visible (as well as
// whether we're in ambient mode), so we may need to start or stop the timer.
updateTimer();
}



テキストの表示方法

今回はアナログ時計を作成しましたが、デジタル時計を作ったりニュースを表示するためにテキストを表示したい場合もあると思います。今回はmDrawPaintを使ってテキストを書いてみます。

onDraw()にてisSizeChangedのタイミングで文字のサイズを指定します。


@Override
public void onDraw(Canvas canvas, Rect bounds) {
mCalendar.setTimeInMillis(System.currentTimeMillis());
boolean isSizeChanged = canvas.getWidth() != mWidth || canvas.getHeight() != mHeight;
if (isSizeChanged) {
// 中略
mScale = longSize / DESIGNED_SIZE;
mDrawPaint.setTextSize(48f * mScale); // 追加
// 中略


onDraw()の最後にテキストを描画する処理を追加します。
引数は表示するテキスト、X座標(左端)、Y座標(下端)、paintです。

canvas.drawText("HELLO_TEXT", 0, 400 * mScale, mDrawPaint);


テキストをセンタリングする

テキストを左右中央寄せで表示したいという時もよくあると思います。
drawTextは左端を指定する必要があるのでテキストのサイズを調べて中央位置からその半分のサイズを引く必要があります。

PaintのmeasureText()を指定することでフォントと文字に応じた幅をしらべることが出来ます。
PaintにtextSizeが指定されている状態で、引数に計測したい幅の文字列を渡します。

String text = "HELLO_TEXT";
float x = mCenterX - (mDrawPaint.measureText(text) / 2f);
canvas.drawText(text, x, 400 * mScale, mDrawPaint);


その他のTips

その他お問い合わせはFacebookなどでお受けしております。
汎用的なTipsはこちらに追加していきたいと思います。

前:Watch Faceを作る コンパニオンアプリの作成 次:baserCMSのコンテストでノージャンル賞を受賞しました

関連キーワード

[Android][Java][モバイル][IT][ウェアラブル]

コメントを投稿する

名前URI
コメント