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