Help us understand the problem. What is going on with this article?

AndroidWearのFace(文字盤)をアニメのキャラにしたかったので自作した話(作成手順公開)

More than 3 years have passed since last update.

はじめに

最近、前々から興味があった「Moto360」というAndroidWear搭載のスマートウォッチを購入しました。
(新型が出たみたいでめっちゃ安くなってました)
amu4irhdburpwbmn3qpi.jpg

購入した当日
「とりあえずFaceはアニメキャラだろ常考」
ということで、早速ストアに探しにいきました。
が!しかし!!

好きなキャラどころか、キャラのFaceが全くない!!

な、なんてことだ…何のために…スマートウォッチを買ったんだ…
と、買ったばっかの「Moto360」がいきなりお蔵入り寸前。
ですが僕は思うわけです。

ないなら自分で作ればいいじゃない!!

そう、僕はエンジニアです。ないなら自分で作るしかない!
ということでAndroidWearのFaceを自作してみました。

とりあえずプロジェクトを作成してみる

とりあえずプロジェクトを作成してみよう。
ということで早速AndroidStudioを立ち上げて作成してみる。
ですが、AndroidStudioのバージョンが1.1.0と低かったためAndroidWearのプロジェクトが作成できませんでした。
なので、AndroidStudioを1.5.0にあげて再チャレンジ!
無事サンプルプロジェクトを作成出来ました。

プロジェクト作成手順

下記画像の通りにプロジェクトを作成しました。

まずはプロジェクトネームを決める

スクリーンショット 2015-12-03 17.38.00.png

AndroidWearを選択

Create_New_Project.png

Faceを選択

Create_New_Project 2.png

Faceのタイプを設定

Create_New_Project.png

以上でプロジェクトが作成されました。

コードを読んで書いてみる

プロジェクトが作成されると下記図のような構成になっていました。
MyWatchFace_java_-_Umr_-____AndroidStudioProjects_Umr_.png

どうやら、MyWatchFaceが重要そう…ということで読んでみると3つのクラスで構成されていました。

  • MyWatchFace・・・本体となるクラス。ここは特に弄らなくても大丈夫そう
  • Engine・・・ ライフサイクルを持っているAcitivyみたいなやつ。ここがかなり要
  • EngineHandler・・・ 定時間後にEngineの処理を呼びます。ここも特に弄ってない

ほうほうどうやらEngineクラスが重要そうだな…
ということで、重点的に読みコメントアウトを入れて、背景、キャラを設定出来るようにしてみました。
(コードは雑ですが悪しからず)

Engine
private class Engine extends CanvasWatchFaceService.Engine {
    // 一定時間ごとのイベントを取り扱うためのEngineHandlerを持つ
    final Handler mUpdateTimeHandler = new EngineHandler(this);

