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 }