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