Home | History | Annotate | Download | only in watchface
      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