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