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