    //定期的に呼ばれる。タイムゾーンが変更されてもここで自動的に合わせてくれる
    final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mTime.clear(intent.getStringExtra("time-zone"));
            mTime.setToNow();
        }
    };

    // タイムゾーンの変更時のReceiverが登録されているかどうかを持つ
    boolean mRegisteredTimeZoneReceiver = false;

    // 背景描画
    Bitmap mBackgroundBitmap;
    Bitmap mCharacterBitmap;
    Bitmap mAmbienteBitmap;
    // 表示するテキスト描画
    Paint mTextPaint;
    Paint mTextLinePaint;

    // ambienteモードかのフラグ
    boolean mAmbient;
    // 時間を保存する。(2032年移行使えないの要注意)
    Time mTime;

    float mXOffset;
    float mYOffset;

    // Ambient時に白黒2値をサポートするかどうかを格納する
    boolean mLowBitAmbient;




    @Override
    // Watch Faceは実行時間がかなり長くなるのと、普段使用時の消費電力を極端に抑える必要が有ることから、1回で済む処理は全てここで記載してしまい、あとから行う処理を最小限にする。
    public void onCreate(SurfaceHolder holder) {
        super.onCreate(holder);

        setWatchFaceStyle(new WatchFaceStyle.Builder(MyWatchFace.this)
                 // 通知が来た時にどのようにWatchFace上に通知を見せるかを指定
                .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
                 // 通知が来た時に背景を表示するかどうかを指定
                .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
                 // システムが提供する標準的なデジタルの時刻表示を行うかどうかを指定
                .setShowSystemUiTime(false)
                .build());

        // 設定してるリソースを取得する
        Resources resources = MyWatchFace.this.getResources();
        mYOffset = resources.getDimension(R.dimen.digital_y_offset);

        // バックグラウンドイメージの画像設定
        mBackgroundBitmap = ((BitmapDrawable) resources.getDrawable(R.mipmap.preview)).getBitmap();
        // キャラの画像設定
        mCharacterBitmap  = ((BitmapDrawable) resources.getDrawable(R.mipmap.char_1)).getBitmap();
        // ambienteモード時の画像設定
        mAmbienteBitmap   = ((BitmapDrawable) resources.getDrawable(R.mipmap.ambiente)).getBitmap();

        // 表示する文字盤の設定
        mTextPaint = new Paint();
        mTextPaint = createTextPaint(resources.getColor(R.color.digital_text));

        // 表示する文字盤の枠設定
        mTextLinePaint = new Paint();
        mTextLinePaint = createTextLinePaint(resources.getColor(R.color.digital_line_text));

        // 時刻の初期化
        mTime = new Time();
    }

    @Override
    // 一定時間ごとに処理を行うmUpdateTimeHandlerのメッセージを削除して、これ以上処理が行われないようにする。
    public void onDestroy() {
        mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
        super.onDestroy();
    }

    private Paint createTextPaint(int textColor) {
        Paint paint = new Paint();
        paint.setColor(textColor);
        paint.setTypeface(NORMAL_TYPEFACE);
        paint.setAntiAlias(true);
        return paint;
    }

    private Paint createTextLinePaint(int textColor) {
        Paint paint = new Paint();
        paint.setColor(textColor);
        paint.setStrokeWidth(5.0f);
        paint.setTypeface(NORMAL_TYPEFACE);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        return paint;
    }

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

        if (visible) {
            registerReceiver();

            mTime.clear(TimeZone.getDefault().getID());
            mTime.setToNow();
        } else {
            unregisterReceiver();
        }


        updateTimer();
    }

    // タイムゾーンが変更された時に呼び出されるReceiverを登録
    private void registerReceiver() {
        if (mRegisteredTimeZoneReceiver) {
            return;
        }
        mRegisteredTimeZoneReceiver = true;
        IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
        MyWatchFace.this.registerReceiver(mTimeZoneReceiver, filter);
    }

    // registerReceiverで登録したReceiverを解除
    private void unregisterReceiver() {
        if (!mRegisteredTimeZoneReceiver) {
            return;
        }
        mRegisteredTimeZoneReceiver = false;
        MyWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
    }

    @Override
    public void onApplyWindowInsets(WindowInsets insets) {
        super.onApplyWindowInsets(insets);

        Resources resources = MyWatchFace.this.getResources();
        boolean isRound = insets.isRound();
        mXOffset = resources.getDimension(isRound
                ? R.dimen.digital_x_offset_round : R.dimen.digital_x_offset);
        float textSize = resources.getDimension(isRound
                ? R.dimen.digital_text_size_round : R.dimen.digital_text_size);

        mTextPaint.setTextSize(textSize);
        mTextLinePaint.setTextSize(textSize);
    }

    @Override
    /*
    プロパティーが変更された時に呼び出されます。
    ここではAmbient時に白黒2値をサポートしているかを判別し保存しています。
     */
    public void onPropertiesChanged(Bundle properties) {
        super.onPropertiesChanged(properties);
        mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
    }

    @Override
    // 一定時間ごとに呼び出されます。invalidate()を呼んで画面の再描画を促します。
    public void onTimeTick() {
        super.onTimeTick();
         // 描画する
        invalidate();
    }

    @Override
    // ambienteになった際に呼ばれる
    public void onAmbientModeChanged(boolean inAmbientMode) {
        super.onAmbientModeChanged(inAmbientMode);
        // 現在ambienteなのか判定
        if (mAmbient != inAmbientMode) {
            mAmbient = inAmbientMode;
            // ambienteが白黒モードに対応しているかの判定
            if (mLowBitAmbient) {
                mTextPaint.setAntiAlias(!inAmbientMode);
            }
            // 描画する
            invalidate();
        }

        updateTimer();
    }



    @Override
     // 描画を実際に行う(ここは極力小さくする)
    public void onDraw(Canvas canvas, Rect bounds) {
        // Draw the background.
        if (isInAmbientMode()) {
            canvas.drawColor(Color.BLACK);
            canvas.drawBitmap(mAmbienteBitmap, 20, 80, null);
        } else {
            canvas.drawBitmap(mBackgroundBitmap, 0, 0, null);
            canvas.drawBitmap(mCharacterBitmap, 60, 70, null);
        }

        mTime.setToNow();
        String text = String.format("%d:%02d", mTime.hour, mTime.minute);

        if(!isInAmbientMode()) canvas.drawText(text, mXOffset, mYOffset, mTextLinePaint);
        canvas.drawText(text, mXOffset, mYOffset, mTextPaint);
    }


     // タイマーを解除した後、1秒毎に更新が必要な場合、タイマーを設定し直します。
    private void updateTimer() {
        mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
        if (shouldTimerBeRunning()) {
            mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
        }
    }

    // 1秒ごとに更新が必要かどうかを返します。
    // 1秒毎に更新が必要なのは画面が描画されておりambientModeではない時です。
    private boolean shouldTimerBeRunning() {
        return isVisible() && !isInAmbientMode();
    }

    private void handleUpdateTimeMessage() {
        invalidate();
        if (shouldTimerBeRunning()) {
            long timeMs = System.currentTimeMillis();
            long delayMs = INTERACTIVE_UPDATE_RATE_MS
                    - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
            mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
        }
    }
}

