blob: f3600b0de22bf6d0c54c17c86e0a963da717e370 [file] [log] [blame]
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001/*
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
17package android.widget;
18
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070019import android.animation.ObjectAnimator;
Alan Viverette66a85622016-08-04 13:24:14 -040020import android.annotation.IntDef;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070021import android.content.Context;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080022import android.content.res.ColorStateList;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070023import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080028import android.graphics.Path;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070029import android.graphics.Rect;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080030import android.graphics.Region;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070031import android.graphics.Typeface;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070032import android.os.Bundle;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070033import android.util.AttributeSet;
Alan Viverette2b4dc112015-10-02 15:29:43 -040034import android.util.FloatProperty;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070035import android.util.IntArray;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070036import android.util.Log;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070037import android.util.MathUtils;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080038import android.util.StateSet;
Alan Viverette51344782014-07-16 17:39:27 -070039import android.util.TypedValue;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070040import android.view.HapticFeedbackConstants;
41import android.view.MotionEvent;
Aurimas Liutikas99441c52016-10-11 16:48:32 -070042import android.view.PointerIcon;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070043import android.view.View;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070044import android.view.accessibility.AccessibilityEvent;
45import android.view.accessibility.AccessibilityNodeInfo;
Alan Viveretteffb46bf2014-10-24 12:06:11 -070046import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Alan Viveretteeb1d3792014-06-03 18:36:03 -070047
Aurimas Liutikas99441c52016-10-11 16:48:32 -070048import com.android.internal.R;
49import com.android.internal.widget.ExploreByTouchHelper;
50
Alan Viverette66a85622016-08-04 13:24:14 -040051import java.lang.annotation.Retention;
52import java.lang.annotation.RetentionPolicy;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070053import java.util.Calendar;
54import java.util.Locale;
55
56/**
57 * View to show a clock circle picker (with one or two picking circles)
58 *
59 * @hide
60 */
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080061public class RadialTimePickerView extends View {
62 private static final String TAG = "RadialTimePickerView";
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070063
Alan Viveretteb0f54612016-04-12 14:58:09 -040064 public static final int HOURS = 0;
65 public static final int MINUTES = 1;
Alan Viverette66a85622016-08-04 13:24:14 -040066
67 /** @hide */
68 @IntDef({HOURS, MINUTES})
69 @Retention(RetentionPolicy.SOURCE)
70 @interface PickerType {}
71
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070072 private static final int HOURS_INNER = 2;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070073
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
Alan Viverettefc76a642015-04-07 15:37:30 -070081 private static final int HOURS_IN_CIRCLE = 12;
82 private static final int MINUTES_IN_CIRCLE = 60;
83 private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE;
84 private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -070085
86 private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
87 private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
88 private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
89
Alan Viverette2b4dc112015-10-02 15:29:43 -040090 private static final int ANIM_DURATION_NORMAL = 500;
91 private static final int ANIM_DURATION_TOUCH = 60;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -080092
93 private static final int[] SNAP_PREFER_30S_MAP = new int[361];
94
95 private static final int NUM_POSITIONS = 12;
96 private static final float[] COS_30 = new float[NUM_POSITIONS];
97 private static final float[] SIN_30 = new float[NUM_POSITIONS];
98
Alan Viverette2b4dc112015-10-02 15:29:43 -040099 /** "Something is wrong" color used when a color attribute is missing. */
100 private static final int MISSING_COLOR = Color.MAGENTA;
101
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800102 static {
103 // Prepare mapping to snap touchable degrees to selectable degrees.
104 preparePrefer30sMap();
105
106 final double increment = 2.0 * Math.PI / NUM_POSITIONS;
107 double angle = Math.PI / 2.0;
108 for (int i = 0; i < NUM_POSITIONS; i++) {
109 COS_30[i] = (float) Math.cos(angle);
110 SIN_30[i] = (float) Math.sin(angle);
111 angle += increment;
112 }
113 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700114
Alan Viverette2b4dc112015-10-02 15:29:43 -0400115 private final FloatProperty<RadialTimePickerView> HOURS_TO_MINUTES =
116 new FloatProperty<RadialTimePickerView>("hoursToMinutes") {
117 @Override
118 public Float get(RadialTimePickerView radialTimePickerView) {
119 return radialTimePickerView.mHoursToMinutes;
120 }
121
122 @Override
123 public void setValue(RadialTimePickerView object, float value) {
124 object.mHoursToMinutes = value;
125 object.invalidate();
126 }
127 };
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700128
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700129 private final String[] mHours12Texts = new String[12];
130 private final String[] mOuterHours24Texts = new String[12];
131 private final String[] mInnerHours24Texts = new String[12];
132 private final String[] mMinutesTexts = new String[12];
133
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700134 private final Paint[] mPaint = new Paint[2];
135 private final Paint mPaintCenter = new Paint();
Alan Viverette2b4dc112015-10-02 15:29:43 -0400136 private final Paint[] mPaintSelector = new Paint[3];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700137 private final Paint mPaintBackground = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700138
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700139 private final Typeface mTypeface;
140
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800141 private final ColorStateList[] mTextColor = new ColorStateList[3];
142 private final int[] mTextSize = new int[3];
143 private final int[] mTextInset = new int[3];
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700144
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800145 private final float[][] mOuterTextX = new float[2][12];
146 private final float[][] mOuterTextY = new float[2][12];
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700147
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800148 private final float[] mInnerTextX = new float[12];
149 private final float[] mInnerTextY = new float[12];
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700150
Alan Viverette88e51032015-04-06 14:38:37 -0700151 private final int[] mSelectionDegrees = new int[2];
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700152
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700153 private final RadialPickerTouchHelper mTouchHelper;
154
Alan Viverettef2525f62015-03-24 18:03:38 -0700155 private final Path mSelectorPath = new Path();
156
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700157 private boolean mIs24HourMode;
158 private boolean mShowHours;
Alan Viveretted735c9b2014-09-15 18:54:10 -0700159
Alan Viverette2b4dc112015-10-02 15:29:43 -0400160 private ObjectAnimator mHoursToMinutesAnimator;
161 private float mHoursToMinutes;
162
Alan Viveretted735c9b2014-09-15 18:54:10 -0700163 /**
164 * When in 24-hour mode, indicates that the current hour is between
165 * 1 and 12 (inclusive).
166 */
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700167 private boolean mIsOnInnerCircle;
168
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800169 private int mSelectorRadius;
Alan Viverette62c79e92015-02-26 09:47:10 -0800170 private int mSelectorStroke;
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800171 private int mSelectorDotRadius;
172 private int mCenterDotRadius;
173
Alan Viverette2b4dc112015-10-02 15:29:43 -0400174 private int mSelectorColor;
175 private int mSelectorDotColor;
176
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800177 private int mXCenter;
178 private int mYCenter;
179 private int mCircleRadius;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700180
Alan Viverette88e51032015-04-06 14:38:37 -0700181 private int mMinDistForInnerNumber;
182 private int mMaxDistForOuterNumber;
183 private int mHalfwayDist;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700184
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700185 private String[] mOuterTextHours;
186 private String[] mInnerTextHours;
Alan Viverette88e51032015-04-06 14:38:37 -0700187 private String[] mMinutesText;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700188
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700189 private int mAmOrPm;
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800190
191 private float mDisabledAlpha;
Alan Viverette51344782014-07-16 17:39:27 -0700192
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700193 private OnValueSelectedListener mListener;
194
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700195 private boolean mInputEnabled = true;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700196
Alan Viverette66a85622016-08-04 13:24:14 -0400197 interface OnValueSelectedListener {
198 /**
199 * Called when the selected value at a given picker index has changed.
200 *
201 * @param pickerType the type of value that has changed, one of:
202 * <ul>
203 * <li>{@link #MINUTES}
204 * <li>{@link #HOURS}
205 * </ul>
206 * @param newValue the new value as minute in hour (0-59) or hour in
207 * day (0-23)
208 * @param autoAdvance when the picker type is {@link #HOURS},
209 * {@code true} to switch to the {@link #MINUTES}
210 * picker or {@code false} to stay on the current
211 * picker. No effect when picker type is
212 * {@link #MINUTES}.
213 */
214 void onValueSelected(@PickerType int pickerType, int newValue, boolean autoAdvance);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700215 }
216
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700217 /**
218 * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
219 * selectable area to each of the 12 visible values, such that the ratio of space apportioned
220 * to a visible value : space apportioned to a non-visible value will be 14 : 4.
221 * E.g. the output of 30 degrees should have a higher range of input associated with it than
222 * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
223 * circle (5 on the minutes, 1 or 13 on the hours).
224 */
225 private static void preparePrefer30sMap() {
226 // We'll split up the visible output and the non-visible output such that each visible
227 // output will correspond to a range of 14 associated input degrees, and each non-visible
228 // output will correspond to a range of 4 associate input degrees, so visible numbers
229 // are more than 3 times easier to get than non-visible numbers:
230 // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
231 //
232 // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
233 // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
234 // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
235 // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
236 // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
237 // ability to aggressively prefer the visible values by a factor of more than 3:1, which
238 // greatly contributes to the selectability of these values.
239
240 // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
241 int snappedOutputDegrees = 0;
242 // Count of how many inputs we've designated to the specified output.
243 int count = 1;
244 // How many input we expect for a specified output. This will be 14 for output divisible
245 // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
246 // the caller can decide which they need.
247 int expectedCount = 8;
248 // Iterate through the input.
249 for (int degrees = 0; degrees < 361; degrees++) {
250 // Save the input-output mapping.
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800251 SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700252 // If this is the last input for the specified output, calculate the next output and
253 // the next expected count.
254 if (count == expectedCount) {
255 snappedOutputDegrees += 6;
256 if (snappedOutputDegrees == 360) {
257 expectedCount = 7;
258 } else if (snappedOutputDegrees % 30 == 0) {
259 expectedCount = 14;
260 } else {
261 expectedCount = 4;
262 }
263 count = 1;
264 } else {
265 count++;
266 }
267 }
268 }
269
270 /**
271 * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
272 * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
273 * weighted heavier than the degrees corresponding to non-visible numbers.
274 * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
275 * mapping.
276 */
277 private static int snapPrefer30s(int degrees) {
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800278 if (SNAP_PREFER_30S_MAP == null) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700279 return -1;
280 }
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800281 return SNAP_PREFER_30S_MAP[degrees];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700282 }
283
284 /**
285 * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
286 * multiples of 30), where the input will be "snapped" to the closest visible degrees.
287 * @param degrees The input degrees
288 * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
289 * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
290 * strictly lower, and 0 to snap to the closer one.
291 * @return output degrees, will be a multiple of 30
292 */
293 private static int snapOnly30s(int degrees, int forceHigherOrLower) {
294 final int stepSize = DEGREES_FOR_ONE_HOUR;
295 int floor = (degrees / stepSize) * stepSize;
296 final int ceiling = floor + stepSize;
297 if (forceHigherOrLower == 1) {
298 degrees = ceiling;
299 } else if (forceHigherOrLower == -1) {
300 if (degrees == floor) {
301 floor -= stepSize;
302 }
303 degrees = floor;
304 } else {
305 if ((degrees - floor) < (ceiling - degrees)) {
306 degrees = floor;
307 } else {
308 degrees = ceiling;
309 }
310 }
311 return degrees;
312 }
313
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700314 @SuppressWarnings("unused")
315 public RadialTimePickerView(Context context) {
316 this(context, null);
317 }
318
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700319 public RadialTimePickerView(Context context, AttributeSet attrs) {
320 this(context, attrs, R.attr.timePickerStyle);
321 }
322
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700323 public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr) {
324 this(context, attrs, defStyleAttr, 0);
325 }
326
327 public RadialTimePickerView(
328 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700329 super(context, attrs);
330
Alan Viverette2b4dc112015-10-02 15:29:43 -0400331 applyAttributes(attrs, defStyleAttr, defStyleRes);
332
Alan Viverette51344782014-07-16 17:39:27 -0700333 // Pull disabled alpha from theme.
334 final TypedValue outValue = new TypedValue();
335 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800336 mDisabledAlpha = outValue.getFloat();
Alan Viverette51344782014-07-16 17:39:27 -0700337
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700338 mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
339
340 mPaint[HOURS] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700341 mPaint[HOURS].setAntiAlias(true);
342 mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
343
344 mPaint[MINUTES] = new Paint();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700345 mPaint[MINUTES].setAntiAlias(true);
346 mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
347
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700348 mPaintCenter.setAntiAlias(true);
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800349
Alan Viverette2b4dc112015-10-02 15:29:43 -0400350 mPaintSelector[SELECTOR_CIRCLE] = new Paint();
351 mPaintSelector[SELECTOR_CIRCLE].setAntiAlias(true);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700352
Alan Viverette2b4dc112015-10-02 15:29:43 -0400353 mPaintSelector[SELECTOR_DOT] = new Paint();
354 mPaintSelector[SELECTOR_DOT].setAntiAlias(true);
Alan Viverettef2525f62015-03-24 18:03:38 -0700355
Alan Viverette2b4dc112015-10-02 15:29:43 -0400356 mPaintSelector[SELECTOR_LINE] = new Paint();
357 mPaintSelector[SELECTOR_LINE].setAntiAlias(true);
358 mPaintSelector[SELECTOR_LINE].setStrokeWidth(2);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700359
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700360 mPaintBackground.setAntiAlias(true);
361
Alan Viverette2b4dc112015-10-02 15:29:43 -0400362 final Resources res = getResources();
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800363 mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius);
Alan Viverette62c79e92015-02-26 09:47:10 -0800364 mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800365 mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius);
366 mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius);
367
368 mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
369 mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
370 mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner);
371
372 mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
373 mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
374 mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700375
376 mShowHours = true;
Alan Viverette2b4dc112015-10-02 15:29:43 -0400377 mHoursToMinutes = HOURS;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700378 mIs24HourMode = false;
379 mAmOrPm = AM;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700380
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700381 // Set up accessibility components.
382 mTouchHelper = new RadialPickerTouchHelper();
383 setAccessibilityDelegate(mTouchHelper);
384
385 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
386 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
387 }
388
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700389 initHoursAndMinutesText();
390 initData();
391
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700392 // Initial values
393 final Calendar calendar = Calendar.getInstance(Locale.getDefault());
394 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
395 final int currentMinute = calendar.get(Calendar.MINUTE);
396
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700397 setCurrentHourInternal(currentHour, false, false);
398 setCurrentMinuteInternal(currentMinute, false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700399
400 setHapticFeedbackEnabled(true);
401 }
402
Alan Viverette2b4dc112015-10-02 15:29:43 -0400403 void applyAttributes(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
404 final Context context = getContext();
405 final TypedArray a = getContext().obtainStyledAttributes(attrs,
406 R.styleable.TimePicker, defStyleAttr, defStyleRes);
Aurimas Liutikasab324cf2019-02-07 16:46:38 -0800407 saveAttributeDataForStyleable(context, R.styleable.TimePicker,
408 attrs, a, defStyleAttr, defStyleRes);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400409
410 final ColorStateList numbersTextColor = a.getColorStateList(
411 R.styleable.TimePicker_numbersTextColor);
412 final ColorStateList numbersInnerTextColor = a.getColorStateList(
413 R.styleable.TimePicker_numbersInnerTextColor);
414 mTextColor[HOURS] = numbersTextColor == null ?
415 ColorStateList.valueOf(MISSING_COLOR) : numbersTextColor;
416 mTextColor[HOURS_INNER] = numbersInnerTextColor == null ?
417 ColorStateList.valueOf(MISSING_COLOR) : numbersInnerTextColor;
418 mTextColor[MINUTES] = mTextColor[HOURS];
419
420 // Set up various colors derived from the selector "activated" state.
421 final ColorStateList selectorColors = a.getColorStateList(
422 R.styleable.TimePicker_numbersSelectorColor);
423 final int selectorActivatedColor;
424 if (selectorColors != null) {
425 final int[] stateSetEnabledActivated = StateSet.get(
426 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
427 selectorActivatedColor = selectorColors.getColorForState(
428 stateSetEnabledActivated, 0);
429 } else {
430 selectorActivatedColor = MISSING_COLOR;
431 }
432
433 mPaintCenter.setColor(selectorActivatedColor);
434
435 final int[] stateSetActivated = StateSet.get(
436 StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
437
438 mSelectorColor = selectorActivatedColor;
439 mSelectorDotColor = mTextColor[HOURS].getColorForState(stateSetActivated, 0);
440
441 mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor,
442 context.getColor(R.color.timepicker_default_numbers_background_color_material)));
443
444 a.recycle();
445 }
446
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700447 public void initialize(int hour, int minute, boolean is24HourMode) {
Alan Viverette448ff712014-11-18 18:28:04 -0800448 if (mIs24HourMode != is24HourMode) {
449 mIs24HourMode = is24HourMode;
450 initData();
451 }
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700452
453 setCurrentHourInternal(hour, false, false);
454 setCurrentMinuteInternal(minute, false);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700455 }
456
457 public void setCurrentItemShowing(int item, boolean animate) {
458 switch (item){
459 case HOURS:
460 showHours(animate);
461 break;
462 case MINUTES:
463 showMinutes(animate);
464 break;
465 default:
466 Log.e(TAG, "ClockView does not support showing item " + item);
467 }
468 }
469
470 public int getCurrentItemShowing() {
471 return mShowHours ? HOURS : MINUTES;
472 }
473
474 public void setOnValueSelectedListener(OnValueSelectedListener listener) {
475 mListener = listener;
476 }
477
Alan Viveretted735c9b2014-09-15 18:54:10 -0700478 /**
479 * Sets the current hour in 24-hour time.
480 *
481 * @param hour the current hour between 0 and 23 (inclusive)
482 */
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700483 public void setCurrentHour(int hour) {
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700484 setCurrentHourInternal(hour, true, false);
485 }
486
487 /**
488 * Sets the current hour.
489 *
490 * @param hour The current hour
491 * @param callback Whether the value listener should be invoked
492 * @param autoAdvance Whether the listener should auto-advance to the next
493 * selection mode, e.g. hour to minutes
494 */
495 private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700496 final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
497 mSelectionDegrees[HOURS] = degrees;
Alan Viveretted735c9b2014-09-15 18:54:10 -0700498
499 // 0 is 12 AM (midnight) and 12 is 12 PM (noon).
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700500 final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
Alan Viverette88e51032015-04-06 14:38:37 -0700501 final boolean isOnInnerCircle = getInnerCircleForHour(hour);
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700502 if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) {
503 mAmOrPm = amOrPm;
504 mIsOnInnerCircle = isOnInnerCircle;
Alan Viveretted735c9b2014-09-15 18:54:10 -0700505
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700506 initData();
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700507 mTouchHelper.invalidateRoot();
508 }
509
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700510 invalidate();
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700511
512 if (callback && mListener != null) {
513 mListener.onValueSelected(HOURS, hour, autoAdvance);
514 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700515 }
516
Alan Viveretted735c9b2014-09-15 18:54:10 -0700517 /**
518 * Returns the current hour in 24-hour time.
519 *
520 * @return the current hour between 0 and 23 (inclusive)
521 */
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700522 public int getCurrentHour() {
Alan Viverette88e51032015-04-06 14:38:37 -0700523 return getHourForDegrees(mSelectionDegrees[HOURS], mIsOnInnerCircle);
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700524 }
525
526 private int getHourForDegrees(int degrees, boolean innerCircle) {
527 int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700528 if (mIs24HourMode) {
Alan Viveretted735c9b2014-09-15 18:54:10 -0700529 // Convert the 12-hour value into 24-hour time based on where the
530 // selector is positioned.
Alan Viverette88e51032015-04-06 14:38:37 -0700531 if (!innerCircle && hour == 0) {
532 // Outer circle is 1 through 12.
Alan Viveretted735c9b2014-09-15 18:54:10 -0700533 hour = 12;
Alan Viverette88e51032015-04-06 14:38:37 -0700534 } else if (innerCircle && hour != 0) {
535 // Inner circle is 13 through 23 and 0.
Alan Viveretted735c9b2014-09-15 18:54:10 -0700536 hour += 12;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700537 }
Alan Viveretted735c9b2014-09-15 18:54:10 -0700538 } else if (mAmOrPm == PM) {
539 hour += 12;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700540 }
Alan Viveretted735c9b2014-09-15 18:54:10 -0700541 return hour;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700542 }
543
Alan Viverette88e51032015-04-06 14:38:37 -0700544 /**
545 * @param hour the hour in 24-hour time or 12-hour time
546 */
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700547 private int getDegreesForHour(int hour) {
548 // Convert to be 0-11.
549 if (mIs24HourMode) {
550 if (hour >= 12) {
551 hour -= 12;
552 }
553 } else if (hour == 12) {
554 hour = 0;
555 }
556 return hour * DEGREES_FOR_ONE_HOUR;
557 }
558
Alan Viverette88e51032015-04-06 14:38:37 -0700559 /**
560 * @param hour the hour in 24-hour time or 12-hour time
561 */
562 private boolean getInnerCircleForHour(int hour) {
563 return mIs24HourMode && (hour == 0 || hour > 12);
564 }
565
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700566 public void setCurrentMinute(int minute) {
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700567 setCurrentMinuteInternal(minute, true);
568 }
569
570 private void setCurrentMinuteInternal(int minute, boolean callback) {
Alan Viverettefc76a642015-04-07 15:37:30 -0700571 mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE;
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700572
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700573 invalidate();
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700574
575 if (callback && mListener != null) {
576 mListener.onValueSelected(MINUTES, minute, false);
577 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700578 }
579
580 // Returns minutes in 0-59 range
581 public int getCurrentMinute() {
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700582 return getMinuteForDegrees(mSelectionDegrees[MINUTES]);
583 }
584
585 private int getMinuteForDegrees(int degrees) {
586 return degrees / DEGREES_FOR_ONE_MINUTE;
587 }
588
589 private int getDegreesForMinute(int minute) {
590 return minute * DEGREES_FOR_ONE_MINUTE;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700591 }
592
Alan Viverette30b57b62016-04-19 09:29:20 -0400593 /**
594 * Sets whether the picker is showing AM or PM hours. Has no effect when
595 * in 24-hour mode.
596 *
597 * @param amOrPm {@link #AM} or {@link #PM}
598 * @return {@code true} if the value changed from what was previously set,
599 * or {@code false} otherwise
600 */
601 public boolean setAmOrPm(int amOrPm) {
602 if (mAmOrPm == amOrPm || mIs24HourMode) {
603 return false;
604 }
605
606 mAmOrPm = amOrPm;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700607 invalidate();
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700608 mTouchHelper.invalidateRoot();
Alan Viverette30b57b62016-04-19 09:29:20 -0400609 return true;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700610 }
611
612 public int getAmOrPm() {
613 return mAmOrPm;
614 }
615
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700616 public void showHours(boolean animate) {
Alan Viverette2b4dc112015-10-02 15:29:43 -0400617 showPicker(true, animate);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700618 }
619
620 public void showMinutes(boolean animate) {
Alan Viverette2b4dc112015-10-02 15:29:43 -0400621 showPicker(false, animate);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700622 }
623
624 private void initHoursAndMinutesText() {
625 // Initialize the hours and minutes numbers.
626 for (int i = 0; i < 12; i++) {
627 mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
Alan Viverettef2525f62015-03-24 18:03:38 -0700628 mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
629 mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700630 mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
631 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700632 }
633
634 private void initData() {
635 if (mIs24HourMode) {
636 mOuterTextHours = mOuterHours24Texts;
637 mInnerTextHours = mInnerHours24Texts;
638 } else {
639 mOuterTextHours = mHours12Texts;
Alan Viverettef2525f62015-03-24 18:03:38 -0700640 mInnerTextHours = mHours12Texts;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700641 }
642
Alan Viverette88e51032015-04-06 14:38:37 -0700643 mMinutesText = mMinutesTexts;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700644 }
645
646 @Override
647 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800648 if (!changed) {
649 return;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700650 }
651
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800652 mXCenter = getWidth() / 2;
653 mYCenter = getHeight() / 2;
654 mCircleRadius = Math.min(mXCenter, mYCenter);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700655
Alan Viverette88e51032015-04-06 14:38:37 -0700656 mMinDistForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius;
657 mMaxDistForOuterNumber = mCircleRadius - mTextInset[HOURS] + mSelectorRadius;
658 mHalfwayDist = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2;
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800659
660 calculatePositionsHours();
661 calculatePositionsMinutes();
662
Alan Viveretteffb46bf2014-10-24 12:06:11 -0700663 mTouchHelper.invalidateRoot();
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700664 }
665
666 @Override
667 public void onDraw(Canvas canvas) {
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800668 final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700669
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700670 drawCircleBackground(canvas);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400671
672 final Path selectorPath = mSelectorPath;
673 drawSelector(canvas, selectorPath);
674 drawHours(canvas, selectorPath, alphaMod);
675 drawMinutes(canvas, selectorPath, alphaMod);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800676 drawCenter(canvas, alphaMod);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700677 }
678
Alan Viverette2b4dc112015-10-02 15:29:43 -0400679 private void showPicker(boolean hours, boolean animate) {
680 if (mShowHours == hours) {
681 return;
682 }
683
684 mShowHours = hours;
685
686 if (animate) {
687 animatePicker(hours, ANIM_DURATION_NORMAL);
Alan Viverette32f7dab2016-05-06 15:31:23 -0400688 } else {
689 // If we have a pending or running animator, cancel it.
690 if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
691 mHoursToMinutesAnimator.cancel();
692 mHoursToMinutesAnimator = null;
693 }
694 mHoursToMinutes = hours ? 0.0f : 1.0f;
Alan Viverette2b4dc112015-10-02 15:29:43 -0400695 }
696
697 initData();
698 invalidate();
699 mTouchHelper.invalidateRoot();
700 }
701
702 private void animatePicker(boolean hoursToMinutes, long duration) {
703 final float target = hoursToMinutes ? HOURS : MINUTES;
704 if (mHoursToMinutes == target) {
705 // If we have a pending or running animator, cancel it.
706 if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
707 mHoursToMinutesAnimator.cancel();
708 mHoursToMinutesAnimator = null;
709 }
710
711 // We're already showing the correct picker.
712 return;
713 }
714
715 mHoursToMinutesAnimator = ObjectAnimator.ofFloat(this, HOURS_TO_MINUTES, target);
716 mHoursToMinutesAnimator.setAutoCancel(true);
717 mHoursToMinutesAnimator.setDuration(duration);
718 mHoursToMinutesAnimator.start();
719 }
720
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700721 private void drawCircleBackground(Canvas canvas) {
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800722 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700723 }
724
Alan Viverette2b4dc112015-10-02 15:29:43 -0400725 private void drawHours(Canvas canvas, Path selectorPath, float alphaMod) {
726 final int hoursAlpha = (int) (255f * (1f - mHoursToMinutes) * alphaMod + 0.5f);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800727 if (hoursAlpha > 0) {
Alan Viverette2b4dc112015-10-02 15:29:43 -0400728 // Exclude the selector region, then draw inner/outer hours with no
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800729 // activated states.
730 canvas.save(Canvas.CLIP_SAVE_FLAG);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400731 canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
732 drawHoursClipped(canvas, hoursAlpha, false);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800733 canvas.restore();
734
735 // Intersect the selector region, then draw minutes with only
736 // activated states.
737 canvas.save(Canvas.CLIP_SAVE_FLAG);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400738 canvas.clipPath(selectorPath, Region.Op.INTERSECT);
739 drawHoursClipped(canvas, hoursAlpha, true);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800740 canvas.restore();
741 }
742 }
743
Alan Viverette2b4dc112015-10-02 15:29:43 -0400744 private void drawHoursClipped(Canvas canvas, int hoursAlpha, boolean showActivated) {
745 // Draw outer hours.
746 drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS], mOuterTextHours,
747 mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], hoursAlpha,
748 showActivated && !mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
749
750 // Draw inner hours (13-00) for 24-hour time.
751 if (mIs24HourMode && mInnerTextHours != null) {
752 drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER],
753 mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha,
754 showActivated && mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
755 }
756 }
757
758 private void drawMinutes(Canvas canvas, Path selectorPath, float alphaMod) {
759 final int minutesAlpha = (int) (255f * mHoursToMinutes * alphaMod + 0.5f);
760 if (minutesAlpha > 0) {
761 // Exclude the selector region, then draw minutes with no
762 // activated states.
763 canvas.save(Canvas.CLIP_SAVE_FLAG);
764 canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
765 drawMinutesClipped(canvas, minutesAlpha, false);
766 canvas.restore();
767
768 // Intersect the selector region, then draw minutes with only
769 // activated states.
770 canvas.save(Canvas.CLIP_SAVE_FLAG);
771 canvas.clipPath(selectorPath, Region.Op.INTERSECT);
772 drawMinutesClipped(canvas, minutesAlpha, true);
773 canvas.restore();
774 }
775 }
776
777 private void drawMinutesClipped(Canvas canvas, int minutesAlpha, boolean showActivated) {
778 drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], mMinutesText,
779 mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], minutesAlpha,
780 showActivated, mSelectionDegrees[MINUTES], showActivated);
781 }
782
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800783 private void drawCenter(Canvas canvas, float alphaMod) {
784 mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f));
785 canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700786 }
787
Alan Viveretteeb1d3792014-06-03 18:36:03 -0700788 private int getMultipliedAlpha(int argb, int alpha) {
789 return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5);
790 }
791
Alan Viverette2b4dc112015-10-02 15:29:43 -0400792 private void drawSelector(Canvas canvas, Path selectorPath) {
793 // Determine the current length, angle, and dot scaling factor.
794 final int hoursIndex = mIsOnInnerCircle ? HOURS_INNER : HOURS;
795 final int hoursInset = mTextInset[hoursIndex];
796 final int hoursAngleDeg = mSelectionDegrees[hoursIndex % 2];
797 final float hoursDotScale = mSelectionDegrees[hoursIndex % 2] % 30 != 0 ? 1 : 0;
798
799 final int minutesIndex = MINUTES;
800 final int minutesInset = mTextInset[minutesIndex];
801 final int minutesAngleDeg = mSelectionDegrees[minutesIndex];
802 final float minutesDotScale = mSelectionDegrees[minutesIndex] % 30 != 0 ? 1 : 0;
Alan Viverettef2525f62015-03-24 18:03:38 -0700803
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700804 // Calculate the current radius at which to place the selection circle.
Alan Viverettef2525f62015-03-24 18:03:38 -0700805 final int selRadius = mSelectorRadius;
Alan Viverette2b4dc112015-10-02 15:29:43 -0400806 final float selLength =
807 mCircleRadius - MathUtils.lerp(hoursInset, minutesInset, mHoursToMinutes);
808 final double selAngleRad =
809 Math.toRadians(MathUtils.lerpDeg(hoursAngleDeg, minutesAngleDeg, mHoursToMinutes));
Alan Viverettef2525f62015-03-24 18:03:38 -0700810 final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad);
811 final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700812
Alan Viverettef2525f62015-03-24 18:03:38 -0700813 // Draw the selection circle.
Alan Viverette2b4dc112015-10-02 15:29:43 -0400814 final Paint paint = mPaintSelector[SELECTOR_CIRCLE];
815 paint.setColor(mSelectorColor);
Alan Viverettef2525f62015-03-24 18:03:38 -0700816 canvas.drawCircle(selCenterX, selCenterY, selRadius, paint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700817
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800818 // If needed, set up the clip path for later.
819 if (selectorPath != null) {
Alan Viverettef2525f62015-03-24 18:03:38 -0700820 selectorPath.reset();
821 selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW);
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800822 }
823
Alan Viverettef2525f62015-03-24 18:03:38 -0700824 // Draw the dot if we're between two items.
Alan Viverette2b4dc112015-10-02 15:29:43 -0400825 final float dotScale = MathUtils.lerp(hoursDotScale, minutesDotScale, mHoursToMinutes);
826 if (dotScale > 0) {
827 final Paint dotPaint = mPaintSelector[SELECTOR_DOT];
Alan Viverette88e51032015-04-06 14:38:37 -0700828 dotPaint.setColor(mSelectorDotColor);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400829 canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius * dotScale, dotPaint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700830 }
831
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800832 // Shorten the line to only go from the edge of the center dot to the
833 // edge of the selection circle.
Alan Viverettef2525f62015-03-24 18:03:38 -0700834 final double sin = Math.sin(selAngleRad);
835 final double cos = Math.cos(selAngleRad);
Alan Viverette2b4dc112015-10-02 15:29:43 -0400836 final float lineLength = selLength - selRadius;
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800837 final int centerX = mXCenter + (int) (mCenterDotRadius * sin);
838 final int centerY = mYCenter - (int) (mCenterDotRadius * cos);
Alan Viverettef2525f62015-03-24 18:03:38 -0700839 final float linePointX = centerX + (int) (lineLength * sin);
840 final float linePointY = centerY - (int) (lineLength * cos);
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800841
Alan Viverettef2525f62015-03-24 18:03:38 -0700842 // Draw the line.
Alan Viverette2b4dc112015-10-02 15:29:43 -0400843 final Paint linePaint = mPaintSelector[SELECTOR_LINE];
844 linePaint.setColor(mSelectorColor);
Alan Viverettef2525f62015-03-24 18:03:38 -0700845 linePaint.setStrokeWidth(mSelectorStroke);
846 canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700847 }
848
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800849 private void calculatePositionsHours() {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700850 // Calculate the text positions
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800851 final float numbersRadius = mCircleRadius - mTextInset[HOURS];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700852
853 // Calculate the positions for the 12 numbers in the main circle.
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800854 calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
855 mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700856
857 // If we have an inner circle, calculate those positions too.
858 if (mIs24HourMode) {
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800859 final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER];
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800860 calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
861 mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700862 }
863 }
864
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800865 private void calculatePositionsMinutes() {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700866 // Calculate the text positions
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800867 final float numbersRadius = mCircleRadius - mTextInset[MINUTES];
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700868
869 // Calculate the positions for the 12 numbers in the main circle.
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800870 calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
871 mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700872 }
873
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700874 /**
875 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
876 * drawn at based on the specified circle radius. Place the values in the textGridHeights and
877 * textGridWidths parameters.
878 */
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800879 private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter,
880 float textSize, float[] x, float[] y) {
881 // Adjust yCenter to account for the text's baseline.
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700882 paint.setTextSize(textSize);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700883 yCenter -= (paint.descent() + paint.ascent()) / 2;
884
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800885 for (int i = 0; i < NUM_POSITIONS; i++) {
886 x[i] = xCenter - radius * COS_30[i];
887 y[i] = yCenter - radius * SIN_30[i];
888 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700889 }
890
891 /**
892 * Draw the 12 text values at the positions specified by the textGrid parameters.
893 */
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800894 private void drawTextElements(Canvas canvas, float textSize, Typeface typeface,
895 ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint,
896 int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700897 paint.setTextSize(textSize);
898 paint.setTypeface(typeface);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700899
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800900 // The activated index can touch a range of elements.
901 final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS);
902 final int activatedFloor = (int) activatedIndex;
903 final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700904
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800905 for (int i = 0; i < 12; i++) {
906 final boolean activated = (activatedFloor == i || activatedCeil == i);
907 if (activatedOnly && !activated) {
908 continue;
909 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700910
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800911 final int stateMask = StateSet.VIEW_STATE_ENABLED
912 | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0);
Alan Viveretteadbc95f2015-02-20 10:51:33 -0800913 final int color = textColor.getColorForState(StateSet.get(stateMask), 0);
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800914 paint.setColor(color);
915 paint.setAlpha(getMultipliedAlpha(color, alpha));
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700916
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800917 canvas.drawText(texts[i], textX[i], textY[i], paint);
918 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700919 }
920
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800921 private int getDegreesFromXY(float x, float y, boolean constrainOutside) {
Alan Viverette88e51032015-04-06 14:38:37 -0700922 // Ensure the point is inside the touchable area.
923 final int innerBound;
924 final int outerBound;
925 if (mIs24HourMode && mShowHours) {
926 innerBound = mMinDistForInnerNumber;
927 outerBound = mMaxDistForOuterNumber;
928 } else {
929 final int index = mShowHours ? HOURS : MINUTES;
930 final int center = mCircleRadius - mTextInset[index];
931 innerBound = center - mSelectorRadius;
932 outerBound = center + mSelectorRadius;
933 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700934
Alan Viverette88e51032015-04-06 14:38:37 -0700935 final double dX = x - mXCenter;
936 final double dY = y - mYCenter;
937 final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
938 if (distFromCenter < innerBound || constrainOutside && distFromCenter > outerBound) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700939 return -1;
940 }
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800941
Alan Viverette88e51032015-04-06 14:38:37 -0700942 // Convert to degrees.
943 final int degrees = (int) (Math.toDegrees(Math.atan2(dY, dX) + Math.PI / 2) + 0.5);
944 if (degrees < 0) {
945 return degrees + 360;
946 } else {
947 return degrees;
948 }
949 }
950
951 private boolean getInnerCircleFromXY(float x, float y) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700952 if (mIs24HourMode && mShowHours) {
Alan Viverette88e51032015-04-06 14:38:37 -0700953 final double dX = x - mXCenter;
954 final double dY = y - mYCenter;
955 final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
956 return distFromCenter <= mHalfwayDist;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700957 }
Alan Viverette88e51032015-04-06 14:38:37 -0700958 return false;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700959 }
960
Alan Viverette002f9182014-12-01 16:13:32 -0800961 boolean mChangedDuringTouch = false;
962
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700963 @Override
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800964 public boolean onTouchEvent(MotionEvent event) {
Alan Viverette002f9182014-12-01 16:13:32 -0800965 if (!mInputEnabled) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700966 return true;
967 }
968
Alan Viverette002f9182014-12-01 16:13:32 -0800969 final int action = event.getActionMasked();
970 if (action == MotionEvent.ACTION_MOVE
971 || action == MotionEvent.ACTION_UP
972 || action == MotionEvent.ACTION_DOWN) {
973 boolean forceSelection = false;
974 boolean autoAdvance = false;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700975
Alan Viverette002f9182014-12-01 16:13:32 -0800976 if (action == MotionEvent.ACTION_DOWN) {
977 // This is a new event stream, reset whether the value changed.
978 mChangedDuringTouch = false;
979 } else if (action == MotionEvent.ACTION_UP) {
980 autoAdvance = true;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700981
Alan Viverette002f9182014-12-01 16:13:32 -0800982 // If we saw a down/up pair without the value changing, assume
983 // this is a single-tap selection and force a change.
984 if (!mChangedDuringTouch) {
985 forceSelection = true;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700986 }
Alan Viverette002f9182014-12-01 16:13:32 -0800987 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700988
Alan Viverette002f9182014-12-01 16:13:32 -0800989 mChangedDuringTouch |= handleTouchInput(
990 event.getX(), event.getY(), forceSelection, autoAdvance);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -0700991 }
Alan Viverette002f9182014-12-01 16:13:32 -0800992
993 return true;
994 }
995
996 private boolean handleTouchInput(
997 float x, float y, boolean forceSelection, boolean autoAdvance) {
Alan Viverette88e51032015-04-06 14:38:37 -0700998 final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
Alan Viveretteec9fe1a2015-01-14 10:46:48 -0800999 final int degrees = getDegreesFromXY(x, y, false);
Alan Viverette002f9182014-12-01 16:13:32 -08001000 if (degrees == -1) {
1001 return false;
1002 }
1003
Alan Viverette2b4dc112015-10-02 15:29:43 -04001004 // Ensure we're showing the correct picker.
1005 animatePicker(mShowHours, ANIM_DURATION_TOUCH);
1006
Alan Viverette66a85622016-08-04 13:24:14 -04001007 final @PickerType int type;
Alan Viverette11a68e12014-12-08 15:54:38 -08001008 final int newValue;
1009 final boolean valueChanged;
Alan Viverette002f9182014-12-01 16:13:32 -08001010
1011 if (mShowHours) {
1012 final int snapDegrees = snapOnly30s(degrees, 0) % 360;
Alan Viverette88e51032015-04-06 14:38:37 -07001013 valueChanged = mIsOnInnerCircle != isOnInnerCircle
1014 || mSelectionDegrees[HOURS] != snapDegrees;
1015 mIsOnInnerCircle = isOnInnerCircle;
1016 mSelectionDegrees[HOURS] = snapDegrees;
Alan Viverette11a68e12014-12-08 15:54:38 -08001017 type = HOURS;
1018 newValue = getCurrentHour();
Alan Viverette002f9182014-12-01 16:13:32 -08001019 } else {
1020 final int snapDegrees = snapPrefer30s(degrees) % 360;
Alan Viverette88e51032015-04-06 14:38:37 -07001021 valueChanged = mSelectionDegrees[MINUTES] != snapDegrees;
1022 mSelectionDegrees[MINUTES] = snapDegrees;
Alan Viverette11a68e12014-12-08 15:54:38 -08001023 type = MINUTES;
1024 newValue = getCurrentMinute();
Alan Viverette002f9182014-12-01 16:13:32 -08001025 }
1026
Alan Viverette11a68e12014-12-08 15:54:38 -08001027 if (valueChanged || forceSelection || autoAdvance) {
1028 // Fire the listener even if we just need to auto-advance.
Alan Viverette002f9182014-12-01 16:13:32 -08001029 if (mListener != null) {
1030 mListener.onValueSelected(type, newValue, autoAdvance);
1031 }
Alan Viverette11a68e12014-12-08 15:54:38 -08001032
1033 // Only provide feedback if the value actually changed.
1034 if (valueChanged || forceSelection) {
1035 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
1036 invalidate();
1037 }
Alan Viverette002f9182014-12-01 16:13:32 -08001038 return true;
1039 }
1040
1041 return false;
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001042 }
1043
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001044 @Override
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001045 public boolean dispatchHoverEvent(MotionEvent event) {
1046 // First right-of-refusal goes the touch exploration helper.
1047 if (mTouchHelper.dispatchHoverEvent(event)) {
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001048 return true;
1049 }
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001050 return super.dispatchHoverEvent(event);
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001051 }
1052
1053 public void setInputEnabled(boolean inputEnabled) {
1054 mInputEnabled = inputEnabled;
1055 invalidate();
1056 }
Alan Viveretteeb1d3792014-06-03 18:36:03 -07001057
Vladislav Kaznacheev47f333a2016-09-21 11:37:08 -07001058 @Override
1059 public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
1060 if (!isEnabled()) {
1061 return null;
1062 }
1063 final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
1064 if (degrees != -1) {
1065 return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
1066 }
1067 return super.onResolvePointerIcon(event, pointerIndex);
1068 }
1069
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001070 private class RadialPickerTouchHelper extends ExploreByTouchHelper {
1071 private final Rect mTempRect = new Rect();
1072
1073 private final int TYPE_HOUR = 1;
1074 private final int TYPE_MINUTE = 2;
1075
1076 private final int SHIFT_TYPE = 0;
1077 private final int MASK_TYPE = 0xF;
1078
1079 private final int SHIFT_VALUE = 8;
1080 private final int MASK_VALUE = 0xFF;
1081
1082 /** Increment in which virtual views are exposed for minutes. */
1083 private final int MINUTE_INCREMENT = 5;
1084
1085 public RadialPickerTouchHelper() {
1086 super(RadialTimePickerView.this);
1087 }
1088
1089 @Override
1090 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
1091 super.onInitializeAccessibilityNodeInfo(host, info);
1092
1093 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
1094 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
1095 }
1096
1097 @Override
1098 public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
1099 if (super.performAccessibilityAction(host, action, arguments)) {
1100 return true;
1101 }
1102
1103 switch (action) {
1104 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1105 adjustPicker(1);
1106 return true;
1107 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1108 adjustPicker(-1);
1109 return true;
1110 }
1111
1112 return false;
1113 }
1114
1115 private void adjustPicker(int step) {
1116 final int stepSize;
Alan Viverette4a6bd692015-03-23 10:34:23 -07001117 final int initialStep;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001118 final int maxValue;
1119 final int minValue;
1120 if (mShowHours) {
Alan Viverette4a6bd692015-03-23 10:34:23 -07001121 stepSize = 1;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001122
Alan Viverette4a6bd692015-03-23 10:34:23 -07001123 final int currentHour24 = getCurrentHour();
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001124 if (mIs24HourMode) {
Alan Viverette4a6bd692015-03-23 10:34:23 -07001125 initialStep = currentHour24;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001126 minValue = 0;
Alan Viverette4a6bd692015-03-23 10:34:23 -07001127 maxValue = 23;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001128 } else {
Alan Viverette4a6bd692015-03-23 10:34:23 -07001129 initialStep = hour24To12(currentHour24);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001130 minValue = 1;
Alan Viverette4a6bd692015-03-23 10:34:23 -07001131 maxValue = 12;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001132 }
1133 } else {
Alan Viverette4a6bd692015-03-23 10:34:23 -07001134 stepSize = 5;
1135 initialStep = getCurrentMinute() / stepSize;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001136 minValue = 0;
Alan Viverette4a6bd692015-03-23 10:34:23 -07001137 maxValue = 55;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001138 }
1139
Alan Viverette4a6bd692015-03-23 10:34:23 -07001140 final int nextValue = (initialStep + step) * stepSize;
1141 final int clampedValue = MathUtils.constrain(nextValue, minValue, maxValue);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001142 if (mShowHours) {
1143 setCurrentHour(clampedValue);
1144 } else {
1145 setCurrentMinute(clampedValue);
1146 }
1147 }
1148
1149 @Override
1150 protected int getVirtualViewAt(float x, float y) {
1151 final int id;
Alan Viveretted8c2af52015-01-27 13:35:14 -08001152 final int degrees = getDegreesFromXY(x, y, true);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001153 if (degrees != -1) {
1154 final int snapDegrees = snapOnly30s(degrees, 0) % 360;
1155 if (mShowHours) {
Alan Viverette88e51032015-04-06 14:38:37 -07001156 final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
Alan Viverette5efe0d12015-01-26 15:34:56 -08001157 final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle);
1158 final int hour = mIs24HourMode ? hour24 : hour24To12(hour24);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001159 id = makeId(TYPE_HOUR, hour);
1160 } else {
1161 final int current = getCurrentMinute();
1162 final int touched = getMinuteForDegrees(degrees);
1163 final int snapped = getMinuteForDegrees(snapDegrees);
1164
1165 // If the touched minute is closer to the current minute
1166 // than it is to the snapped minute, return current.
Alan Viverettefc76a642015-04-07 15:37:30 -07001167 final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE);
1168 final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001169 final int minute;
Alan Viverette88e51032015-04-06 14:38:37 -07001170 if (currentOffset < snappedOffset) {
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001171 minute = current;
1172 } else {
1173 minute = snapped;
1174 }
1175 id = makeId(TYPE_MINUTE, minute);
1176 }
1177 } else {
1178 id = INVALID_ID;
1179 }
1180
1181 return id;
1182 }
1183
Alan Viverette88e51032015-04-06 14:38:37 -07001184 /**
1185 * Returns the difference in degrees between two values along a circle.
1186 *
1187 * @param first value in the range [0,max]
1188 * @param second value in the range [0,max]
1189 * @param max the maximum value along the circle
1190 * @return the difference in between the two values
1191 */
1192 private int getCircularDiff(int first, int second, int max) {
1193 final int diff = Math.abs(first - second);
1194 final int midpoint = max / 2;
1195 return (diff > midpoint) ? (max - diff) : diff;
1196 }
1197
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001198 @Override
1199 protected void getVisibleVirtualViews(IntArray virtualViewIds) {
1200 if (mShowHours) {
1201 final int min = mIs24HourMode ? 0 : 1;
1202 final int max = mIs24HourMode ? 23 : 12;
1203 for (int i = min; i <= max ; i++) {
1204 virtualViewIds.add(makeId(TYPE_HOUR, i));
1205 }
1206 } else {
1207 final int current = getCurrentMinute();
Alan Viverettefc76a642015-04-07 15:37:30 -07001208 for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) {
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001209 virtualViewIds.add(makeId(TYPE_MINUTE, i));
1210
1211 // If the current minute falls between two increments,
1212 // insert an extra node for it.
1213 if (current > i && current < i + MINUTE_INCREMENT) {
1214 virtualViewIds.add(makeId(TYPE_MINUTE, current));
1215 }
1216 }
1217 }
1218 }
1219
1220 @Override
1221 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
1222 event.setClassName(getClass().getName());
1223
1224 final int type = getTypeFromId(virtualViewId);
1225 final int value = getValueFromId(virtualViewId);
1226 final CharSequence description = getVirtualViewDescription(type, value);
1227 event.setContentDescription(description);
1228 }
1229
1230 @Override
1231 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
1232 node.setClassName(getClass().getName());
1233 node.addAction(AccessibilityAction.ACTION_CLICK);
1234
1235 final int type = getTypeFromId(virtualViewId);
1236 final int value = getValueFromId(virtualViewId);
1237 final CharSequence description = getVirtualViewDescription(type, value);
1238 node.setContentDescription(description);
1239
1240 getBoundsForVirtualView(virtualViewId, mTempRect);
1241 node.setBoundsInParent(mTempRect);
1242
1243 final boolean selected = isVirtualViewSelected(type, value);
1244 node.setSelected(selected);
Alan Viverette3fc00e312014-12-10 09:46:49 -08001245
1246 final int nextId = getVirtualViewIdAfter(type, value);
1247 if (nextId != INVALID_ID) {
1248 node.setTraversalBefore(RadialTimePickerView.this, nextId);
1249 }
1250 }
1251
1252 private int getVirtualViewIdAfter(int type, int value) {
1253 if (type == TYPE_HOUR) {
1254 final int nextValue = value + 1;
1255 final int max = mIs24HourMode ? 23 : 12;
1256 if (nextValue <= max) {
1257 return makeId(type, nextValue);
1258 }
1259 } else if (type == TYPE_MINUTE) {
1260 final int current = getCurrentMinute();
1261 final int snapValue = value - (value % MINUTE_INCREMENT);
1262 final int nextValue = snapValue + MINUTE_INCREMENT;
1263 if (value < current && nextValue > current) {
1264 // The current value is between two snap values.
1265 return makeId(type, current);
Alan Viverettefc76a642015-04-07 15:37:30 -07001266 } else if (nextValue < MINUTES_IN_CIRCLE) {
Alan Viverette3fc00e312014-12-10 09:46:49 -08001267 return makeId(type, nextValue);
1268 }
1269 }
1270 return INVALID_ID;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001271 }
1272
1273 @Override
1274 protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
1275 Bundle arguments) {
1276 if (action == AccessibilityNodeInfo.ACTION_CLICK) {
1277 final int type = getTypeFromId(virtualViewId);
1278 final int value = getValueFromId(virtualViewId);
1279 if (type == TYPE_HOUR) {
1280 final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm);
1281 setCurrentHour(hour);
1282 return true;
1283 } else if (type == TYPE_MINUTE) {
1284 setCurrentMinute(value);
1285 return true;
1286 }
1287 }
1288 return false;
1289 }
1290
1291 private int hour12To24(int hour12, int amOrPm) {
1292 int hour24 = hour12;
1293 if (hour12 == 12) {
1294 if (amOrPm == AM) {
1295 hour24 = 0;
1296 }
1297 } else if (amOrPm == PM) {
1298 hour24 += 12;
1299 }
1300 return hour24;
1301 }
1302
Alan Viverette5efe0d12015-01-26 15:34:56 -08001303 private int hour24To12(int hour24) {
1304 if (hour24 == 0) {
1305 return 12;
1306 } else if (hour24 > 12) {
1307 return hour24 - 12;
1308 } else {
1309 return hour24;
1310 }
1311 }
1312
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001313 private void getBoundsForVirtualView(int virtualViewId, Rect bounds) {
1314 final float radius;
1315 final int type = getTypeFromId(virtualViewId);
1316 final int value = getValueFromId(virtualViewId);
1317 final float centerRadius;
1318 final float degrees;
1319 if (type == TYPE_HOUR) {
Alan Viverette88e51032015-04-06 14:38:37 -07001320 final boolean innerCircle = getInnerCircleForHour(value);
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001321 if (innerCircle) {
Alan Viveretteadbc95f2015-02-20 10:51:33 -08001322 centerRadius = mCircleRadius - mTextInset[HOURS_INNER];
1323 radius = mSelectorRadius;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001324 } else {
Alan Viveretteadbc95f2015-02-20 10:51:33 -08001325 centerRadius = mCircleRadius - mTextInset[HOURS];
1326 radius = mSelectorRadius;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001327 }
1328
1329 degrees = getDegreesForHour(value);
1330 } else if (type == TYPE_MINUTE) {
Alan Viveretteadbc95f2015-02-20 10:51:33 -08001331 centerRadius = mCircleRadius - mTextInset[MINUTES];
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001332 degrees = getDegreesForMinute(value);
Alan Viveretteadbc95f2015-02-20 10:51:33 -08001333 radius = mSelectorRadius;
Alan Viveretteffb46bf2014-10-24 12:06:11 -07001334 } else {
1335 // This should never happen.
1336 centerRadius = 0;
1337 degrees = 0;
1338 radius = 0;
1339 }
1340
1341 final double radians = Math.toRadians(degrees);
1342 final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians);
1343 final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians);
1344
1345 bounds.set((int) (xCenter - radius), (int) (yCenter - radius),
1346 (int) (xCenter + radius), (int) (yCenter + radius));
1347 }
1348
1349 private CharSequence getVirtualViewDescription(int type, int value) {
1350 final CharSequence description;
1351 if (type == TYPE_HOUR || type == TYPE_MINUTE) {
1352 description = Integer.toString(value);
1353 } else {
1354 description = null;
1355 }
1356 return description;
1357 }
1358
1359 private boolean isVirtualViewSelected(int type, int value) {
1360 final boolean selected;
1361 if (type == TYPE_HOUR) {
1362 selected = getCurrentHour() == value;
1363 } else if (type == TYPE_MINUTE) {
1364 selected = getCurrentMinute() == value;
1365 } else {
1366 selected = false;
1367 }
1368 return selected;
1369 }
1370
1371 private int makeId(int type, int value) {
1372 return type << SHIFT_TYPE | value << SHIFT_VALUE;
1373 }
1374
1375 private int getTypeFromId(int id) {
1376 return id >>> SHIFT_TYPE & MASK_TYPE;
1377 }
1378
1379 private int getValueFromId(int id) {
1380 return id >>> SHIFT_VALUE & MASK_VALUE;
1381 }
1382 }
Fabrice Di Meglioeeff63a2013-08-05 12:07:24 -07001383}