blob: a1dae040eddff1467787f8b9ec1544ed02a60bc6 [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
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.Button
import android.widget.FrameLayout
import android.widget.TextView
import kotlin.math.min
import kotlin.math.sqrt
/**
* This class adjusts the locations of child buttons and text of this view group by adjusting the
* margins of each item. The left and right buttons are aligned with the bottom of the circle. The
* stop button and label text are located within the circle with the stop button near the bottom and
* the label text near the top. The maximum text size for the label text view is also calculated.
*/
class CircleButtonsLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private val mDiamOffset: Float
private var mCircleView: View? = null
private var mResetAddButton: Button? = null
private var mLabel: TextView? = null
init {
val res = getContext().resources
val strokeSize = res.getDimension(R.dimen.circletimer_circle_size)
val dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size)
val markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size)
mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2
}
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// We must call onMeasure both before and after re-measuring our views because the circle
// may not always be drawn here yet. The first onMeasure will force the circle to be drawn,
// and the second will force our re-measurements to take effect.
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
remeasureViews()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
private fun remeasureViews() {
if (mLabel == null) {
mCircleView = findViewById(R.id.timer_time)
mLabel = findViewById<View>(R.id.timer_label) as TextView
mResetAddButton = findViewById<View>(R.id.reset_add) as Button
}
val frameWidth = mCircleView!!.measuredWidth
val frameHeight = mCircleView!!.measuredHeight
val minBound = min(frameWidth, frameHeight)
val circleDiam = (minBound - mDiamOffset).toInt()
mResetAddButton?.let {
val resetAddParams = it.layoutParams as MarginLayoutParams
resetAddParams.bottomMargin = circleDiam / 6
if (minBound == frameWidth) {
resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2
}
}
mLabel?.let {
val labelParams = it.layoutParams as MarginLayoutParams
labelParams.topMargin = circleDiam / 6
if (minBound == frameWidth) {
labelParams.topMargin += (frameHeight - frameWidth) / 2
}
/* The following formula has been simplified based on the following:
* Our goal is to calculate the maximum width for the label frame.
* We may do this with the following diagram to represent the top half of the circle:
* ___
* . | .
* ._________| .
* . ^ | .
* / x | \
* |_______________|_______________|
*
* where x represents the value we would like to calculate, and the final width of the
* label will be w = 2 * x.
*
* We may find x by drawing a right triangle from the center of the circle:
* ___
* . | .
* ._________| .
* . . | .
* / . | }y \
* |_____________.t|_______________|
*
* where t represents the angle of that triangle, and y is the height of that triangle.
*
* If r = radius of the circle, we know the following trigonometric identities:
* cos(t) = y / r
* and sin(t) = x / r
* => r * sin(t) = x
* and sin^2(t) = 1 - cos^2(t)
* => sin(t) = +/- sqrt(1 - cos^2(t))
* (note: because we need the positive value, we may drop the +/-).
*
* To calculate the final width, we may combine our formulas:
* w = 2 * x
* => w = 2 * r * sin(t)
* => w = 2 * r * sqrt(1 - cos^2(t))
* => w = 2 * r * sqrt(1 - (y / r)^2)
*
* Simplifying even further, to mitigate the complexity of the final formula:
* sqrt(1 - (y / r)^2)
* => sqrt(1 - (y^2 / r^2))
* => sqrt((r^2 / r^2) - (y^2 / r^2))
* => sqrt((r^2 - y^2) / (r^2))
* => sqrt(r^2 - y^2) / sqrt(r^2)
* => sqrt(r^2 - y^2) / r
* => sqrt((r + y)*(r - y)) / r
*
* Placing this back in our formula, we end up with, as our final, reduced equation:
* w = 2 * r * sqrt(1 - (y / r)^2)
* => w = 2 * r * sqrt((r + y)*(r - y)) / r
* => w = 2 * sqrt((r + y)*(r - y))
*/
// Radius of the circle.
val r = circleDiam / 2
// Y value of the top of the label, calculated from the center of the circle.
val y = frameHeight / 2 - labelParams.topMargin
// New maximum width of the label.
val w = 2 * sqrt((r + y) * (r - y).toDouble())
it.maxWidth = w.toInt()
}
}
}