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 }