1package com.android.deskclock;
2
3import android.content.Context;
4import android.util.AttributeSet;
5import android.widget.FrameLayout;
6import android.widget.ImageButton;
7import android.widget.ImageView;
8import 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 */
16public 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")
30    public CircleButtonsLayout(Context context) {
31        this(context, null);
32        mContext = context;
33    }
34
35    public CircleButtonsLayout(Context context, AttributeSet attrs) {
36        super(context, attrs);
37        mContext = context;
38    }
39
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
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
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