LoginSignup
6
6

More than 5 years have passed since last update.

Android Studioを使用して、とりあえず動くWatch faceを作る(2/3)

Last updated at Posted at 2015-01-03



1/3からの続きです。

Serviceの実装

リソースファイルの追加

画像を追加します。

スクリーンショット 2015-01-03 22.39.56.png

  • watchfaceの背景画像と、プレビュー用の画像を追加します。
  • とりあえず、背景画像として以下の様な320*320くらいの黒で塗りつぶしたファイルを用意しましょう。ファイル名は、ここでは"bg.xxx"とします。(xxxはpngやjpg等、画像形式にあわせて入力して下さい)
  • そのファイルを、wear→res→drawableに入れます。画像をコピーして、drawableの上でペーストすると、入ります。

background.png

  • 次に、プレビュー用の画像を追加します。
  • プレビュー用の画像は、円形のwearableと四角形のwearable用に2種類用意する必要があります。
  • 中身は同じでも問題ないのですが、とりあえず以下のような画像ファイルを用意しました。
  • 背景画像と同様に、drawableに入れます。ファイル名は、それぞれ"preview.xxx"、"preview_circular.xxx"とします。

preview.png preview_circular.png

コードの記述

1/3で作成した、AnalogWatchFaceServiceに、以下を記述しました。とりあえず動作確認用に使用するだけなので、コピペしてしまってください。なお、このコードは、SDKのサンプルをそのまま使用しています。(android-sdk/samples/android-21/wearable/WatchFace)

public class AnalogWatchFaceService extends CanvasWatchFaceService {
  private static final String TAG = "AnalogWatchFaceService";

  /**
   * Update rate in milliseconds for interactive mode. We update once a second to advance the
   * second hand.
   */
  private static final long INTERACTIVE_UPDATE_RATE_MS = TimeUnit.SECONDS.toMillis(1);

  @Override
  public Engine onCreateEngine() {
    return new Engine();
  }

  private class Engine extends CanvasWatchFaceService.Engine {
    static final int MSG_UPDATE_TIME = 0;

    static final float TWO_PI = (float) Math.PI * 2f;

    Paint mHourPaint;
    Paint mMinutePaint;
    Paint mSecondPaint;
    Paint mTickPaint;
    boolean mMute;
    Calendar mCalendar;

    /**
     * Handler to update the time once a second in interactive mode.
     */
    final Handler mUpdateTimeHandler = new Handler() {
      @Override
      public void handleMessage(Message message) {
        switch (message.what) {
          case MSG_UPDATE_TIME:
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
              Log.v(TAG, "updating time");
            }
            invalidate();
            if (shouldTimerBeRunning()) {
              long timeMs = System.currentTimeMillis();
              long delayMs = INTERACTIVE_UPDATE_RATE_MS
                  - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
              mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
            }
            break;
        }
      }
    };

