/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.android.wearable.watchface; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.opengl.GLES20; import android.opengl.Matrix; import android.support.wearable.watchface.Gles2WatchFaceService; import android.support.wearable.watchface.WatchFaceStyle; import android.util.Log; import android.view.Gravity; import android.view.SurfaceHolder; import java.util.Calendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; /** * Sample watch face using OpenGL. The watch face is rendered using * {@link Gles2ColoredTriangleList}s. The camera moves around in interactive mode and stops moving * when the watch enters ambient mode. */ public class OpenGLWatchFaceService extends Gles2WatchFaceService { private static final String TAG = "OpenGLWatchFaceService"; /** Expected frame rate in interactive mode. */ private static final long FPS = 60; /** Z distance from the camera to the watchface. */ private static final float EYE_Z = -2.3f; /** How long each frame is displayed at expected frame rate. */ private static final long FRAME_PERIOD_MS = TimeUnit.SECONDS.toMillis(1) / FPS; @Override public Engine onCreateEngine() { return new Engine(); } private class Engine extends Gles2WatchFaceService.Engine { /** Cycle time before the camera motion repeats. */ private static final long CYCLE_PERIOD_SECONDS = 5; /** Number of camera angles to precompute. */ private final int mNumCameraAngles = (int) (CYCLE_PERIOD_SECONDS * FPS); /** Projection transformation matrix. Converts from 3D to 2D. */ private final float[] mProjectionMatrix = new float[16]; /** * View transformation matrices to use in interactive mode. Converts from world to camera- * relative coordinates. One matrix per camera position. */ private final float[][] mViewMatrices = new float[mNumCameraAngles][16]; /** The view transformation matrix to use in ambient mode */ private final float[] mAmbientViewMatrix = new float[16]; /** * Model transformation matrices. Converts from model-relative coordinates to world * coordinates. One matrix per degree of rotation. */ private final float[][] mModelMatrices = new float[360][16]; /** * Products of {@link #mViewMatrices} and {@link #mProjectionMatrix}. One matrix per camera * position. */ private final float[][] mVpMatrices = new float[mNumCameraAngles][16]; /** The product of {@link #mAmbientViewMatrix} and {@link #mProjectionMatrix} */ private final float[] mAmbientVpMatrix = new float[16]; /** * Product of {@link #mModelMatrices}, {@link #mViewMatrices}, and * {@link #mProjectionMatrix}. */ private final float[] mMvpMatrix = new float[16]; /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */ private Gles2ColoredTriangleList mMajorTickTriangles; /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */ private Gles2ColoredTriangleList mMinorTickTriangles; /** Triangle for the second hand. */ private Gles2ColoredTriangleList mSecondHandTriangle; /** Triangle for the minute hand. */ private Gles2ColoredTriangleList mMinuteHandTriangle; /** Triangle for the hour hand. */ private Gles2ColoredTriangleList mHourHandTriangle; private Calendar mCalendar = Calendar.getInstance(); /** Whether we've registered {@link #mTimeZoneReceiver}. */ private boolean mRegisteredTimeZoneReceiver; private final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mCalendar.setTimeZone(TimeZone.getDefault()); invalidate(); } }; @Override public void onCreate(SurfaceHolder surfaceHolder) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onCreate"); } super.onCreate(surfaceHolder); setWatchFaceStyle(new WatchFaceStyle.Builder(OpenGLWatchFaceService.this) .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT) .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE) .setStatusBarGravity(Gravity.RIGHT | Gravity.TOP) .setHotwordIndicatorGravity(Gravity.LEFT | Gravity.TOP) .setShowSystemUiTime(false) .build()); } @Override public void onGlContextCreated() { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onGlContextCreated"); } super.onGlContextCreated(); // Create program for drawing triangles. Gles2ColoredTriangleList.Program triangleProgram = new Gles2ColoredTriangleList.Program(); // We only draw triangles which all use the same program so we don't need to switch // programs mid-frame. This means we can tell OpenGL to use this program only once // rather than having to do so for each frame. This makes OpenGL draw faster. triangleProgram.use(); // Create triangles for the ticks. mMajorTickTriangles = createMajorTicks(triangleProgram); mMinorTickTriangles = createMinorTicks(triangleProgram); // Create triangles for the hands. mSecondHandTriangle = createHand( triangleProgram, 0.02f /* width */, 1.0f /* height */, new float[]{ 1.0f /* red */, 0.0f /* green */, 0.0f /* blue */, 1.0f /* alpha */ } ); mMinuteHandTriangle = createHand( triangleProgram, 0.06f /* width */, 1f /* height */, new float[]{ 0.7f /* red */, 0.7f /* green */, 0.7f /* blue */, 1.0f /* alpha */ } ); mHourHandTriangle = createHand( triangleProgram, 0.1f /* width */, 0.6f /* height */, new float[]{ 0.9f /* red */, 0.9f /* green */, 0.9f /* blue */, 1.0f /* alpha */ } ); // Precompute the clock angles. for (int i = 0; i < mModelMatrices.length; ++i) { Matrix.setRotateM(mModelMatrices[i], 0, i, 0, 0, 1); } // Precompute the camera angles. for (int i = 0; i < mNumCameraAngles; ++i) { // Set the camera position (View matrix). When active, move the eye around to show // off that this is 3D. final float cameraAngle = (float) (((float) i) / mNumCameraAngles * 2 * Math.PI); final float eyeX = (float) Math.cos(cameraAngle); final float eyeY = (float) Math.sin(cameraAngle); Matrix.setLookAtM(mViewMatrices[i], 0, // dest index eyeX, eyeY, EYE_Z, // eye 0, 0, 0, // center 0, 1, 0); // up vector } Matrix.setLookAtM(mAmbientViewMatrix, 0, // dest index 0, 0, EYE_Z, // eye 0, 0, 0, // center 0, 1, 0); // up vector } @Override public void onGlSurfaceCreated(int width, int height) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onGlSurfaceCreated: " + width + " x " + height); } super.onGlSurfaceCreated(width, height); // Update the projection matrix based on the new aspect ratio. final float aspectRatio = (float) width / height; Matrix.frustumM(mProjectionMatrix, 0 /* offset */, -aspectRatio /* left */, aspectRatio /* right */, -1 /* bottom */, 1 /* top */, 2 /* near */, 7 /* far */); // Precompute the products of Projection and View matrices for each camera angle. for (int i = 0; i < mNumCameraAngles; ++i) { Matrix.multiplyMM(mVpMatrices[i], 0, mProjectionMatrix, 0, mViewMatrices[i], 0); } Matrix.multiplyMM(mAmbientVpMatrix, 0, mProjectionMatrix, 0, mAmbientViewMatrix, 0); } /** * Creates a triangle for a hand on the watch face. * * @param program program for drawing triangles * @param width width of base of triangle * @param length length of triangle * @param color color in RGBA order, each in the range [0, 1] */ private Gles2ColoredTriangleList createHand(Gles2ColoredTriangleList.Program program, float width, float length, float[] color) { // Create the data for the VBO. float[] triangleCoords = new float[]{ // in counterclockwise order: 0, length, 0, // top -width / 2, 0, 0, // bottom left width / 2, 0, 0 // bottom right }; return new Gles2ColoredTriangleList(program, triangleCoords, color); } /** * Creates a triangle list for the major ticks on the watch face. * * @param program program for drawing triangles */ private Gles2ColoredTriangleList createMajorTicks( Gles2ColoredTriangleList.Program program) { // Create the data for the VBO. float[] trianglesCoords = new float[9 * 4]; for (int i = 0; i < 4; i++) { float[] triangleCoords = getMajorTickTriangleCoords(i); System.arraycopy(triangleCoords, 0, trianglesCoords, i * 9, triangleCoords.length); } return new Gles2ColoredTriangleList(program, trianglesCoords, new float[]{ 1.0f /* red */, 1.0f /* green */, 1.0f /* blue */, 1.0f /* alpha */ } ); } /** * Creates a triangle list for the minor ticks on the watch face. * * @param program program for drawing triangles */ private Gles2ColoredTriangleList createMinorTicks( Gles2ColoredTriangleList.Program program) { // Create the data for the VBO. float[] trianglesCoords = new float[9 * (12 - 4)]; int index = 0; for (int i = 0; i < 12; i++) { if (i % 3 == 0) { // This is where a major tick goes, so skip it. continue; } float[] triangleCoords = getMinorTickTriangleCoords(i); System.arraycopy(triangleCoords, 0, trianglesCoords, index, triangleCoords.length); index += 9; } return new Gles2ColoredTriangleList(program, trianglesCoords, new float[]{ 0.5f /* red */, 0.5f /* green */, 0.5f /* blue */, 1.0f /* alpha */ } ); } private float[] getMajorTickTriangleCoords(int index) { return getTickTriangleCoords(0.03f /* width */, 0.09f /* length */, index * 360 / 4 /* angleDegrees */); } private float[] getMinorTickTriangleCoords(int index) { return getTickTriangleCoords(0.02f /* width */, 0.06f /* length */, index * 360 / 12 /* angleDegrees */); } private float[] getTickTriangleCoords(float width, float length, int angleDegrees) { // Create the data for the VBO. float[] coords = new float[]{ // in counterclockwise order: 0, 1, 0, // top width / 2, length + 1, 0, // bottom left -width / 2, length + 1, 0 // bottom right }; rotateCoords(coords, angleDegrees); return coords; } /** * Destructively rotates the given coordinates in the XY plane about the origin by the given * angle. * * @param coords flattened 3D coordinates * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the * Z axis */ private void rotateCoords(float[] coords, int angleDegrees) { double angleRadians = Math.toRadians(angleDegrees); double cos = Math.cos(angleRadians); double sin = Math.sin(angleRadians); for (int i = 0; i < coords.length; i += 3) { float x = coords[i]; float y = coords[i + 1]; coords[i] = (float) (cos * x - sin * y); coords[i + 1] = (float) (sin * x + cos * y); } } @Override public void onAmbientModeChanged(boolean inAmbientMode) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode); } super.onAmbientModeChanged(inAmbientMode); invalidate(); } @Override public void onVisibilityChanged(boolean visible) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onVisibilityChanged: " + visible); } super.onVisibilityChanged(visible); if (visible) { registerReceiver(); // Update time zone in case it changed while we were detached. mCalendar.setTimeZone(TimeZone.getDefault()); invalidate(); } else { unregisterReceiver(); } } private void registerReceiver() { if (mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = true; IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED); OpenGLWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter); } private void unregisterReceiver() { if (!mRegisteredTimeZoneReceiver) { return; } mRegisteredTimeZoneReceiver = false; OpenGLWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver); } @Override public void onTimeTick() { super.onTimeTick(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode()); } invalidate(); } @Override public void onDraw() { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "onDraw"); } super.onDraw(); final float[] vpMatrix; // Draw background color and select the appropriate view projection matrix. The // background should always be black in ambient mode. The view projection matrix used is // overhead in ambient. In interactive mode, it's tilted depending on the current time. if (isInAmbientMode()) { GLES20.glClearColor(0, 0, 0, 1); vpMatrix = mAmbientVpMatrix; } else { GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1); final int cameraIndex = (int) ((System.currentTimeMillis() / FRAME_PERIOD_MS) % mNumCameraAngles); vpMatrix = mVpMatrices[cameraIndex]; } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // Compute angle indices for the three hands. mCalendar.setTimeInMillis(System.currentTimeMillis()); float seconds = mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f; float minutes = mCalendar.get(Calendar.MINUTE) + seconds / 60f; float hours = mCalendar.get(Calendar.HOUR) + minutes / 60f; final int secIndex = (int) (seconds / 60f * 360f); final int minIndex = (int) (minutes / 60f * 360f); final int hoursIndex = (int) (hours / 12f * 360f); // Draw triangles from back to front. Don't draw the second hand in ambient mode. // Combine the model matrix with the projection and camera view. Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[hoursIndex], 0); // Draw the triangle. mHourHandTriangle.draw(mMvpMatrix); // Combine the model matrix with the projection and camera view. Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[minIndex], 0); // Draw the triangle. mMinuteHandTriangle.draw(mMvpMatrix); if (!isInAmbientMode()) { // Combine the model matrix with the projection and camera view. Matrix.multiplyMM(mMvpMatrix, 0, vpMatrix, 0, mModelMatrices[secIndex], 0); // Draw the triangle. mSecondHandTriangle.draw(mMvpMatrix); } // Draw the major and minor ticks. mMajorTickTriangles.draw(vpMatrix); mMinorTickTriangles.draw(vpMatrix); // Draw every frame as long as we're visible and in interactive mode. if (isVisible() && !isInAmbientMode()) { invalidate(); } } } }