/* * 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(R.id.timer_label) as TextView mResetAddButton = findViewById(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() } } }