1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.wearable.watchface; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.opengl.GLES20; 24 import android.opengl.Matrix; 25 import android.support.wearable.watchface.Gles2WatchFaceService; 26 import android.support.wearable.watchface.WatchFaceStyle; 27 import android.util.Log; 28 import android.view.Gravity; 29 import android.view.SurfaceHolder; 30 31 import java.util.Calendar; 32 import java.util.TimeZone; 33 import java.util.concurrent.TimeUnit; 34 35 /** 36 * Sample watch face using OpenGL. The watch face is rendered using 37 * {@link Gles2ColoredTriangleList}s. The camera moves around in interactive mode and stops moving 38 * when the watch enters ambient mode. 39 */ 40 public class OpenGLWatchFaceService extends Gles2WatchFaceService { 41 42 private static final String TAG = "OpenGLWatchFaceService"; 43 44 /** Expected frame rate in interactive mode. */ 45 private static final long FPS = 60; 46 47 /** Z distance from the camera to the watchface. */ 48 private static final float EYE_Z = -2.3f; 49 50 /** How long each frame is displayed at expected frame rate. */ 51 private static final long FRAME_PERIOD_MS = TimeUnit.SECONDS.toMillis(1) / FPS; 52 53 @Override onCreateEngine()54 public Engine onCreateEngine() { 55 return new Engine(); 56 } 57 58 private class Engine extends Gles2WatchFaceService.Engine { 59 /** Cycle time before the camera motion repeats. */ 60 private static final long CYCLE_PERIOD_SECONDS = 5; 61 62 /** Number of camera angles to precompute. */ 63 private final int mNumCameraAngles = (int) (CYCLE_PERIOD_SECONDS * FPS); 64 65 /** Projection transformation matrix. Converts from 3D to 2D. */ 66 private final float[] mProjectionMatrix = new float[16]; 67 68 /** 69 * View transformation matrices to use in interactive mode. Converts from world to camera- 70 * relative coordinates. One matrix per camera position. 71 */ 72 private final float[][] mViewMatrices = new float[mNumCameraAngles][16]; 73 74 /** The view transformation matrix to use in ambient mode */ 75 private final float[] mAmbientViewMatrix = new float[16]; 76 77 /** 78 * Model transformation matrices. Converts from model-relative coordinates to world 79 * coordinates. One matrix per degree of rotation. 80 */ 81 private final float[][] mModelMatrices = new float[360][16]; 82 83 /** 84 * Products of {@link #mViewMatrices} and {@link #mProjectionMatrix}. One matrix per camera 85 * position. 86 */ 87 private final float[][] mVpMatrices = new float[mNumCameraAngles][16]; 88 89 /** The product of {@link #mAmbientViewMatrix} and {@link #mProjectionMatrix} */ 90 private final float[] mAmbientVpMatrix = new float[16]; 91 92 /** 93 * Product of {@link #mModelMatrices}, {@link #mViewMatrices}, and 94 * {@link #mProjectionMatrix}. 95 */ 96 private final float[] mMvpMatrix = new float[16]; 97 98 /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */ 99 private Gles2ColoredTriangleList mMajorTickTriangles; 100 101 /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */ 102 private Gles2ColoredTriangleList mMinorTickTriangles; 103 104 /** Triangle for the second hand. */ 105 private Gles2ColoredTriangleList mSecondHandTriangle; 106 107 /** Triangle for the minute hand. */ 108 private Gles2ColoredTriangleList mMinuteHandTriangle; 109 110 /** Triangle for the hour hand. */ 111 private Gles2ColoredTriangleList mHourHandTriangle; 112 113 private Calendar mCalendar = Calendar.getInstance(); 114 115 /** Whether we've registered {@link #mTimeZoneReceiver}. */ 116 private boolean mRegisteredTimeZoneReceiver; 117 118 private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { 119 @Override 120 public void onReceive(Context context, Intent intent) { 121 mCalendar.setTimeZone(TimeZone.getDefault()); 122 invalidate(); 123 } 124 }; 125 126 @Override onCreate(SurfaceHolder surfaceHolder)127 public void onCreate(SurfaceHolder surfaceHolder) { 128 if (Log.isLoggable(TAG, Log.DEBUG)) { 129 Log.d(TAG, "onCreate"); 130 } 131 super.onCreate(surfaceHolder); 132 setWatchFaceStyle(new WatchFaceStyle.Builder(OpenGLWatchFaceService.this) 133 .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) 134 .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) 135 .setStatusBarGravity(Gravity.RIGHT | Gravity.TOP) 136 .setHotwordIndicatorGravity(Gravity.LEFT | Gravity.TOP) 137 .setShowSystemUiTime(false) 138 .build()); 139 } 140 141 @Override onGlContextCreated()142 public void onGlContextCreated() { 143 if (Log.isLoggable(TAG, Log.DEBUG)) { 144 Log.d(TAG, "onGlContextCreated"); 145 } 146 super.onGlContextCreated(); 147 148 // Create program for drawing triangles. 149 Gles2ColoredTriangleList.Program triangleProgram = 150 new Gles2ColoredTriangleList.Program(); 151 152 // We only draw triangles which all use the same program so we don't need to switch 153 // programs mid-frame. This means we can tell OpenGL to use this program only once 154 // rather than having to do so for each frame. This makes OpenGL draw faster. 155 triangleProgram.use(); 156 157 // Create triangles for the ticks. 158 mMajorTickTriangles = createMajorTicks(triangleProgram); 159 mMinorTickTriangles = createMinorTicks(triangleProgram); 160 161 // Create triangles for the hands. 162 mSecondHandTriangle = createHand( 163 triangleProgram, 164 0.02f /* width */, 165 1.0f /* height */, 166 new float[]{ 167 1.0f /* red */, 168 0.0f /* green */, 169 0.0f /* blue */, 170 1.0f /* alpha */ 171 } 172 ); 173 mMinuteHandTriangle = createHand( 174 triangleProgram, 175 0.06f /* width */, 176 1f /* height */, 177 new float[]{ 178 0.7f /* red */, 179 0.7f /* green */, 180 0.7f /* blue */, 181 1.0f /* alpha */ 182 } 183 ); 184 mHourHandTriangle = createHand( 185 triangleProgram, 186 0.1f /* width */, 187 0.6f /* height */, 188 new float[]{ 189 0.9f /* red */, 190 0.9f /* green */, 191 0.9f /* blue */, 192 1.0f /* alpha */ 193 } 194 ); 195 196 // Precompute the clock angles. 197 for (int i = 0; i < mModelMatrices.length; ++i) { 198 Matrix.setRotateM(mModelMatrices[i], 0, i, 0, 0, 1); 199 } 200 201 // Precompute the camera angles. 202 for (int i = 0; i < mNumCameraAngles; ++i) { 203 // Set the camera position (View matrix). When active, move the eye around to show 204 // off that this is 3D. 205 final float cameraAngle = (float) (((float) i) / mNumCameraAngles * 2 * Math.PI); 206 final float eyeX = (float) Math.cos(cameraAngle); 207 final float eyeY = (float) Math.sin(cameraAngle); 208 Matrix.setLookAtM(mViewMatrices[i], 209 0, // dest index 210 eyeX, eyeY, EYE_Z, // eye 211 0, 0, 0, // center 212 0, 1, 0); // up vector 213 } 214 215 Matrix.setLookAtM(mAmbientViewMatrix, 216 0, // dest index 217 0, 0, EYE_Z, // eye 218 0, 0, 0, // center 219 0, 1, 0); // up vector 220 } 221 222 @Override onGlSurfaceCreated(int width, int height)223 public void onGlSurfaceCreated(int width, int height) { 224 if (Log.isLoggable(TAG, Log.DEBUG)) { 225 Log.d(TAG, "onGlSurfaceCreated: " + width + " x " + height); 226 } 227 super.onGlSurfaceCreated(width, height); 228 229 // Update the projection matrix based on the new aspect ratio. 230 final float aspectRatio = (float) width / height; 231 Matrix.frustumM(mProjectionMatrix, 232 0 /* offset */, 233 -aspectRatio /* left */, 234 aspectRatio /* right */, 235 -1 /* bottom */, 236 1 /* top */, 237 2 /* near */, 238 7 /* far */); 239 240 // Precompute the products of Projection and View matrices for each camera angle. 241 for (int i = 0; i < mNumCameraAngles; ++i) { 242 Matrix.multiplyMM(mVpMatrices[i], 0, mProjectionMatrix, 0, mViewMatrices[i], 0); 243 } 244 245 Matrix.multiplyMM(mAmbientVpMatrix, 0, mProjectionMatrix, 0, mAmbientViewMatrix, 0); 246 } 247 248 /** 249 * Creates a triangle for a hand on the watch face. 250 * 251 * @param program program for drawing triangles 252 * @param width width of base of triangle 253 * @param length length of triangle 254 * @param color color in RGBA order, each in the range [0, 1] 255 */ createHand(Gles2ColoredTriangleList.Program program, float width, float length, float[] color)256 private Gles2ColoredTriangleList createHand(Gles2ColoredTriangleList.Program program, 257 float width, float length, float[] color) { 258 // Create the data for the VBO. 259 float[] triangleCoords = new float[]{ 260 // in counterclockwise order: 261 0, length, 0, // top 262 -width / 2, 0, 0, // bottom left 263 width / 2, 0, 0 // bottom right 264 }; 265 return new Gles2ColoredTriangleList(program, triangleCoords, color); 266 } 267 268 /** 269 * Creates a triangle list for the major ticks on the watch face. 270 * 271 * @param program program for drawing triangles 272 */ createMajorTicks( Gles2ColoredTriangleList.Program program)273 private Gles2ColoredTriangleList createMajorTicks( 274 Gles2ColoredTriangleList.Program program) { 275 // Create the data for the VBO. 276 float[] trianglesCoords = new float[9 * 4]; 277 for (int i = 0; i < 4; i++) { 278 float[] triangleCoords = getMajorTickTriangleCoords(i); 279 System.arraycopy(triangleCoords, 0, trianglesCoords, i * 9, triangleCoords.length); 280 } 281 282 return new Gles2ColoredTriangleList(program, trianglesCoords, 283 new float[]{ 284 1.0f /* red */, 285 1.0f /* green */, 286 1.0f /* blue */, 287 1.0f /* alpha */ 288 } 289 ); 290 } 291 292 /** 293 * Creates a triangle list for the minor ticks on the watch face. 294 * 295 * @param program program for drawing triangles 296 */ createMinorTicks( Gles2ColoredTriangleList.Program program)297 private Gles2ColoredTriangleList createMinorTicks( 298 Gles2ColoredTriangleList.Program program) { 299 // Create the data for the VBO. 300 float[] trianglesCoords = new float[9 * (12 - 4)]; 301 int index = 0; 302 for (int i = 0; i < 12; i++) { 303 if (i % 3 == 0) { 304 // This is where a major tick goes, so skip it. 305 continue; 306 } 307 float[] triangleCoords = getMinorTickTriangleCoords(i); 308 System.arraycopy(triangleCoords, 0, trianglesCoords, index, triangleCoords.length); 309 index += 9; 310 } 311 312 return new Gles2ColoredTriangleList(program, trianglesCoords, 313 new float[]{ 314 0.5f /* red */, 315 0.5f /* green */, 316 0.5f /* blue */, 317 1.0f /* alpha */ 318 } 319 ); 320 } 321 getMajorTickTriangleCoords(int index)322 private float[] getMajorTickTriangleCoords(int index) { 323 return getTickTriangleCoords(0.03f /* width */, 0.09f /* length */, 324 index * 360 / 4 /* angleDegrees */); 325 } 326 getMinorTickTriangleCoords(int index)327 private float[] getMinorTickTriangleCoords(int index) { 328 return getTickTriangleCoords(0.02f /* width */, 0.06f /* length */, 329 index * 360 / 12 /* angleDegrees */); 330 } 331 getTickTriangleCoords(float width, float length, int angleDegrees)332 private float[] getTickTriangleCoords(float width, float length, int angleDegrees) { 333 // Create the data for the VBO. 334 float[] coords = new float[]{ 335 // in counterclockwise order: 336 0, 1, 0, // top 337 width / 2, length + 1, 0, // bottom left 338 -width / 2, length + 1, 0 // bottom right 339 }; 340 341 rotateCoords(coords, angleDegrees); 342 return coords; 343 } 344 345 /** 346 * Destructively rotates the given coordinates in the XY plane about the origin by the given 347 * angle. 348 * 349 * @param coords flattened 3D coordinates 350 * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the 351 * Z axis 352 */ rotateCoords(float[] coords, int angleDegrees)353 private void rotateCoords(float[] coords, int angleDegrees) { 354 double angleRadians = Math.toRadians(angleDegrees); 355 double cos = Math.cos(angleRadians); 356 double sin = Math.sin(angleRadians); 357 for (int i = 0; i < coords.length; i += 3) { 358 float x = coords[i]; 359 float y = coords[i + 1]; 360 coords[i] = (float) (cos * x - sin * y); 361 coords[i + 1] = (float) (sin * x + cos * y); 362 } 363 } 364 365 @Override onAmbientModeChanged(boolean inAmbientMode)366 public void onAmbientModeChanged(boolean inAmbientMode) { 367 if (Log.isLoggable(TAG, Log.DEBUG)) { 368 Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); 369 } 370 super.onAmbientModeChanged(inAmbientMode); 371 invalidate(); 372 } 373 374 @Override onVisibilityChanged(boolean visible)375 public void onVisibilityChanged(boolean visible) { 376 if (Log.isLoggable(TAG, Log.DEBUG)) { 377 Log.d(TAG, "onVisibilityChanged: " + visible); 378 } 379 super.onVisibilityChanged(visible); 380 if (visible) { 381 registerReceiver(); 382 383 // Update time zone in case it changed while we were detached. 384 mCalendar.setTimeZone(TimeZone.getDefault()); 385 386 invalidate(); 387 } else { 388 unregisterReceiver(); 389 } 390 } 391 registerReceiver()392 private void registerReceiver() { 393 if (mRegisteredTimeZoneReceiver) { 394 return; 395 } 396 mRegisteredTimeZoneReceiver = true; 397 IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); 398 OpenGLWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); 399 } 400 unregisterReceiver()401 private void unregisterReceiver() { 402 if (!mRegisteredTimeZoneReceiver) { 403 return; 404 } 405 mRegisteredTimeZoneReceiver = false; 406 OpenGLWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); 407 } 408 409 @Override onTimeTick()410 public void onTimeTick() { 411 super.onTimeTick(); 412 if (Log.isLoggable(TAG, Log.DEBUG)) { 413 Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode()); 414 } 415 invalidate(); 416 } 417 418 @Override onDraw()419 public void onDraw() { 420 if (Log.isLoggable(TAG, Log.VERBOSE)) { 421 Log.v(TAG, "onDraw"); 422 } 423 super.onDraw(); 424 final float[] vpMatrix; 425 426 // Draw background color and select the appropriate view projection matrix. The 427 // background should always be black in ambient mode. The view projection matrix used is 428 // overhead in ambient. In interactive mode, it's tilted depending on the current time. 429 if (isInAmbientMode()) { 430 GLES20.glClearColor(0, 0, 0, 1); 431 vpMatrix = mAmbientVpMatrix; 432 } else { 433 GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1); 434 final int cameraIndex = 435 (int) ((System.currentTimeMillis() / FRAME_PERIOD_MS) % mNumCameraAngles); 436 vpMatrix = mVpMatrices[cameraIndex]; 437 } 438 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); 439 440 // Compute angle indices for the three hands. 441 mCalendar.setTimeInMillis(System.currentTimeMillis()); 442 float seconds = 443 mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f; 444 float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f; 445 float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f; 446 final int secIndex = (int) (seconds / 60f * 360f); 447 final int minIndex = (int) (minutes / 60f * 360f); 448 final int hoursIndex = (int) (hours / 12f * 360f); 449 450 // Draw triangles from back to front. Don't draw the second hand in ambient mode. 451 452 // Combine the model matrix with the projection and camera view. 453 Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[hoursIndex], 0); 454 455 // Draw the triangle. 456 mHourHandTriangle.draw(mMvpMatrix); 457 458 // Combine the model matrix with the projection and camera view. 459 Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[minIndex], 0); 460 461 // Draw the triangle. 462 mMinuteHandTriangle.draw(mMvpMatrix); 463 if (!isInAmbientMode()) { 464 // Combine the model matrix with the projection and camera view. 465 Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[secIndex], 0); 466 467 // Draw the triangle. 468 mSecondHandTriangle.draw(mMvpMatrix); 469 } 470 471 // Draw the major and minor ticks. 472 mMajorTickTriangles.draw(vpMatrix); 473 mMinorTickTriangles.draw(vpMatrix); 474 475 // Draw every frame as long as we're visible and in interactive mode. 476 if (isVisible() && !isInAmbientMode()) { 477 invalidate(); 478 } 479 } 480 } 481 } 482