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") 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