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