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