blob: a7d869903a97b570c2bc5018a2a5a56c92e3c082 [file] [log] [blame]
/*
* Copyright (C) 2020 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.android.deskclock.stopwatch
import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import com.android.deskclock.R
import com.android.deskclock.ThemeUtils
import com.android.deskclock.Utils
import com.android.deskclock.data.DataModel
import com.android.deskclock.data.Lap
import com.android.deskclock.data.Stopwatch
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
/**
* Custom view that draws a reference lap as a circle when one exists.
*/
class StopwatchCircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
/** The size of the dot indicating the user's position within the reference lap. */
private val mDotRadius: Float
/** An amount to subtract from the true radius to account for drawing thicknesses. */
private val mRadiusOffset: Float
/** Used to scale the width of the marker to make it similarly visible on all screens. */
private val mScreenDensity: Float
/** The color indicating the remaining portion of the current lap. */
private val mRemainderColor: Int
/** The color indicating the completed portion of the lap. */
private val mCompletedColor: Int
/** The size of the stroke that paints the lap circle. */
private val mStrokeSize: Float
/** The size of the stroke that paints the marker for the end of the prior lap. */
private val mMarkerStrokeSize: Float
private val mPaint: Paint = Paint()
private val mFill: Paint = Paint()
private val mArcRect: RectF = RectF()
constructor(context: Context) : this(context, null) {
}
init {
val resources: Resources = context.getResources()
val dotDiameter: Float = resources.getDimension(R.dimen.circletimer_dot_size)
mDotRadius = dotDiameter / 2f
mScreenDensity = resources.getDisplayMetrics().density
mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size)
mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size)
mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize)
mRemainderColor = Color.WHITE
mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent)
mPaint.setAntiAlias(true)
mPaint.setStyle(Paint.Style.STROKE)
mFill.setAntiAlias(true)
mFill.setColor(mCompletedColor)
mFill.setStyle(Paint.Style.FILL)
}
/**
* Start the animation if it is not currently running.
*/
fun update() {
postInvalidateOnAnimation()
}
override fun onDraw(canvas: Canvas) {
// Compute the size and location of the circle to be drawn.
val xCenter: Int = getWidth() / 2
val yCenter: Int = getHeight() / 2
val radius = min(xCenter, yCenter) - mRadiusOffset
// Reset old painting state.
mPaint.setColor(mRemainderColor)
mPaint.setStrokeWidth(mStrokeSize)
val laps = laps
// If a reference lap does not exist or should not be drawn, draw a simple white circle.
if (laps.isEmpty() || !DataModel.dataModel.canAddMoreLaps()) {
// Draw a complete white circle; no red arc required.
canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
// No need to continue animating the plain white circle.
return
}
// The first lap is the reference lap to which all future laps are compared.
val stopwatch = stopwatch
val lapCount = laps.size
val firstLap = laps[lapCount - 1]
val priorLap = laps[0]
val firstLapTime = firstLap.lapTime
val currentLapTime = stopwatch.totalTime - priorLap.accumulatedTime
// Draw a combination of red and white arcs to create a circle.
mArcRect.top = yCenter - radius
mArcRect.bottom = yCenter + radius
mArcRect.left = xCenter - radius
mArcRect.right = xCenter + radius
val redPercent = currentLapTime.toFloat() / firstLapTime.toFloat()
val whitePercent: Float = 1f - if (redPercent > 1) 1f else redPercent
// Draw a white arc to indicate the amount of reference lap that remains.
canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint)
// Draw a red arc to indicate the amount of reference lap completed.
mPaint.setColor(mCompletedColor)
canvas.drawArc(mArcRect, 270f, redPercent * 360, false, mPaint)
// Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
if (lapCount > 1) {
mPaint.setColor(mRemainderColor)
mPaint.setStrokeWidth(mMarkerStrokeSize)
val markerAngle = priorLap.lapTime.toFloat() / firstLapTime.toFloat() * 360
val startAngle = 270 + markerAngle
val sweepAngle = mScreenDensity * (360 / (radius * Math.PI)).toFloat()
canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint)
}
// Draw a red dot to indicate current position relative to reference lap.
val dotAngleDegrees = 270 + redPercent * 360
val dotAngleRadians = Math.toRadians(dotAngleDegrees.toDouble())
val dotX = xCenter + (radius * cos(dotAngleRadians)).toFloat()
val dotY = yCenter + (radius * sin(dotAngleRadians)).toFloat()
canvas.drawCircle(dotX, dotY, mDotRadius, mFill)
// If the stopwatch is not running it does not require continuous updates.
if (stopwatch.isRunning) {
postInvalidateOnAnimation()
}
}
private val stopwatch: Stopwatch
get() = DataModel.dataModel.stopwatch
private val laps: List<Lap>
get() = DataModel.dataModel.laps
}