    final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        mCalendar.setTimeZone(TimeZone.getDefault());
        invalidate();
      }
    };
    boolean mRegisteredTimeZoneReceiver = false;

    /**
     * Whether the display supports fewer bits for each color in ambient mode. When true, we
     * disable anti-aliasing in ambient mode.
     */
    boolean mLowBitAmbient;

    Bitmap mBackgroundBitmap;
    Bitmap mBackgroundScaledBitmap;

    @Override
    public void onCreate(SurfaceHolder holder) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "onCreate");
      }
      super.onCreate(holder);

      setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
          .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
          .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
          .setShowSystemUiTime(false)
          .build());

      Resources resources = AnalogWatchFaceService.this.getResources();
      Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg, null /* theme */);
      mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

      mHourPaint = new Paint();
      mHourPaint.setARGB(255, 200, 200, 200);
      mHourPaint.setStrokeWidth(5.f);
      mHourPaint.setAntiAlias(true);
      mHourPaint.setStrokeCap(Paint.Cap.ROUND);

      mMinutePaint = new Paint();
      mMinutePaint.setARGB(255, 200, 200, 200);
      mMinutePaint.setStrokeWidth(3.f);
      mMinutePaint.setAntiAlias(true);
      mMinutePaint.setStrokeCap(Paint.Cap.ROUND);

      mSecondPaint = new Paint();
      mSecondPaint.setARGB(255, 255, 0, 0);
      mSecondPaint.setStrokeWidth(2.f);
      mSecondPaint.setAntiAlias(true);
      mSecondPaint.setStrokeCap(Paint.Cap.ROUND);

      mTickPaint = new Paint();
      mTickPaint.setARGB(100, 255, 255, 255);
      mTickPaint.setStrokeWidth(2.f);
      mTickPaint.setAntiAlias(true);

      mCalendar = Calendar.getInstance();
    }

    @Override
    public void onDestroy() {
      mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
      super.onDestroy();
    }

    @Override
    public void onPropertiesChanged(Bundle properties) {
      super.onPropertiesChanged(properties);
      mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + mLowBitAmbient);
      }
    }

    @Override
    public void onTimeTick() {
      super.onTimeTick();
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());
      }
      invalidate();
    }

    @Override
    public void onAmbientModeChanged(boolean inAmbientMode) {
      super.onAmbientModeChanged(inAmbientMode);
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);
      }
      if (mLowBitAmbient) {
        boolean antiAlias = !inAmbientMode;
        mHourPaint.setAntiAlias(antiAlias);
        mMinutePaint.setAntiAlias(antiAlias);
        mSecondPaint.setAntiAlias(antiAlias);
        mTickPaint.setAntiAlias(antiAlias);
      }
      invalidate();

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

    @Override
    public void onInterruptionFilterChanged(int interruptionFilter) {
      super.onInterruptionFilterChanged(interruptionFilter);
      boolean inMuteMode = (interruptionFilter == WatchFaceService.INTERRUPTION_FILTER_NONE);
      if (mMute != inMuteMode) {
        mMute = inMuteMode;
        mHourPaint.setAlpha(inMuteMode ? 100 : 255);
        mMinutePaint.setAlpha(inMuteMode ? 100 : 255);
        mSecondPaint.setAlpha(inMuteMode ? 80 : 255);
        invalidate();
      }
    }

    @Override
    public void onDraw(Canvas canvas, Rect bounds) {
      mCalendar.setTimeInMillis(System.currentTimeMillis());

      int width = bounds.width();
      int height = bounds.height();

      // Draw the background, scaled to fit.
      if (mBackgroundScaledBitmap == null
          || mBackgroundScaledBitmap.getWidth() != width
          || mBackgroundScaledBitmap.getHeight() != height) {
        mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
            width, height, true /* filter */);
      }
      canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);

      // Find the center. Ignore the window insets so that, on round watches with a
      // "chin", the watch face is centered on the entire screen, not just the usable
      // portion.
      float centerX = width / 2f;
      float centerY = height / 2f;

      // Draw the ticks.
      float innerTickRadius = centerX - 10;
      float outerTickRadius = centerX;
      for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
        float tickRot = tickIndex * TWO_PI / 12;
        float innerX = (float) Math.sin(tickRot) * innerTickRadius;
        float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
        float outerX = (float) Math.sin(tickRot) * outerTickRadius;
        float outerY = (float) -Math.cos(tickRot) * outerTickRadius;
        canvas.drawLine(centerX + innerX, centerY + innerY,
            centerX + outerX, centerY + outerY, mTickPaint);
      }

      float seconds =
          mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f;
      float secRot = seconds / 60f * TWO_PI;
      float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f;
      float minRot = minutes / 60f * TWO_PI;
      float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f;
      float hrRot = hours / 12f * TWO_PI;

      float secLength = centerX - 20;
      float minLength = centerX - 40;
      float hrLength = centerX - 80;

      if (!isInAmbientMode()) {
        float secX = (float) Math.sin(secRot) * secLength;
        float secY = (float) -Math.cos(secRot) * secLength;
        canvas.drawLine(centerX, centerY, centerX + secX, centerY + secY, mSecondPaint);
      }

      float minX = (float) Math.sin(minRot) * minLength;
      float minY = (float) -Math.cos(minRot) * minLength;
      canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY, mMinutePaint);

      float hrX = (float) Math.sin(hrRot) * hrLength;
      float hrY = (float) -Math.cos(hrRot) * hrLength;
      canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY, mHourPaint);
    }

    @Override
    public void onVisibilityChanged(boolean visible) {
      super.onVisibilityChanged(visible);
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "onVisibilityChanged: " + visible);
      }

      if (visible) {
        registerReceiver();

        // Update time zone in case it changed while we weren't visible.
        mCalendar.setTimeZone(TimeZone.getDefault());
      } else {
        unregisterReceiver();
      }

      // 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();
    }

    private void registerReceiver() {
      if (mRegisteredTimeZoneReceiver) {
        return;
      }
      mRegisteredTimeZoneReceiver = true;
      IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
      AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
    }

    private void unregisterReceiver() {
      if (!mRegisteredTimeZoneReceiver) {
        return;
      }
      mRegisteredTimeZoneReceiver = false;
      AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
    }

    /**
     * Starts the {@link #mUpdateTimeHandler} timer if it should be running and isn't currently
     * or stops it if it shouldn't be running but currently is.
     */
    private void updateTimer() {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "updateTimer");
      }
      mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
      if (shouldTimerBeRunning()) {
        mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
      }
    }

    /**
     * Returns whether the {@link #mUpdateTimeHandler} timer should be running. The timer should
     * only run when we're visible and in interactive mode.
     */
    private boolean shouldTimerBeRunning() {
      return isVisible() && !isInAmbientMode();
    }

  }
}

