1 /* 2 * Copyright (C) 2015 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.android.deskclock.stopwatch; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.graphics.RectF; 25 import android.util.AttributeSet; 26 import android.view.View; 27 28 import com.android.deskclock.R; 29 import com.android.deskclock.ThemeUtils; 30 import com.android.deskclock.Utils; 31 import com.android.deskclock.data.DataModel; 32 import com.android.deskclock.data.Lap; 33 import com.android.deskclock.data.Stopwatch; 34 35 import java.util.List; 36 37 /** 38 * Custom view that draws a reference lap as a circle when one exists. 39 */ 40 public final class StopwatchCircleView extends View { 41 42 /** The size of the dot indicating the user's position within the reference lap. */ 43 private final float mDotRadius; 44 45 /** An amount to subtract from the true radius to account for drawing thicknesses. */ 46 private final float mRadiusOffset; 47 48 /** Used to scale the width of the marker to make it similarly visible on all screens. */ 49 private final float mScreenDensity; 50 51 /** The color indicating the remaining portion of the current lap. */ 52 private final int mRemainderColor; 53 54 /** The color indicating the completed portion of the lap. */ 55 private final int mCompletedColor; 56 57 /** The size of the stroke that paints the lap circle. */ 58 private final float mStrokeSize; 59 60 /** The size of the stroke that paints the marker for the end of the prior lap. */ 61 private final float mMarkerStrokeSize; 62 63 private final Paint mPaint = new Paint(); 64 private final Paint mFill = new Paint(); 65 private final RectF mArcRect = new RectF(); 66 67 @SuppressWarnings("unused") StopwatchCircleView(Context context)68 public StopwatchCircleView(Context context) { 69 this(context, null); 70 } 71 StopwatchCircleView(Context context, AttributeSet attrs)72 public StopwatchCircleView(Context context, AttributeSet attrs) { 73 super(context, attrs); 74 75 final Resources resources = context.getResources(); 76 final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size); 77 78 mDotRadius = dotDiameter / 2f; 79 mScreenDensity = resources.getDisplayMetrics().density; 80 mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size); 81 mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size); 82 mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize); 83 84 mRemainderColor = Color.WHITE; 85 mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent); 86 87 mPaint.setAntiAlias(true); 88 mPaint.setStyle(Paint.Style.STROKE); 89 90 mFill.setAntiAlias(true); 91 mFill.setColor(mCompletedColor); 92 mFill.setStyle(Paint.Style.FILL); 93 } 94 95 /** 96 * Start the animation if it is not currently running. 97 */ update()98 void update() { 99 postInvalidateOnAnimation(); 100 } 101 102 @Override onDraw(Canvas canvas)103 public void onDraw(Canvas canvas) { 104 // Compute the size and location of the circle to be drawn. 105 final int xCenter = getWidth() / 2; 106 final int yCenter = getHeight() / 2; 107 final float radius = Math.min(xCenter, yCenter) - mRadiusOffset; 108 109 // Reset old painting state. 110 mPaint.setColor(mRemainderColor); 111 mPaint.setStrokeWidth(mStrokeSize); 112 113 final List<Lap> laps = getLaps(); 114 115 // If a reference lap does not exist or should not be drawn, draw a simple white circle. 116 if (laps.isEmpty() || !DataModel.getDataModel().canAddMoreLaps()) { 117 // Draw a complete white circle; no red arc required. 118 canvas.drawCircle(xCenter, yCenter, radius, mPaint); 119 120 // No need to continue animating the plain white circle. 121 return; 122 } 123 124 // The first lap is the reference lap to which all future laps are compared. 125 final Stopwatch stopwatch = getStopwatch(); 126 final int lapCount = laps.size(); 127 final Lap firstLap = laps.get(lapCount - 1); 128 final Lap priorLap = laps.get(0); 129 final long firstLapTime = firstLap.getLapTime(); 130 final long currentLapTime = stopwatch.getTotalTime() - priorLap.getAccumulatedTime(); 131 132 // Draw a combination of red and white arcs to create a circle. 133 mArcRect.top = yCenter - radius; 134 mArcRect.bottom = yCenter + radius; 135 mArcRect.left = xCenter - radius; 136 mArcRect.right = xCenter + radius; 137 final float redPercent = (float) currentLapTime / (float) firstLapTime; 138 final float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent); 139 140 // Draw a white arc to indicate the amount of reference lap that remains. 141 canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint); 142 143 // Draw a red arc to indicate the amount of reference lap completed. 144 mPaint.setColor(mCompletedColor); 145 canvas.drawArc(mArcRect, 270, redPercent * 360 , false, mPaint); 146 147 // Starting on lap 2, a marker can be drawn indicating where the prior lap ended. 148 if (lapCount > 1) { 149 mPaint.setColor(mRemainderColor); 150 mPaint.setStrokeWidth(mMarkerStrokeSize); 151 final float markerAngle = (float) priorLap.getLapTime() / (float) firstLapTime * 360; 152 final float startAngle = 270 + markerAngle; 153 final float sweepAngle = mScreenDensity * (float) (360 / (radius * Math.PI)); 154 canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint); 155 } 156 157 // Draw a red dot to indicate current position relative to reference lap. 158 final float dotAngleDegrees = 270 + redPercent * 360; 159 final double dotAngleRadians = Math.toRadians(dotAngleDegrees); 160 final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians)); 161 final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians)); 162 canvas.drawCircle(dotX, dotY, mDotRadius, mFill); 163 164 // If the stopwatch is not running it does not require continuous updates. 165 if (stopwatch.isRunning()) { 166 postInvalidateOnAnimation(); 167 } 168 } 169 getStopwatch()170 private Stopwatch getStopwatch() { 171 return DataModel.getDataModel().getStopwatch(); 172 } 173 getLaps()174 private List<Lap> getLaps() { 175 return DataModel.getDataModel().getLaps(); 176 } 177 } 178