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.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 */ 15 public 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