実行

ここまででエラーがでていなければ、ひとまずプログラムは完成です。
早速、エミュレーター及び実機で実行してみましょう。

エミュレーターでの実行

エミュレーターの作成及び起動

スクリーンショット 2015-01-03 23.02.41.png

  • まず、エミュレーターを作成します。
  • Tools→Android→AVD Managerを選択し、AVD Managerを起動します。
  • Create Virtual Device...を選択します。
  • 左側の"Category"から、"Wear"を選択します。
  • 右側の、Android Wear SquareまたはAndroid Wear Roundを選択します。
  • System Imageは、とりあえずLolipop, API Level21, x86のものを選択しておけばいいと思います。
  • "Use Host GPU"には、一応チェックをつけておき、Finishで完成です。
  • 完成したら、Actionsから再生ボタンを押して、エミュレーターを起動しましょう。

スクリーンショット 2015-01-03 23.10.23.png

Wearableの実行

スクリーンショット 2015-01-03 23.13.38.png

  • まず、Run→Edit Configurations...を選択します。
  • 左側から、wearを選択します。(現在は、赤でバツが付いていると思います)
  • 右側の、Activityの部分で、Do not launch Activity を選択し、OKとします。

スクリーンショット 2015-01-03 23.15.42.png

  • 以下から、wearを選択します。

スクリーンショット 2015-01-03 23.17.55.png

  • Run→Run wearを選択します。

スクリーンショット 2015-01-03 23.19.42.png

  • Choose a running Deviceで、先ほど作成したエミュレーターを選択します。
  • 画面下部の、4:Runを選択すると、現在の状況が表示されます。こちらの最終行に、Successと表示されるまで待ちます。

スクリーンショット 2015-01-03 23.26.22.png

  • エミュレーター上で、時計が表示されている画面で、クリック長押しをすると、watch face選択画面に移行します。その中に、My Applicationという名前で、先ほど作成したプレビュー画像があると思います。
  • そちらを選択すると、Watch faceが変更されます。

スクリーンショット 2015-01-03 23.30.01.png

スクリーンショット 2015-01-03 23.31.46.png

実機での実行

以下が詳しいです。接続ができたら、エミュレーターと同様に実行します。

Android Wear実機へのadb接続

カスタマイズ等

3/3に続きます。

参考にしたサイト等

美女時計を作ろう!Android Wear開発入門 30分で作る盤面アプリ
Building a Watch Face Service(Android developers)

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6