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 18 19 import android.content.Context 20 import android.util.AttributeSet 21 import android.view.View 22 import android.widget.Button 23 import android.widget.FrameLayout 24 import android.widget.TextView 25 26 import kotlin.math.min 27 import kotlin.math.sqrt 28 29 /** 30 * This class adjusts the locations of child buttons and text of this view group by adjusting the 31 * margins of each item. The left and right buttons are aligned with the bottom of the circle. The 32 * stop button and label text are located within the circle with the stop button near the bottom and 33 * the label text near the top. The maximum text size for the label text view is also calculated. 34 */ 35 class CircleButtonsLayout @JvmOverloads constructor( 36 context: Context, 37 attrs: AttributeSet? = null 38 ) : FrameLayout(context, attrs) { 39 private val mDiamOffset: Float 40 private var mCircleView: View? = null 41 private var mResetAddButton: Button? = null 42 private var mLabel: TextView? = null 43 44 init { 45 val res = getContext().resources 46 val strokeSize = res.getDimension(R.dimen.circletimer_circle_size) 47 val dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size) 48 val markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size) 49 mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2 50 } 51 onMeasurenull52 public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 53 // We must call onMeasure both before and after re-measuring our views because the circle 54 // may not always be drawn here yet. The first onMeasure will force the circle to be drawn, 55 // and the second will force our re-measurements to take effect. 56 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 57 remeasureViews() 58 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 59 } 60 remeasureViewsnull61 private fun remeasureViews() { 62 if (mLabel == null) { 63 mCircleView = findViewById(R.id.timer_time) 64 mLabel = findViewById<View>(R.id.timer_label) as TextView 65 mResetAddButton = findViewById<View>(R.id.reset_add) as Button 66 } 67 68 val frameWidth = mCircleView!!.measuredWidth 69 val frameHeight = mCircleView!!.measuredHeight 70 val minBound = min(frameWidth, frameHeight) 71 val circleDiam = (minBound - mDiamOffset).toInt() 72 73 mResetAddButton?.let { 74 val resetAddParams = it.layoutParams as MarginLayoutParams 75 resetAddParams.bottomMargin = circleDiam / 6 76 if (minBound == frameWidth) { 77 resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2 78 } 79 } 80 81 mLabel?.let { 82 val labelParams = it.layoutParams as MarginLayoutParams 83 labelParams.topMargin = circleDiam / 6 84 if (minBound == frameWidth) { 85 labelParams.topMargin += (frameHeight - frameWidth) / 2 86 } 87 /* The following formula has been simplified based on the following: 88 * Our goal is to calculate the maximum width for the label frame. 89 * We may do this with the following diagram to represent the top half of the circle: 90 * ___ 91 * . | . 92 * ._________| . 93 * . ^ | . 94 * / x | \ 95 * |_______________|_______________| 96 * 97 * where x represents the value we would like to calculate, and the final width of the 98 * label will be w = 2 * x. 99 * 100 * We may find x by drawing a right triangle from the center of the circle: 101 * ___ 102 * . | . 103 * ._________| . 104 * . . | . 105 * / . | }y \ 106 * |_____________.t|_______________| 107 * 108 * where t represents the angle of that triangle, and y is the height of that triangle. 109 * 110 * If r = radius of the circle, we know the following trigonometric identities: 111 * cos(t) = y / r 112 * and sin(t) = x / r 113 * => r * sin(t) = x 114 * and sin^2(t) = 1 - cos^2(t) 115 * => sin(t) = +/- sqrt(1 - cos^2(t)) 116 * (note: because we need the positive value, we may drop the +/-). 117 * 118 * To calculate the final width, we may combine our formulas: 119 * w = 2 * x 120 * => w = 2 * r * sin(t) 121 * => w = 2 * r * sqrt(1 - cos^2(t)) 122 * => w = 2 * r * sqrt(1 - (y / r)^2) 123 * 124 * Simplifying even further, to mitigate the complexity of the final formula: 125 * sqrt(1 - (y / r)^2) 126 * => sqrt(1 - (y^2 / r^2)) 127 * => sqrt((r^2 / r^2) - (y^2 / r^2)) 128 * => sqrt((r^2 - y^2) / (r^2)) 129 * => sqrt(r^2 - y^2) / sqrt(r^2) 130 * => sqrt(r^2 - y^2) / r 131 * => sqrt((r + y)*(r - y)) / r 132 * 133 * Placing this back in our formula, we end up with, as our final, reduced equation: 134 * w = 2 * r * sqrt(1 - (y / r)^2) 135 * => w = 2 * r * sqrt((r + y)*(r - y)) / r 136 * => w = 2 * sqrt((r + y)*(r - y)) 137 */ 138 // Radius of the circle. 139 val r = circleDiam / 2 140 // Y value of the top of the label, calculated from the center of the circle. 141 val y = frameHeight / 2 - labelParams.topMargin 142 // New maximum width of the label. 143 val w = 2 * sqrt((r + y) * (r - y).toDouble()) 144 145 it.maxWidth = w.toInt() 146 } 147 } 148 }