Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2013 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package android.widget; |
| 18 | |
| 19 | import android.animation.Animator; |
| 20 | import android.animation.AnimatorSet; |
| 21 | import android.animation.Keyframe; |
| 22 | import android.animation.ObjectAnimator; |
| 23 | import android.animation.PropertyValuesHolder; |
| 24 | import android.animation.ValueAnimator; |
| 25 | import android.annotation.SuppressLint; |
| 26 | import android.content.Context; |
Alan Viverette | 60727e0 | 2014-07-28 16:56:32 -0700 | [diff] [blame] | 27 | import android.content.res.ColorStateList; |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 28 | import android.content.res.Resources; |
| 29 | import android.content.res.TypedArray; |
| 30 | import android.graphics.Canvas; |
| 31 | import android.graphics.Color; |
| 32 | import android.graphics.Paint; |
| 33 | import android.graphics.Typeface; |
| 34 | import android.graphics.RectF; |
| 35 | import android.os.Bundle; |
| 36 | import android.text.format.DateUtils; |
| 37 | import android.text.format.Time; |
| 38 | import android.util.AttributeSet; |
| 39 | import android.util.Log; |
Alan Viverette | 5134478 | 2014-07-16 17:39:27 -0700 | [diff] [blame] | 40 | import android.util.TypedValue; |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 41 | import android.view.HapticFeedbackConstants; |
| 42 | import android.view.MotionEvent; |
| 43 | import android.view.View; |
| 44 | import android.view.ViewGroup; |
| 45 | import android.view.accessibility.AccessibilityEvent; |
| 46 | import android.view.accessibility.AccessibilityNodeInfo; |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 47 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 48 | import com.android.internal.R; |
| 49 | |
| 50 | import java.text.DateFormatSymbols; |
| 51 | import java.util.ArrayList; |
| 52 | import java.util.Calendar; |
| 53 | import java.util.Locale; |
| 54 | |
| 55 | /** |
| 56 | * View to show a clock circle picker (with one or two picking circles) |
| 57 | * |
| 58 | * @hide |
| 59 | */ |
| 60 | public class RadialTimePickerView extends View implements View.OnTouchListener { |
| 61 | private static final String TAG = "ClockView"; |
| 62 | |
| 63 | private static final boolean DEBUG = false; |
| 64 | |
| 65 | private static final int DEBUG_COLOR = 0x20FF0000; |
| 66 | private static final int DEBUG_TEXT_COLOR = 0x60FF0000; |
| 67 | private static final int DEBUG_STROKE_WIDTH = 2; |
| 68 | |
| 69 | private static final int HOURS = 0; |
| 70 | private static final int MINUTES = 1; |
| 71 | private static final int HOURS_INNER = 2; |
| 72 | private static final int AMPM = 3; |
| 73 | |
| 74 | private static final int SELECTOR_CIRCLE = 0; |
| 75 | private static final int SELECTOR_DOT = 1; |
| 76 | private static final int SELECTOR_LINE = 2; |
| 77 | |
| 78 | private static final int AM = 0; |
| 79 | private static final int PM = 1; |
| 80 | |
| 81 | // Opaque alpha level |
| 82 | private static final int ALPHA_OPAQUE = 255; |
| 83 | |
| 84 | // Transparent alpha level |
| 85 | private static final int ALPHA_TRANSPARENT = 0; |
| 86 | |
| 87 | // Alpha level of color for selector. |
Alan Viverette | 518ff0d | 2014-08-15 14:20:35 -0700 | [diff] [blame] | 88 | private static final int ALPHA_SELECTOR = 60; // was 51 |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 89 | |
| 90 | // Alpha level of color for selected circle. |
| 91 | private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR; |
| 92 | |
| 93 | // Alpha level of color for pressed circle. |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 94 | private static final int ALPHA_AMPM_PRESSED = 255; // was 175 |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 95 | |
| 96 | private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; |
| 97 | private static final float SINE_30_DEGREES = 0.5f; |
| 98 | |
| 99 | private static final int DEGREES_FOR_ONE_HOUR = 30; |
| 100 | private static final int DEGREES_FOR_ONE_MINUTE = 6; |
| 101 | |
| 102 | private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; |
| 103 | private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; |
| 104 | private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; |
| 105 | |
| 106 | private static final int CENTER_RADIUS = 2; |
| 107 | |
Alan Viverette | 518ff0d | 2014-08-15 14:20:35 -0700 | [diff] [blame] | 108 | private static final int[] STATE_SET_SELECTED = new int[] {R.attr.state_selected}; |
Alan Viverette | 60727e0 | 2014-07-28 16:56:32 -0700 | [diff] [blame] | 109 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 110 | private static int[] sSnapPrefer30sMap = new int[361]; |
| 111 | |
| 112 | private final String[] mHours12Texts = new String[12]; |
| 113 | private final String[] mOuterHours24Texts = new String[12]; |
| 114 | private final String[] mInnerHours24Texts = new String[12]; |
| 115 | private final String[] mMinutesTexts = new String[12]; |
| 116 | |
| 117 | private final String[] mAmPmText = new String[2]; |
| 118 | |
| 119 | private final Paint[] mPaint = new Paint[2]; |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 120 | private final int[] mColor = new int[2]; |
| 121 | private final IntHolder[] mAlpha = new IntHolder[2]; |
| 122 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 123 | private final Paint mPaintCenter = new Paint(); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 124 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 125 | private final Paint[][] mPaintSelector = new Paint[2][3]; |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 126 | private final int[][] mColorSelector = new int[2][3]; |
| 127 | private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; |
| 128 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 129 | private final Paint mPaintAmPmText = new Paint(); |
| 130 | private final Paint[] mPaintAmPmCircle = new Paint[2]; |
| 131 | |
| 132 | private final Paint mPaintBackground = new Paint(); |
| 133 | private final Paint mPaintDisabled = new Paint(); |
| 134 | private final Paint mPaintDebug = new Paint(); |
| 135 | |
| 136 | private Typeface mTypeface; |
| 137 | |
| 138 | private boolean mIs24HourMode; |
| 139 | private boolean mShowHours; |
| 140 | private boolean mIsOnInnerCircle; |
| 141 | |
| 142 | private int mXCenter; |
| 143 | private int mYCenter; |
| 144 | |
| 145 | private float[] mCircleRadius = new float[3]; |
| 146 | |
| 147 | private int mMinHypotenuseForInnerNumber; |
| 148 | private int mMaxHypotenuseForOuterNumber; |
| 149 | private int mHalfwayHypotenusePoint; |
| 150 | |
| 151 | private float[] mTextSize = new float[2]; |
| 152 | private float mInnerTextSize; |
| 153 | |
| 154 | private float[][] mTextGridHeights = new float[2][7]; |
| 155 | private float[][] mTextGridWidths = new float[2][7]; |
| 156 | |
| 157 | private float[] mInnerTextGridHeights = new float[7]; |
| 158 | private float[] mInnerTextGridWidths = new float[7]; |
| 159 | |
| 160 | private String[] mOuterTextHours; |
| 161 | private String[] mInnerTextHours; |
| 162 | private String[] mOuterTextMinutes; |
| 163 | |
| 164 | private float[] mCircleRadiusMultiplier = new float[2]; |
| 165 | private float[] mNumbersRadiusMultiplier = new float[3]; |
| 166 | |
| 167 | private float[] mTextSizeMultiplier = new float[3]; |
| 168 | |
| 169 | private float[] mAnimationRadiusMultiplier = new float[3]; |
| 170 | |
| 171 | private float mTransitionMidRadiusMultiplier; |
| 172 | private float mTransitionEndRadiusMultiplier; |
| 173 | |
| 174 | private AnimatorSet mTransition; |
| 175 | private InvalidateUpdateListener mInvalidateUpdateListener = new InvalidateUpdateListener(); |
| 176 | |
| 177 | private int[] mLineLength = new int[3]; |
| 178 | private int[] mSelectionRadius = new int[3]; |
| 179 | private float mSelectionRadiusMultiplier; |
| 180 | private int[] mSelectionDegrees = new int[3]; |
| 181 | |
| 182 | private int mAmPmCircleRadius; |
| 183 | private float mAmPmYCenter; |
| 184 | |
| 185 | private float mAmPmCircleRadiusMultiplier; |
| 186 | private int mAmPmTextColor; |
| 187 | |
| 188 | private float mLeftIndicatorXCenter; |
| 189 | private float mRightIndicatorXCenter; |
| 190 | |
| 191 | private int mAmPmUnselectedColor; |
| 192 | private int mAmPmSelectedColor; |
| 193 | |
| 194 | private int mAmOrPm; |
| 195 | private int mAmOrPmPressed; |
| 196 | |
Alan Viverette | 5134478 | 2014-07-16 17:39:27 -0700 | [diff] [blame] | 197 | private int mDisabledAlpha; |
| 198 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 199 | private RectF mRectF = new RectF(); |
| 200 | private boolean mInputEnabled = true; |
| 201 | private OnValueSelectedListener mListener; |
| 202 | |
| 203 | private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); |
| 204 | private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); |
| 205 | |
| 206 | public interface OnValueSelectedListener { |
| 207 | void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); |
| 208 | } |
| 209 | |
| 210 | static { |
| 211 | // Prepare mapping to snap touchable degrees to selectable degrees. |
| 212 | preparePrefer30sMap(); |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger |
| 217 | * selectable area to each of the 12 visible values, such that the ratio of space apportioned |
| 218 | * to a visible value : space apportioned to a non-visible value will be 14 : 4. |
| 219 | * E.g. the output of 30 degrees should have a higher range of input associated with it than |
| 220 | * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock |
| 221 | * circle (5 on the minutes, 1 or 13 on the hours). |
| 222 | */ |
| 223 | private static void preparePrefer30sMap() { |
| 224 | // We'll split up the visible output and the non-visible output such that each visible |
| 225 | // output will correspond to a range of 14 associated input degrees, and each non-visible |
| 226 | // output will correspond to a range of 4 associate input degrees, so visible numbers |
| 227 | // are more than 3 times easier to get than non-visible numbers: |
| 228 | // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. |
| 229 | // |
| 230 | // If an output of 30 degrees should correspond to a range of 14 associated degrees, then |
| 231 | // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should |
| 232 | // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you |
| 233 | // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this |
| 234 | // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the |
| 235 | // ability to aggressively prefer the visible values by a factor of more than 3:1, which |
| 236 | // greatly contributes to the selectability of these values. |
| 237 | |
| 238 | // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. |
| 239 | int snappedOutputDegrees = 0; |
| 240 | // Count of how many inputs we've designated to the specified output. |
| 241 | int count = 1; |
| 242 | // How many input we expect for a specified output. This will be 14 for output divisible |
| 243 | // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so |
| 244 | // the caller can decide which they need. |
| 245 | int expectedCount = 8; |
| 246 | // Iterate through the input. |
| 247 | for (int degrees = 0; degrees < 361; degrees++) { |
| 248 | // Save the input-output mapping. |
| 249 | sSnapPrefer30sMap[degrees] = snappedOutputDegrees; |
| 250 | // If this is the last input for the specified output, calculate the next output and |
| 251 | // the next expected count. |
| 252 | if (count == expectedCount) { |
| 253 | snappedOutputDegrees += 6; |
| 254 | if (snappedOutputDegrees == 360) { |
| 255 | expectedCount = 7; |
| 256 | } else if (snappedOutputDegrees % 30 == 0) { |
| 257 | expectedCount = 14; |
| 258 | } else { |
| 259 | expectedCount = 4; |
| 260 | } |
| 261 | count = 1; |
| 262 | } else { |
| 263 | count++; |
| 264 | } |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | /** |
| 269 | * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, |
| 270 | * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be |
| 271 | * weighted heavier than the degrees corresponding to non-visible numbers. |
| 272 | * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the |
| 273 | * mapping. |
| 274 | */ |
| 275 | private static int snapPrefer30s(int degrees) { |
| 276 | if (sSnapPrefer30sMap == null) { |
| 277 | return -1; |
| 278 | } |
| 279 | return sSnapPrefer30sMap[degrees]; |
| 280 | } |
| 281 | |
| 282 | /** |
| 283 | * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all |
| 284 | * multiples of 30), where the input will be "snapped" to the closest visible degrees. |
| 285 | * @param degrees The input degrees |
| 286 | * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may |
| 287 | * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force |
| 288 | * strictly lower, and 0 to snap to the closer one. |
| 289 | * @return output degrees, will be a multiple of 30 |
| 290 | */ |
| 291 | private static int snapOnly30s(int degrees, int forceHigherOrLower) { |
| 292 | final int stepSize = DEGREES_FOR_ONE_HOUR; |
| 293 | int floor = (degrees / stepSize) * stepSize; |
| 294 | final int ceiling = floor + stepSize; |
| 295 | if (forceHigherOrLower == 1) { |
| 296 | degrees = ceiling; |
| 297 | } else if (forceHigherOrLower == -1) { |
| 298 | if (degrees == floor) { |
| 299 | floor -= stepSize; |
| 300 | } |
| 301 | degrees = floor; |
| 302 | } else { |
| 303 | if ((degrees - floor) < (ceiling - degrees)) { |
| 304 | degrees = floor; |
| 305 | } else { |
| 306 | degrees = ceiling; |
| 307 | } |
| 308 | } |
| 309 | return degrees; |
| 310 | } |
| 311 | |
| 312 | public RadialTimePickerView(Context context, AttributeSet attrs) { |
| 313 | this(context, attrs, R.attr.timePickerStyle); |
| 314 | } |
| 315 | |
| 316 | public RadialTimePickerView(Context context, AttributeSet attrs, int defStyle) { |
| 317 | super(context, attrs); |
| 318 | |
Alan Viverette | 5134478 | 2014-07-16 17:39:27 -0700 | [diff] [blame] | 319 | // Pull disabled alpha from theme. |
| 320 | final TypedValue outValue = new TypedValue(); |
| 321 | context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); |
| 322 | mDisabledAlpha = (int) (outValue.getFloat() * 255 + 0.5f); |
| 323 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 324 | // process style attributes |
Alan Viverette | 5134478 | 2014-07-16 17:39:27 -0700 | [diff] [blame] | 325 | final Resources res = getResources(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 326 | final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, |
| 327 | defStyle, 0); |
| 328 | |
Alan Viverette | 60727e0 | 2014-07-28 16:56:32 -0700 | [diff] [blame] | 329 | ColorStateList amPmBackgroundColor = a.getColorStateList( |
| 330 | R.styleable.TimePicker_amPmBackgroundColor); |
| 331 | if (amPmBackgroundColor == null) { |
| 332 | amPmBackgroundColor = res.getColorStateList( |
| 333 | R.color.timepicker_default_ampm_unselected_background_color_material); |
| 334 | } |
| 335 | |
| 336 | // Obtain the backup selected color. If the background color state |
| 337 | // list doesn't have a state for selected, we'll use this color. |
| 338 | final int amPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 339 | res.getColor(R.color.timepicker_default_ampm_selected_background_color_material)); |
Alan Viverette | 518ff0d | 2014-08-15 14:20:35 -0700 | [diff] [blame] | 340 | amPmBackgroundColor = ColorStateList.addFirstIfMissing( |
| 341 | amPmBackgroundColor, R.attr.state_selected, amPmSelectedColor); |
| 342 | |
Alan Viverette | 60727e0 | 2014-07-28 16:56:32 -0700 | [diff] [blame] | 343 | mAmPmSelectedColor = amPmBackgroundColor.getColorForState( |
| 344 | STATE_SET_SELECTED, amPmSelectedColor); |
| 345 | mAmPmUnselectedColor = amPmBackgroundColor.getDefaultColor(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 346 | |
| 347 | mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 348 | res.getColor(R.color.timepicker_default_text_color_material)); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 349 | |
| 350 | mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); |
| 351 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 352 | // Initialize all alpha values to opaque. |
| 353 | for (int i = 0; i < mAlpha.length; i++) { |
| 354 | mAlpha[i] = new IntHolder(ALPHA_OPAQUE); |
| 355 | } |
| 356 | for (int i = 0; i < mAlphaSelector.length; i++) { |
| 357 | for (int j = 0; j < mAlphaSelector[i].length; j++) { |
| 358 | mAlphaSelector[i][j] = new IntHolder(ALPHA_OPAQUE); |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 363 | res.getColor(R.color.timepicker_default_text_color_material)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 364 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 365 | mPaint[HOURS] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 366 | mPaint[HOURS].setAntiAlias(true); |
| 367 | mPaint[HOURS].setTextAlign(Paint.Align.CENTER); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 368 | mColor[HOURS] = numbersTextColor; |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 369 | |
| 370 | mPaint[MINUTES] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 371 | mPaint[MINUTES].setAntiAlias(true); |
| 372 | mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 373 | mColor[MINUTES] = numbersTextColor; |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 374 | |
| 375 | mPaintCenter.setColor(numbersTextColor); |
| 376 | mPaintCenter.setAntiAlias(true); |
| 377 | mPaintCenter.setTextAlign(Paint.Align.CENTER); |
| 378 | |
| 379 | mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 380 | mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 381 | mColorSelector[HOURS][SELECTOR_CIRCLE] = a.getColor( |
| 382 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 383 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 384 | |
| 385 | mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 386 | mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 387 | mColorSelector[HOURS][SELECTOR_DOT] = a.getColor( |
| 388 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 389 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 390 | |
| 391 | mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 392 | mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); |
| 393 | mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 394 | mColorSelector[HOURS][SELECTOR_LINE] = a.getColor( |
| 395 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 396 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 397 | |
| 398 | mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 399 | mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 400 | mColorSelector[MINUTES][SELECTOR_CIRCLE] = a.getColor( |
| 401 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 402 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 403 | |
| 404 | mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 405 | mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 406 | mColorSelector[MINUTES][SELECTOR_DOT] = a.getColor( |
| 407 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 408 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 409 | |
| 410 | mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 411 | mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); |
| 412 | mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 413 | mColorSelector[MINUTES][SELECTOR_LINE] = a.getColor( |
| 414 | R.styleable.TimePicker_numbersSelectorColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 415 | R.color.timepicker_default_selector_color_material); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 416 | |
| 417 | mPaintAmPmText.setColor(mAmPmTextColor); |
| 418 | mPaintAmPmText.setTypeface(mTypeface); |
| 419 | mPaintAmPmText.setAntiAlias(true); |
| 420 | mPaintAmPmText.setTextAlign(Paint.Align.CENTER); |
| 421 | |
| 422 | mPaintAmPmCircle[AM] = new Paint(); |
| 423 | mPaintAmPmCircle[AM].setAntiAlias(true); |
| 424 | mPaintAmPmCircle[PM] = new Paint(); |
| 425 | mPaintAmPmCircle[PM].setAntiAlias(true); |
| 426 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 427 | mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, |
Alan Viverette | 830960c | 2014-06-06 15:48:55 -0700 | [diff] [blame] | 428 | res.getColor(R.color.timepicker_default_numbers_background_color_material))); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 429 | mPaintBackground.setAntiAlias(true); |
| 430 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 431 | if (DEBUG) { |
| 432 | mPaintDebug.setColor(DEBUG_COLOR); |
| 433 | mPaintDebug.setAntiAlias(true); |
| 434 | mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH); |
| 435 | mPaintDebug.setStyle(Paint.Style.STROKE); |
| 436 | mPaintDebug.setTextAlign(Paint.Align.CENTER); |
| 437 | } |
| 438 | |
| 439 | mShowHours = true; |
| 440 | mIs24HourMode = false; |
| 441 | mAmOrPm = AM; |
| 442 | mAmOrPmPressed = -1; |
| 443 | |
| 444 | initHoursAndMinutesText(); |
| 445 | initData(); |
| 446 | |
| 447 | mTransitionMidRadiusMultiplier = Float.parseFloat( |
| 448 | res.getString(R.string.timepicker_transition_mid_radius_multiplier)); |
| 449 | mTransitionEndRadiusMultiplier = Float.parseFloat( |
| 450 | res.getString(R.string.timepicker_transition_end_radius_multiplier)); |
| 451 | |
| 452 | mTextGridHeights[HOURS] = new float[7]; |
| 453 | mTextGridHeights[MINUTES] = new float[7]; |
| 454 | |
| 455 | mSelectionRadiusMultiplier = Float.parseFloat( |
| 456 | res.getString(R.string.timepicker_selection_radius_multiplier)); |
| 457 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 458 | a.recycle(); |
| 459 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 460 | setOnTouchListener(this); |
Alan Viverette | ba9bf41 | 2014-09-03 20:14:21 -0700 | [diff] [blame] | 461 | setClickable(true); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 462 | |
| 463 | // Initial values |
| 464 | final Calendar calendar = Calendar.getInstance(Locale.getDefault()); |
| 465 | final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); |
| 466 | final int currentMinute = calendar.get(Calendar.MINUTE); |
| 467 | |
| 468 | setCurrentHour(currentHour); |
| 469 | setCurrentMinute(currentMinute); |
| 470 | |
| 471 | setHapticFeedbackEnabled(true); |
| 472 | } |
| 473 | |
| 474 | /** |
| 475 | * Measure the view to end up as a square, based on the minimum of the height and width. |
| 476 | */ |
| 477 | @Override |
| 478 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| 479 | int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); |
| 480 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| 481 | int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); |
| 482 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| 483 | int minDimension = Math.min(measuredWidth, measuredHeight); |
| 484 | |
| 485 | super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), |
| 486 | MeasureSpec.makeMeasureSpec(minDimension, heightMode)); |
| 487 | } |
| 488 | |
| 489 | public void initialize(int hour, int minute, boolean is24HourMode) { |
| 490 | mIs24HourMode = is24HourMode; |
| 491 | setCurrentHour(hour); |
| 492 | setCurrentMinute(minute); |
| 493 | } |
| 494 | |
| 495 | public void setCurrentItemShowing(int item, boolean animate) { |
| 496 | switch (item){ |
| 497 | case HOURS: |
| 498 | showHours(animate); |
| 499 | break; |
| 500 | case MINUTES: |
| 501 | showMinutes(animate); |
| 502 | break; |
| 503 | default: |
| 504 | Log.e(TAG, "ClockView does not support showing item " + item); |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | public int getCurrentItemShowing() { |
| 509 | return mShowHours ? HOURS : MINUTES; |
| 510 | } |
| 511 | |
| 512 | public void setOnValueSelectedListener(OnValueSelectedListener listener) { |
| 513 | mListener = listener; |
| 514 | } |
| 515 | |
| 516 | public void setCurrentHour(int hour) { |
| 517 | final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; |
| 518 | mSelectionDegrees[HOURS] = degrees; |
| 519 | mSelectionDegrees[HOURS_INNER] = degrees; |
| 520 | mAmOrPm = ((hour % 24) < 12) ? AM : PM; |
| 521 | if (mIs24HourMode) { |
| 522 | mIsOnInnerCircle = (mAmOrPm == AM); |
| 523 | } else { |
| 524 | mIsOnInnerCircle = false; |
| 525 | } |
| 526 | initData(); |
| 527 | updateLayoutData(); |
| 528 | invalidate(); |
| 529 | } |
| 530 | |
| 531 | // Return hours in 0-23 range |
| 532 | public int getCurrentHour() { |
| 533 | int hours = |
| 534 | mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS] / DEGREES_FOR_ONE_HOUR; |
| 535 | if (mIs24HourMode) { |
| 536 | if (mIsOnInnerCircle) { |
| 537 | hours = hours % 12; |
Fabrice Di Meglio | 3fbad42 | 2014-07-24 20:13:56 -0700 | [diff] [blame] | 538 | if (hours == 0) { |
| 539 | hours = 12; |
| 540 | } |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 541 | } else { |
| 542 | if (hours != 0) { |
| 543 | hours += 12; |
| 544 | } |
| 545 | } |
| 546 | } else { |
| 547 | hours = hours % 12; |
| 548 | if (hours == 0) { |
| 549 | if (mAmOrPm == PM) { |
| 550 | hours = 12; |
| 551 | } |
| 552 | } else { |
| 553 | if (mAmOrPm == PM) { |
| 554 | hours += 12; |
| 555 | } |
| 556 | } |
| 557 | } |
| 558 | return hours; |
| 559 | } |
| 560 | |
| 561 | public void setCurrentMinute(int minute) { |
| 562 | mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; |
| 563 | invalidate(); |
| 564 | } |
| 565 | |
| 566 | // Returns minutes in 0-59 range |
| 567 | public int getCurrentMinute() { |
| 568 | return (mSelectionDegrees[MINUTES] / DEGREES_FOR_ONE_MINUTE); |
| 569 | } |
| 570 | |
| 571 | public void setAmOrPm(int val) { |
| 572 | mAmOrPm = (val % 2); |
| 573 | invalidate(); |
| 574 | } |
| 575 | |
| 576 | public int getAmOrPm() { |
| 577 | return mAmOrPm; |
| 578 | } |
| 579 | |
| 580 | public void swapAmPm() { |
| 581 | mAmOrPm = (mAmOrPm == AM) ? PM : AM; |
| 582 | invalidate(); |
| 583 | } |
| 584 | |
| 585 | public void showHours(boolean animate) { |
| 586 | if (mShowHours) return; |
| 587 | mShowHours = true; |
| 588 | if (animate) { |
| 589 | startMinutesToHoursAnimation(); |
| 590 | } |
| 591 | initData(); |
| 592 | updateLayoutData(); |
| 593 | invalidate(); |
| 594 | } |
| 595 | |
| 596 | public void showMinutes(boolean animate) { |
| 597 | if (!mShowHours) return; |
| 598 | mShowHours = false; |
| 599 | if (animate) { |
| 600 | startHoursToMinutesAnimation(); |
| 601 | } |
| 602 | initData(); |
| 603 | updateLayoutData(); |
| 604 | invalidate(); |
| 605 | } |
| 606 | |
| 607 | private void initHoursAndMinutesText() { |
| 608 | // Initialize the hours and minutes numbers. |
| 609 | for (int i = 0; i < 12; i++) { |
| 610 | mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); |
| 611 | mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); |
| 612 | mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); |
| 613 | mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); |
| 614 | } |
| 615 | |
Alan Viverette | ba9bf41 | 2014-09-03 20:14:21 -0700 | [diff] [blame] | 616 | String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(mContext); |
| 617 | mAmPmText[AM] = amPmStrings[0]; |
| 618 | mAmPmText[PM] = amPmStrings[1]; |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 619 | } |
| 620 | |
| 621 | private void initData() { |
| 622 | if (mIs24HourMode) { |
| 623 | mOuterTextHours = mOuterHours24Texts; |
| 624 | mInnerTextHours = mInnerHours24Texts; |
| 625 | } else { |
| 626 | mOuterTextHours = mHours12Texts; |
| 627 | mInnerTextHours = null; |
| 628 | } |
| 629 | |
| 630 | mOuterTextMinutes = mMinutesTexts; |
| 631 | |
| 632 | final Resources res = getResources(); |
| 633 | |
| 634 | if (mShowHours) { |
| 635 | if (mIs24HourMode) { |
| 636 | mCircleRadiusMultiplier[HOURS] = Float.parseFloat( |
| 637 | res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode)); |
| 638 | mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( |
| 639 | res.getString(R.string.timepicker_numbers_radius_multiplier_outer)); |
| 640 | mTextSizeMultiplier[HOURS] = Float.parseFloat( |
| 641 | res.getString(R.string.timepicker_text_size_multiplier_outer)); |
| 642 | |
| 643 | mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat( |
| 644 | res.getString(R.string.timepicker_numbers_radius_multiplier_inner)); |
| 645 | mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat( |
| 646 | res.getString(R.string.timepicker_text_size_multiplier_inner)); |
| 647 | } else { |
| 648 | mCircleRadiusMultiplier[HOURS] = Float.parseFloat( |
| 649 | res.getString(R.string.timepicker_circle_radius_multiplier)); |
| 650 | mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( |
| 651 | res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); |
| 652 | mTextSizeMultiplier[HOURS] = Float.parseFloat( |
| 653 | res.getString(R.string.timepicker_text_size_multiplier_normal)); |
| 654 | } |
| 655 | } else { |
| 656 | mCircleRadiusMultiplier[MINUTES] = Float.parseFloat( |
| 657 | res.getString(R.string.timepicker_circle_radius_multiplier)); |
| 658 | mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat( |
| 659 | res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); |
| 660 | mTextSizeMultiplier[MINUTES] = Float.parseFloat( |
| 661 | res.getString(R.string.timepicker_text_size_multiplier_normal)); |
| 662 | } |
| 663 | |
| 664 | mAnimationRadiusMultiplier[HOURS] = 1; |
| 665 | mAnimationRadiusMultiplier[HOURS_INNER] = 1; |
| 666 | mAnimationRadiusMultiplier[MINUTES] = 1; |
| 667 | |
| 668 | mAmPmCircleRadiusMultiplier = Float.parseFloat( |
| 669 | res.getString(R.string.timepicker_ampm_circle_radius_multiplier)); |
| 670 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 671 | mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); |
| 672 | mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 673 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 674 | mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue( |
| 675 | mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); |
| 676 | mAlphaSelector[HOURS][SELECTOR_DOT].setValue( |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 677 | mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 678 | mAlphaSelector[HOURS][SELECTOR_LINE].setValue( |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 679 | mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); |
| 680 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 681 | mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue( |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 682 | mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 683 | mAlphaSelector[MINUTES][SELECTOR_DOT].setValue( |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 684 | mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 685 | mAlphaSelector[MINUTES][SELECTOR_LINE].setValue( |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 686 | mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); |
| 687 | } |
| 688 | |
| 689 | @Override |
| 690 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| 691 | updateLayoutData(); |
| 692 | } |
| 693 | |
| 694 | private void updateLayoutData() { |
| 695 | mXCenter = getWidth() / 2; |
| 696 | mYCenter = getHeight() / 2; |
| 697 | |
| 698 | final int min = Math.min(mXCenter, mYCenter); |
| 699 | |
| 700 | mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS]; |
| 701 | mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; |
| 702 | mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; |
| 703 | |
| 704 | if (!mIs24HourMode) { |
| 705 | // We'll need to draw the AM/PM circles, so the main circle will need to have |
| 706 | // a slightly higher center. To keep the entire view centered vertically, we'll |
| 707 | // have to push it up by half the radius of the AM/PM circles. |
| 708 | int amPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); |
| 709 | mYCenter -= amPmCircleRadius / 2; |
| 710 | } |
| 711 | |
| 712 | mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] |
| 713 | * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; |
| 714 | mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] |
| 715 | * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS]; |
| 716 | mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS] |
| 717 | * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2)); |
| 718 | |
| 719 | mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS]; |
| 720 | mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES]; |
| 721 | |
| 722 | if (mIs24HourMode) { |
| 723 | mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER]; |
| 724 | } |
| 725 | |
| 726 | calculateGridSizesHours(); |
| 727 | calculateGridSizesMinutes(); |
| 728 | |
| 729 | mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier); |
| 730 | mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS]; |
| 731 | mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); |
| 732 | |
| 733 | mAmPmCircleRadius = (int) (mCircleRadius[HOURS] * mAmPmCircleRadiusMultiplier); |
| 734 | mPaintAmPmText.setTextSize(mAmPmCircleRadius * 3 / 4); |
| 735 | |
| 736 | // Line up the vertical center of the AM/PM circles with the bottom of the main circle. |
| 737 | mAmPmYCenter = mYCenter + mCircleRadius[HOURS]; |
| 738 | |
| 739 | // Line up the horizontal edges of the AM/PM circles with the horizontal edges |
| 740 | // of the main circle |
| 741 | mLeftIndicatorXCenter = mXCenter - mCircleRadius[HOURS] + mAmPmCircleRadius; |
| 742 | mRightIndicatorXCenter = mXCenter + mCircleRadius[HOURS] - mAmPmCircleRadius; |
| 743 | } |
| 744 | |
| 745 | @Override |
| 746 | public void onDraw(Canvas canvas) { |
Alan Viverette | 5134478 | 2014-07-16 17:39:27 -0700 | [diff] [blame] | 747 | if (!mInputEnabled) { |
| 748 | canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), mDisabledAlpha); |
| 749 | } else { |
| 750 | canvas.save(); |
| 751 | } |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 752 | |
| 753 | calculateGridSizesHours(); |
| 754 | calculateGridSizesMinutes(); |
| 755 | |
| 756 | drawCircleBackground(canvas); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 757 | drawSelector(canvas); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 758 | |
| 759 | drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 760 | mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS], |
| 761 | mColor[HOURS], mAlpha[HOURS].getValue()); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 762 | |
| 763 | if (mIs24HourMode && mInnerTextHours != null) { |
| 764 | drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 765 | mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS], |
| 766 | mColor[HOURS], mAlpha[HOURS].getValue()); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 767 | } |
| 768 | |
| 769 | drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 770 | mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES], |
| 771 | mColor[MINUTES], mAlpha[MINUTES].getValue()); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 772 | |
| 773 | drawCenter(canvas); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 774 | if (!mIs24HourMode) { |
| 775 | drawAmPm(canvas); |
| 776 | } |
| 777 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 778 | if (DEBUG) { |
| 779 | drawDebug(canvas); |
| 780 | } |
| 781 | |
| 782 | canvas.restore(); |
| 783 | } |
| 784 | |
| 785 | private void drawCircleBackground(Canvas canvas) { |
| 786 | canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground); |
| 787 | } |
| 788 | |
| 789 | private void drawCenter(Canvas canvas) { |
| 790 | canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter); |
| 791 | } |
| 792 | |
| 793 | private void drawSelector(Canvas canvas) { |
| 794 | drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS); |
| 795 | drawSelector(canvas, MINUTES); |
| 796 | } |
| 797 | |
| 798 | private void drawAmPm(Canvas canvas) { |
| 799 | final boolean isLayoutRtl = isLayoutRtl(); |
| 800 | |
| 801 | int amColor = mAmPmUnselectedColor; |
| 802 | int amAlpha = ALPHA_OPAQUE; |
| 803 | int pmColor = mAmPmUnselectedColor; |
| 804 | int pmAlpha = ALPHA_OPAQUE; |
| 805 | if (mAmOrPm == AM) { |
| 806 | amColor = mAmPmSelectedColor; |
| 807 | amAlpha = ALPHA_AMPM_SELECTED; |
| 808 | } else if (mAmOrPm == PM) { |
| 809 | pmColor = mAmPmSelectedColor; |
| 810 | pmAlpha = ALPHA_AMPM_SELECTED; |
| 811 | } |
| 812 | if (mAmOrPmPressed == AM) { |
| 813 | amColor = mAmPmSelectedColor; |
| 814 | amAlpha = ALPHA_AMPM_PRESSED; |
| 815 | } else if (mAmOrPmPressed == PM) { |
| 816 | pmColor = mAmPmSelectedColor; |
| 817 | pmAlpha = ALPHA_AMPM_PRESSED; |
| 818 | } |
| 819 | |
| 820 | // Draw the two circles |
| 821 | mPaintAmPmCircle[AM].setColor(amColor); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 822 | mPaintAmPmCircle[AM].setAlpha(getMultipliedAlpha(amColor, amAlpha)); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 823 | canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter, |
| 824 | mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]); |
| 825 | |
| 826 | mPaintAmPmCircle[PM].setColor(pmColor); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 827 | mPaintAmPmCircle[PM].setAlpha(getMultipliedAlpha(pmColor, pmAlpha)); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 828 | canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter, |
| 829 | mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]); |
| 830 | |
| 831 | // Draw the AM/PM texts on top |
| 832 | mPaintAmPmText.setColor(mAmPmTextColor); |
| 833 | float textYCenter = mAmPmYCenter - |
| 834 | (int) (mPaintAmPmText.descent() + mPaintAmPmText.ascent()) / 2; |
| 835 | |
| 836 | canvas.drawText(isLayoutRtl ? mAmPmText[PM] : mAmPmText[AM], mLeftIndicatorXCenter, |
| 837 | textYCenter, mPaintAmPmText); |
| 838 | canvas.drawText(isLayoutRtl ? mAmPmText[AM] : mAmPmText[PM], mRightIndicatorXCenter, |
| 839 | textYCenter, mPaintAmPmText); |
| 840 | } |
| 841 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 842 | private int getMultipliedAlpha(int argb, int alpha) { |
| 843 | return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); |
| 844 | } |
| 845 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 846 | private void drawSelector(Canvas canvas, int index) { |
| 847 | // Calculate the current radius at which to place the selection circle. |
| 848 | mLineLength[index] = (int) (mCircleRadius[index] |
| 849 | * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]); |
| 850 | |
| 851 | double selectionRadians = Math.toRadians(mSelectionDegrees[index]); |
| 852 | |
| 853 | int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); |
| 854 | int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); |
| 855 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 856 | int color; |
| 857 | int alpha; |
| 858 | Paint paint; |
| 859 | |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 860 | // Draw the selection circle |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 861 | color = mColorSelector[index % 2][SELECTOR_CIRCLE]; |
| 862 | alpha = mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue(); |
| 863 | paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; |
| 864 | paint.setColor(color); |
| 865 | paint.setAlpha(getMultipliedAlpha(color, alpha)); |
| 866 | canvas.drawCircle(pointX, pointY, mSelectionRadius[index], paint); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 867 | |
| 868 | // Draw the dot if needed |
| 869 | if (mSelectionDegrees[index] % 30 != 0) { |
| 870 | // We're not on a direct tick |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 871 | color = mColorSelector[index % 2][SELECTOR_DOT]; |
| 872 | alpha = mAlphaSelector[index % 2][SELECTOR_DOT].getValue(); |
| 873 | paint = mPaintSelector[index % 2][SELECTOR_DOT]; |
| 874 | paint.setColor(color); |
| 875 | paint.setAlpha(getMultipliedAlpha(color, alpha)); |
| 876 | canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), paint); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 877 | } else { |
| 878 | // We're not drawing the dot, so shorten the line to only go as far as the edge of the |
| 879 | // selection circle |
| 880 | int lineLength = mLineLength[index] - mSelectionRadius[index]; |
| 881 | pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians)); |
| 882 | pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians)); |
| 883 | } |
| 884 | |
| 885 | // Draw the line |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 886 | color = mColorSelector[index % 2][SELECTOR_LINE]; |
| 887 | alpha = mAlphaSelector[index % 2][SELECTOR_LINE].getValue(); |
| 888 | paint = mPaintSelector[index % 2][SELECTOR_LINE]; |
| 889 | paint.setColor(color); |
| 890 | paint.setAlpha(getMultipliedAlpha(color, alpha)); |
| 891 | canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 892 | } |
| 893 | |
| 894 | private void drawDebug(Canvas canvas) { |
| 895 | // Draw outer numbers circle |
| 896 | final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; |
| 897 | canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug); |
| 898 | |
| 899 | // Draw inner numbers circle |
| 900 | final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER]; |
| 901 | canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug); |
| 902 | |
| 903 | // Draw outer background circle |
| 904 | canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug); |
| 905 | |
| 906 | // Draw outer rectangle for circles |
| 907 | float left = mXCenter - outerRadius; |
| 908 | float top = mYCenter - outerRadius; |
| 909 | float right = mXCenter + outerRadius; |
| 910 | float bottom = mYCenter + outerRadius; |
| 911 | mRectF = new RectF(left, top, right, bottom); |
| 912 | canvas.drawRect(mRectF, mPaintDebug); |
| 913 | |
| 914 | // Draw outer rectangle for background |
| 915 | left = mXCenter - mCircleRadius[HOURS]; |
| 916 | top = mYCenter - mCircleRadius[HOURS]; |
| 917 | right = mXCenter + mCircleRadius[HOURS]; |
| 918 | bottom = mYCenter + mCircleRadius[HOURS]; |
| 919 | mRectF.set(left, top, right, bottom); |
| 920 | canvas.drawRect(mRectF, mPaintDebug); |
| 921 | |
| 922 | // Draw outer view rectangle |
| 923 | mRectF.set(0, 0, getWidth(), getHeight()); |
| 924 | canvas.drawRect(mRectF, mPaintDebug); |
| 925 | |
| 926 | // Draw selected time |
| 927 | final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); |
| 928 | |
| 929 | ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, |
| 930 | ViewGroup.LayoutParams.WRAP_CONTENT); |
| 931 | TextView tv = new TextView(getContext()); |
| 932 | tv.setLayoutParams(lp); |
| 933 | tv.setText(selected); |
| 934 | tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); |
| 935 | Paint paint = tv.getPaint(); |
| 936 | paint.setColor(DEBUG_TEXT_COLOR); |
| 937 | |
| 938 | final int width = tv.getMeasuredWidth(); |
| 939 | |
| 940 | float height = paint.descent() - paint.ascent(); |
| 941 | float x = mXCenter - width / 2; |
| 942 | float y = mYCenter + 1.5f * height; |
| 943 | |
| 944 | canvas.drawText(selected.toString(), x, y, paint); |
| 945 | } |
| 946 | |
| 947 | private void calculateGridSizesHours() { |
| 948 | // Calculate the text positions |
| 949 | float numbersRadius = mCircleRadius[HOURS] |
| 950 | * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS]; |
| 951 | |
| 952 | // Calculate the positions for the 12 numbers in the main circle. |
| 953 | calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, |
| 954 | mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]); |
| 955 | |
| 956 | // If we have an inner circle, calculate those positions too. |
| 957 | if (mIs24HourMode) { |
| 958 | float innerNumbersRadius = mCircleRadius[HOURS_INNER] |
| 959 | * mNumbersRadiusMultiplier[HOURS_INNER] |
| 960 | * mAnimationRadiusMultiplier[HOURS_INNER]; |
| 961 | |
| 962 | calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, |
| 963 | mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); |
| 964 | } |
| 965 | } |
| 966 | |
| 967 | private void calculateGridSizesMinutes() { |
| 968 | // Calculate the text positions |
| 969 | float numbersRadius = mCircleRadius[MINUTES] |
| 970 | * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES]; |
| 971 | |
| 972 | // Calculate the positions for the 12 numbers in the main circle. |
| 973 | calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, |
| 974 | mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]); |
| 975 | } |
| 976 | |
| 977 | |
| 978 | /** |
| 979 | * Using the trigonometric Unit Circle, calculate the positions that the text will need to be |
| 980 | * drawn at based on the specified circle radius. Place the values in the textGridHeights and |
| 981 | * textGridWidths parameters. |
| 982 | */ |
| 983 | private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter, |
| 984 | float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) { |
| 985 | /* |
| 986 | * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. |
| 987 | */ |
| 988 | final float offset1 = numbersRadius; |
| 989 | // cos(30) = a / r => r * cos(30) |
| 990 | final float offset2 = numbersRadius * COSINE_30_DEGREES; |
| 991 | // sin(30) = o / r => r * sin(30) |
| 992 | final float offset3 = numbersRadius * SINE_30_DEGREES; |
| 993 | |
| 994 | paint.setTextSize(textSize); |
| 995 | // We'll need yTextBase to be slightly lower to account for the text's baseline. |
| 996 | yCenter -= (paint.descent() + paint.ascent()) / 2; |
| 997 | |
| 998 | textGridHeights[0] = yCenter - offset1; |
| 999 | textGridWidths[0] = xCenter - offset1; |
| 1000 | textGridHeights[1] = yCenter - offset2; |
| 1001 | textGridWidths[1] = xCenter - offset2; |
| 1002 | textGridHeights[2] = yCenter - offset3; |
| 1003 | textGridWidths[2] = xCenter - offset3; |
| 1004 | textGridHeights[3] = yCenter; |
| 1005 | textGridWidths[3] = xCenter; |
| 1006 | textGridHeights[4] = yCenter + offset3; |
| 1007 | textGridWidths[4] = xCenter + offset3; |
| 1008 | textGridHeights[5] = yCenter + offset2; |
| 1009 | textGridWidths[5] = xCenter + offset2; |
| 1010 | textGridHeights[6] = yCenter + offset1; |
| 1011 | textGridWidths[6] = xCenter + offset1; |
| 1012 | } |
| 1013 | |
| 1014 | /** |
| 1015 | * Draw the 12 text values at the positions specified by the textGrid parameters. |
| 1016 | */ |
| 1017 | private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts, |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1018 | float[] textGridWidths, float[] textGridHeights, Paint paint, int color, int alpha) { |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1019 | paint.setTextSize(textSize); |
| 1020 | paint.setTypeface(typeface); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1021 | paint.setColor(color); |
| 1022 | paint.setAlpha(getMultipliedAlpha(color, alpha)); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1023 | canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint); |
| 1024 | canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint); |
| 1025 | canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint); |
| 1026 | canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint); |
| 1027 | canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint); |
| 1028 | canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint); |
| 1029 | canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint); |
| 1030 | canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint); |
| 1031 | canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint); |
| 1032 | canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint); |
| 1033 | canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint); |
| 1034 | canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint); |
| 1035 | } |
| 1036 | |
| 1037 | // Used for animating the hours by changing their radius |
| 1038 | private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { |
| 1039 | mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; |
| 1040 | mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; |
| 1041 | } |
| 1042 | |
| 1043 | // Used for animating the minutes by changing their radius |
| 1044 | private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { |
| 1045 | mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; |
| 1046 | } |
| 1047 | |
| 1048 | private static ObjectAnimator getRadiusDisappearAnimator(Object target, |
| 1049 | String radiusPropertyName, InvalidateUpdateListener updateListener, |
| 1050 | float midRadiusMultiplier, float endRadiusMultiplier) { |
| 1051 | Keyframe kf0, kf1, kf2; |
| 1052 | float midwayPoint = 0.2f; |
| 1053 | int duration = 500; |
| 1054 | |
| 1055 | kf0 = Keyframe.ofFloat(0f, 1); |
| 1056 | kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); |
| 1057 | kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier); |
| 1058 | PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( |
| 1059 | radiusPropertyName, kf0, kf1, kf2); |
| 1060 | |
| 1061 | ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( |
| 1062 | target, radiusDisappear).setDuration(duration); |
| 1063 | animator.addUpdateListener(updateListener); |
| 1064 | return animator; |
| 1065 | } |
| 1066 | |
| 1067 | private static ObjectAnimator getRadiusReappearAnimator(Object target, |
| 1068 | String radiusPropertyName, InvalidateUpdateListener updateListener, |
| 1069 | float midRadiusMultiplier, float endRadiusMultiplier) { |
| 1070 | Keyframe kf0, kf1, kf2, kf3; |
| 1071 | float midwayPoint = 0.2f; |
| 1072 | int duration = 500; |
| 1073 | |
| 1074 | // Set up animator for reappearing. |
| 1075 | float delayMultiplier = 0.25f; |
| 1076 | float transitionDurationMultiplier = 1f; |
| 1077 | float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; |
| 1078 | int totalDuration = (int) (duration * totalDurationMultiplier); |
| 1079 | float delayPoint = (delayMultiplier * duration) / totalDuration; |
| 1080 | midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); |
| 1081 | |
| 1082 | kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier); |
| 1083 | kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier); |
| 1084 | kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); |
| 1085 | kf3 = Keyframe.ofFloat(1f, 1); |
| 1086 | PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( |
| 1087 | radiusPropertyName, kf0, kf1, kf2, kf3); |
| 1088 | |
| 1089 | ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( |
| 1090 | target, radiusReappear).setDuration(totalDuration); |
| 1091 | animator.addUpdateListener(updateListener); |
| 1092 | return animator; |
| 1093 | } |
| 1094 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1095 | private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha, |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1096 | InvalidateUpdateListener updateListener) { |
| 1097 | int duration = 500; |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1098 | ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1099 | animator.setDuration(duration); |
| 1100 | animator.addUpdateListener(updateListener); |
| 1101 | |
| 1102 | return animator; |
| 1103 | } |
| 1104 | |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1105 | private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha, |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1106 | InvalidateUpdateListener updateListener) { |
| 1107 | Keyframe kf0, kf1, kf2; |
| 1108 | int duration = 500; |
| 1109 | |
| 1110 | // Set up animator for reappearing. |
| 1111 | float delayMultiplier = 0.25f; |
| 1112 | float transitionDurationMultiplier = 1f; |
| 1113 | float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; |
| 1114 | int totalDuration = (int) (duration * totalDurationMultiplier); |
| 1115 | float delayPoint = (delayMultiplier * duration) / totalDuration; |
| 1116 | |
| 1117 | kf0 = Keyframe.ofInt(0f, startAlpha); |
| 1118 | kf1 = Keyframe.ofInt(delayPoint, startAlpha); |
| 1119 | kf2 = Keyframe.ofInt(1f, endAlpha); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1120 | PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1121 | |
| 1122 | ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( |
| 1123 | target, fadeIn).setDuration(totalDuration); |
| 1124 | animator.addUpdateListener(updateListener); |
| 1125 | return animator; |
| 1126 | } |
| 1127 | |
| 1128 | private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { |
| 1129 | @Override |
| 1130 | public void onAnimationUpdate(ValueAnimator animation) { |
| 1131 | RadialTimePickerView.this.invalidate(); |
| 1132 | } |
| 1133 | } |
| 1134 | |
| 1135 | private void startHoursToMinutesAnimation() { |
| 1136 | if (mHoursToMinutesAnims.size() == 0) { |
| 1137 | mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, |
| 1138 | "animationRadiusMultiplierHours", mInvalidateUpdateListener, |
| 1139 | mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1140 | mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1141 | ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1142 | mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1143 | ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1144 | mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1145 | ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1146 | mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1147 | ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
| 1148 | |
| 1149 | mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, |
| 1150 | "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, |
| 1151 | mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1152 | mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1153 | ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1154 | mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1155 | ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1156 | mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1157 | ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1158 | mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1159 | ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); |
| 1160 | } |
| 1161 | |
| 1162 | if (mTransition != null && mTransition.isRunning()) { |
| 1163 | mTransition.end(); |
| 1164 | } |
| 1165 | mTransition = new AnimatorSet(); |
| 1166 | mTransition.playTogether(mHoursToMinutesAnims); |
| 1167 | mTransition.start(); |
| 1168 | } |
| 1169 | |
| 1170 | private void startMinutesToHoursAnimation() { |
| 1171 | if (mMinuteToHoursAnims.size() == 0) { |
| 1172 | mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, |
| 1173 | "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, |
| 1174 | mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1175 | mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1176 | ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1177 | mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1178 | ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1179 | mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1180 | ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1181 | mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1182 | ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); |
| 1183 | |
| 1184 | mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, |
| 1185 | "animationRadiusMultiplierHours", mInvalidateUpdateListener, |
| 1186 | mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1187 | mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1188 | ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1189 | mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1190 | ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1191 | mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1192 | ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1193 | mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1194 | ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); |
| 1195 | } |
| 1196 | |
| 1197 | if (mTransition != null && mTransition.isRunning()) { |
| 1198 | mTransition.end(); |
| 1199 | } |
| 1200 | mTransition = new AnimatorSet(); |
| 1201 | mTransition.playTogether(mMinuteToHoursAnims); |
| 1202 | mTransition.start(); |
| 1203 | } |
| 1204 | |
| 1205 | private int getDegreesFromXY(float x, float y) { |
| 1206 | final double hypotenuse = Math.sqrt( |
| 1207 | (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter)); |
| 1208 | |
| 1209 | // Basic check if we're outside the range of the disk |
| 1210 | if (hypotenuse > mCircleRadius[HOURS]) { |
| 1211 | return -1; |
| 1212 | } |
| 1213 | // Check |
| 1214 | if (mIs24HourMode && mShowHours) { |
| 1215 | if (hypotenuse >= mMinHypotenuseForInnerNumber |
| 1216 | && hypotenuse <= mHalfwayHypotenusePoint) { |
| 1217 | mIsOnInnerCircle = true; |
| 1218 | } else if (hypotenuse <= mMaxHypotenuseForOuterNumber |
| 1219 | && hypotenuse >= mHalfwayHypotenusePoint) { |
| 1220 | mIsOnInnerCircle = false; |
| 1221 | } else { |
| 1222 | return -1; |
| 1223 | } |
| 1224 | } else { |
| 1225 | final int index = (mShowHours) ? HOURS : MINUTES; |
| 1226 | final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]); |
| 1227 | final int distanceToNumber = (int) Math.abs(hypotenuse - length); |
| 1228 | final int maxAllowedDistance = |
| 1229 | (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index])); |
| 1230 | if (distanceToNumber > maxAllowedDistance) { |
| 1231 | return -1; |
| 1232 | } |
| 1233 | } |
| 1234 | |
| 1235 | final float opposite = Math.abs(y - mYCenter); |
| 1236 | double degrees = Math.toDegrees(Math.asin(opposite / hypotenuse)); |
| 1237 | |
| 1238 | // Now we have to translate to the correct quadrant. |
| 1239 | boolean rightSide = (x > mXCenter); |
| 1240 | boolean topSide = (y < mYCenter); |
| 1241 | if (rightSide && topSide) { |
| 1242 | degrees = 90 - degrees; |
| 1243 | } else if (rightSide && !topSide) { |
| 1244 | degrees = 90 + degrees; |
| 1245 | } else if (!rightSide && !topSide) { |
| 1246 | degrees = 270 - degrees; |
| 1247 | } else if (!rightSide && topSide) { |
| 1248 | degrees = 270 + degrees; |
| 1249 | } |
| 1250 | return (int) degrees; |
| 1251 | } |
| 1252 | |
| 1253 | private int getIsTouchingAmOrPm(float x, float y) { |
| 1254 | final boolean isLayoutRtl = isLayoutRtl(); |
| 1255 | int squaredYDistance = (int) ((y - mAmPmYCenter) * (y - mAmPmYCenter)); |
| 1256 | |
| 1257 | int distanceToAmCenter = (int) Math.sqrt( |
| 1258 | (x - mLeftIndicatorXCenter) * (x - mLeftIndicatorXCenter) + squaredYDistance); |
| 1259 | if (distanceToAmCenter <= mAmPmCircleRadius) { |
| 1260 | return (isLayoutRtl ? PM : AM); |
| 1261 | } |
| 1262 | |
| 1263 | int distanceToPmCenter = (int) Math.sqrt( |
| 1264 | (x - mRightIndicatorXCenter) * (x - mRightIndicatorXCenter) + squaredYDistance); |
| 1265 | if (distanceToPmCenter <= mAmPmCircleRadius) { |
| 1266 | return (isLayoutRtl ? AM : PM); |
| 1267 | } |
| 1268 | |
| 1269 | // Neither was close enough. |
| 1270 | return -1; |
| 1271 | } |
| 1272 | |
| 1273 | @Override |
| 1274 | public boolean onTouch(View v, MotionEvent event) { |
| 1275 | if(!mInputEnabled) { |
| 1276 | return true; |
| 1277 | } |
| 1278 | |
| 1279 | final float eventX = event.getX(); |
| 1280 | final float eventY = event.getY(); |
| 1281 | |
| 1282 | int degrees; |
| 1283 | int snapDegrees; |
| 1284 | boolean result = false; |
| 1285 | |
| 1286 | switch(event.getAction()) { |
| 1287 | case MotionEvent.ACTION_DOWN: |
| 1288 | case MotionEvent.ACTION_MOVE: |
| 1289 | mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); |
| 1290 | if (mAmOrPmPressed != -1) { |
| 1291 | result = true; |
| 1292 | } else { |
| 1293 | degrees = getDegreesFromXY(eventX, eventY); |
| 1294 | if (degrees != -1) { |
| 1295 | snapDegrees = (mShowHours ? |
| 1296 | snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; |
| 1297 | if (mShowHours) { |
| 1298 | mSelectionDegrees[HOURS] = snapDegrees; |
| 1299 | mSelectionDegrees[HOURS_INNER] = snapDegrees; |
| 1300 | } else { |
| 1301 | mSelectionDegrees[MINUTES] = snapDegrees; |
| 1302 | } |
| 1303 | performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); |
| 1304 | if (mListener != null) { |
| 1305 | if (mShowHours) { |
| 1306 | mListener.onValueSelected(HOURS, getCurrentHour(), false); |
| 1307 | } else { |
| 1308 | mListener.onValueSelected(MINUTES, getCurrentMinute(), false); |
| 1309 | } |
| 1310 | } |
| 1311 | result = true; |
| 1312 | } |
| 1313 | } |
| 1314 | invalidate(); |
| 1315 | return result; |
| 1316 | |
| 1317 | case MotionEvent.ACTION_UP: |
| 1318 | mAmOrPmPressed = getIsTouchingAmOrPm(eventX, eventY); |
| 1319 | if (mAmOrPmPressed != -1) { |
| 1320 | if (mAmOrPm != mAmOrPmPressed) { |
| 1321 | swapAmPm(); |
| 1322 | } |
| 1323 | mAmOrPmPressed = -1; |
| 1324 | if (mListener != null) { |
| 1325 | mListener.onValueSelected(AMPM, getCurrentHour(), true); |
| 1326 | } |
| 1327 | result = true; |
| 1328 | } else { |
| 1329 | degrees = getDegreesFromXY(eventX, eventY); |
| 1330 | if (degrees != -1) { |
| 1331 | snapDegrees = (mShowHours ? |
| 1332 | snapOnly30s(degrees, 0) : snapPrefer30s(degrees)) % 360; |
| 1333 | if (mShowHours) { |
| 1334 | mSelectionDegrees[HOURS] = snapDegrees; |
| 1335 | mSelectionDegrees[HOURS_INNER] = snapDegrees; |
| 1336 | } else { |
| 1337 | mSelectionDegrees[MINUTES] = snapDegrees; |
| 1338 | } |
| 1339 | if (mListener != null) { |
| 1340 | if (mShowHours) { |
| 1341 | mListener.onValueSelected(HOURS, getCurrentHour(), true); |
| 1342 | } else { |
| 1343 | mListener.onValueSelected(MINUTES, getCurrentMinute(), true); |
| 1344 | } |
| 1345 | } |
| 1346 | result = true; |
| 1347 | } |
| 1348 | } |
| 1349 | if (result) { |
| 1350 | invalidate(); |
| 1351 | } |
| 1352 | return result; |
| 1353 | |
| 1354 | default: |
| 1355 | break; |
| 1356 | } |
| 1357 | return false; |
| 1358 | } |
| 1359 | |
| 1360 | /** |
| 1361 | * Necessary for accessibility, to ensure we support "scrolling" forward and backward |
| 1362 | * in the circle. |
| 1363 | */ |
| 1364 | @Override |
| 1365 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| 1366 | super.onInitializeAccessibilityNodeInfo(info); |
| 1367 | info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); |
| 1368 | info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); |
| 1369 | } |
| 1370 | |
| 1371 | /** |
| 1372 | * Announce the currently-selected time when launched. |
| 1373 | */ |
| 1374 | @Override |
| 1375 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| 1376 | if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { |
| 1377 | // Clear the event's current text so that only the current time will be spoken. |
| 1378 | event.getText().clear(); |
| 1379 | Time time = new Time(); |
| 1380 | time.hour = getCurrentHour(); |
| 1381 | time.minute = getCurrentMinute(); |
| 1382 | long millis = time.normalize(true); |
| 1383 | int flags = DateUtils.FORMAT_SHOW_TIME; |
| 1384 | if (mIs24HourMode) { |
| 1385 | flags |= DateUtils.FORMAT_24HOUR; |
| 1386 | } |
| 1387 | String timeString = DateUtils.formatDateTime(getContext(), millis, flags); |
| 1388 | event.getText().add(timeString); |
| 1389 | return true; |
| 1390 | } |
| 1391 | return super.dispatchPopulateAccessibilityEvent(event); |
| 1392 | } |
| 1393 | |
| 1394 | /** |
| 1395 | * When scroll forward/backward events are received, jump the time to the higher/lower |
| 1396 | * discrete, visible value on the circle. |
| 1397 | */ |
| 1398 | @SuppressLint("NewApi") |
| 1399 | @Override |
| 1400 | public boolean performAccessibilityAction(int action, Bundle arguments) { |
| 1401 | if (super.performAccessibilityAction(action, arguments)) { |
| 1402 | return true; |
| 1403 | } |
| 1404 | |
| 1405 | int changeMultiplier = 0; |
| 1406 | if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { |
| 1407 | changeMultiplier = 1; |
| 1408 | } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { |
| 1409 | changeMultiplier = -1; |
| 1410 | } |
| 1411 | if (changeMultiplier != 0) { |
| 1412 | int value = 0; |
| 1413 | int stepSize = 0; |
| 1414 | if (mShowHours) { |
| 1415 | stepSize = DEGREES_FOR_ONE_HOUR; |
| 1416 | value = getCurrentHour() % 12; |
| 1417 | } else { |
| 1418 | stepSize = DEGREES_FOR_ONE_MINUTE; |
| 1419 | value = getCurrentMinute(); |
| 1420 | } |
| 1421 | |
| 1422 | int degrees = value * stepSize; |
| 1423 | degrees = snapOnly30s(degrees, changeMultiplier); |
| 1424 | value = degrees / stepSize; |
| 1425 | int maxValue = 0; |
| 1426 | int minValue = 0; |
| 1427 | if (mShowHours) { |
| 1428 | if (mIs24HourMode) { |
| 1429 | maxValue = 23; |
| 1430 | } else { |
| 1431 | maxValue = 12; |
| 1432 | minValue = 1; |
| 1433 | } |
| 1434 | } else { |
| 1435 | maxValue = 55; |
| 1436 | } |
| 1437 | if (value > maxValue) { |
| 1438 | // If we scrolled forward past the highest number, wrap around to the lowest. |
| 1439 | value = minValue; |
| 1440 | } else if (value < minValue) { |
| 1441 | // If we scrolled backward past the lowest number, wrap around to the highest. |
| 1442 | value = maxValue; |
| 1443 | } |
| 1444 | if (mShowHours) { |
| 1445 | setCurrentHour(value); |
| 1446 | if (mListener != null) { |
| 1447 | mListener.onValueSelected(HOURS, value, false); |
| 1448 | } |
| 1449 | } else { |
| 1450 | setCurrentMinute(value); |
| 1451 | if (mListener != null) { |
| 1452 | mListener.onValueSelected(MINUTES, value, false); |
| 1453 | } |
| 1454 | } |
| 1455 | return true; |
| 1456 | } |
| 1457 | |
| 1458 | return false; |
| 1459 | } |
| 1460 | |
| 1461 | public void setInputEnabled(boolean inputEnabled) { |
| 1462 | mInputEnabled = inputEnabled; |
| 1463 | invalidate(); |
| 1464 | } |
Alan Viverette | eb1d379 | 2014-06-03 18:36:03 -0700 | [diff] [blame] | 1465 | |
| 1466 | private static class IntHolder { |
| 1467 | private int mValue; |
| 1468 | |
| 1469 | public IntHolder(int value) { |
| 1470 | mValue = value; |
| 1471 | } |
| 1472 | |
| 1473 | public void setValue(int value) { |
| 1474 | mValue = value; |
| 1475 | } |
| 1476 | |
| 1477 | public int getValue() { |
| 1478 | return mValue; |
| 1479 | } |
| 1480 | } |
Fabrice Di Meglio | eeff63a | 2013-08-05 12:07:24 -0700 | [diff] [blame] | 1481 | } |