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") 28 public CircleButtonsLayout(Context context) { 29 this(context, null); 30 } 31 32 public CircleButtonsLayout(Context context, AttributeSet attrs) { 33 super(context, attrs); 34 } 35 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 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 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