リソースの設置

背景、キャラの画像を設置
スクリーンショット 2015-12-17 15.13.08.png

文字色、アプリ名を設定

  • colors.xml・・・文字色を設定
  • strings.xml・・・アプリ名を設定

スクリーンショット 2015-12-03 18.46.13.png

ここまで来たらいよいよ実機に移すだけ!「Moto360」にインストールします!

実機で確認

「Moto360」はUSB接続出来ないのでBluetooth経由でインストールしました。

手順

【スマホ側】

①BluetoothをONにします。
②ビルド番号を連打して開発者モードを出します。
③USBデバッグ接続をONにします。

【Wear側】

①スマホ同様ビルド番号を連打して開発者モードを出します。
②ADBデバッグモードを有効にします。
③Bluetooth経由でデバッグを有効にします。

【PC側】

①ターミナルを起動します
②過去にインストールしたsdk/platform-tools以下に移動
③下記コマンドを入力

$ adb forward tcp:4444 localabstract:/adb-hub && adb connect localhost:4444

これで準備完了です。

いざ確認!!

スクリーンショット 2015-12-17 15.14.25.png

うおー!!うごいた〜〜!!!

P_20151203_170657.jpg
良い!めっちゃ良い!✌('ω'✌)三✌('ω')✌三(✌'ω')✌(テンションMAX)

つけた感じも良い!!

スクリーンショット 2015-12-03 19.15.48.png

満足のいく一品に仕上がりました。

さいごに

思ったよりも簡単にFaceを自作することが出来ました!
これで「Moto360」がお蔵入りせずに済みそうです!(歓喜)

次はタップしたら動くようにしてみよう…!

kakky0418
Golangに入門
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away