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