blob: f249e539ccd62ac515d013eb26f6f9add44e85d4 [file] [log] [blame]
cretin450328b872015-01-15 16:04:44 -08001/*
2 * Copyright (C) 2008 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
Maarten Derkse901e0c2017-05-22 11:15:44 +020017package com.fairphone.setupwizard.ui;
cretin450328b872015-01-15 16:04:44 -080018
Maarten Derkse901e0c2017-05-22 11:15:44 +020019import com.fairphone.setupwizard.R;
cretin450328b872015-01-15 16:04:44 -080020
21import android.annotation.Widget;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Color;
27import android.graphics.Paint;
28import android.graphics.Paint.Align;
29import android.graphics.Rect;
30import android.graphics.drawable.Drawable;
31import android.os.Bundle;
32import android.text.InputFilter;
33import android.text.InputType;
34import android.text.Spanned;
35import android.text.TextUtils;
36import android.text.method.NumberKeyListener;
37import android.util.AttributeSet;
38import android.util.SparseArray;
39import android.util.TypedValue;
40import android.view.KeyEvent;
41import android.view.LayoutInflater;
42import android.view.MotionEvent;
43import android.view.VelocityTracker;
44import android.view.View;
45import android.view.ViewConfiguration;
46import android.view.accessibility.AccessibilityEvent;
47import android.view.accessibility.AccessibilityManager;
48import android.view.accessibility.AccessibilityNodeInfo;
49import android.view.accessibility.AccessibilityNodeProvider;
50import android.view.animation.DecelerateInterpolator;
51import android.view.inputmethod.EditorInfo;
52import android.view.inputmethod.InputMethodManager;
53import android.widget.Button;
54import android.widget.EditText;
55import android.widget.ImageButton;
56import android.widget.LinearLayout;
57import android.widget.Scroller;
58import android.widget.TextView;
59
60import java.util.ArrayList;
61import java.util.Collections;
62import java.util.List;
63import java.util.Locale;
64
65import libcore.icu.LocaleData;
66
67/**
68 * A widget that enables the user to select a number form a predefined range.
69 * There are two flavors of this widget and which one is presented to the user
70 * depends on the current theme.
71 * <ul>
72 * <li>
73 * If the current theme is derived from {@link android.R.style#Theme} the widget
74 * presents the current value as an editable input field with an increment button
75 * above and a decrement button below. Long pressing the buttons allows for a quick
76 * change of the current value. Tapping on the input field allows to type in
77 * a desired value.
78 * </li>
79 * <li>
80 * If the current theme is derived from {@link android.R.style#Theme_Holo} or
81 * {@link android.R.style#Theme_Holo_Light} the widget presents the current
82 * value as an editable input field with a lesser value above and a greater
83 * value below. Tapping on the lesser or greater value selects it by animating
84 * the number axis up or down to make the chosen value current. Flinging up
85 * or down allows for multiple increments or decrements of the current value.
86 * Long pressing on the lesser and greater values also allows for a quick change
87 * of the current value. Tapping on the current value allows to type in a
88 * desired value.
89 * </li>
90 * </ul>
91 * <p>
92 * For an example of using this widget, see {@link android.widget.TimePicker}.
93 * </p>
94 */
95@Widget
96public class LocalePicker extends LinearLayout {
97
98 /**
99 * The number of items show in the selector wheel.
100 */
cretin45b64d5e72015-10-27 12:44:11 -0700101 private static int sSelectorWheelItemCount = 3;
cretin450328b872015-01-15 16:04:44 -0800102
103 /**
104 * The default update interval during long press.
105 */
106 private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
107
108 /**
109 * The index of the middle selector item.
110 */
cretin45b64d5e72015-10-27 12:44:11 -0700111 private static int sSelectorMiddleItemIndex = sSelectorWheelItemCount / 2;
cretin450328b872015-01-15 16:04:44 -0800112
113 /**
114 * The coefficient by which to adjust (divide) the max fling velocity.
115 */
116 private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
117
118 /**
119 * The the duration for adjusting the selector wheel.
120 */
121 private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
122
123 /**
124 * The duration of scrolling while snapping to a given position.
125 */
126 private static final int SNAP_SCROLL_DURATION = 300;
127
128 /**
129 * The strength of fading in the top and bottom while drawing the selector.
130 */
131 private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
132
133 /**
134 * The default unscaled height of the selection divider.
135 */
136 private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 1;
137
138 /**
139 * The default unscaled distance between the selection dividers.
140 */
141 private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
142
143 /**
144 * The resource id for the default layout.
145 */
146 private static final int DEFAULT_LAYOUT_RESOURCE_ID =
cretin45b64d5e72015-10-27 12:44:11 -0700147 R.layout.locale_picker;
cretin450328b872015-01-15 16:04:44 -0800148
149 /**
150 * Constant for unspecified size.
151 */
152 private static final int SIZE_UNSPECIFIED = -1;
153
154 /**
155 * Use a custom NumberPicker formatting callback to use two-digit minutes
156 * strings like "01". Keeping a static formatter etc. is the most efficient
157 * way to do this; it avoids creating temporary objects on every call to
158 * format().
159 */
160 private static class TwoDigitFormatter implements LocalePicker.Formatter {
161 final StringBuilder mBuilder = new StringBuilder();
162
163 char mZeroDigit;
164 java.util.Formatter mFmt;
165
166 final Object[] mArgs = new Object[1];
167
168 TwoDigitFormatter() {
169 final Locale locale = Locale.getDefault();
170 init(locale);
171 }
172
173 private void init(Locale locale) {
174 mFmt = createFormatter(locale);
175 mZeroDigit = getZeroDigit(locale);
176 }
177
178 public String format(int value) {
179 final Locale currentLocale = Locale.getDefault();
180 if (mZeroDigit != getZeroDigit(currentLocale)) {
181 init(currentLocale);
182 }
183 mArgs[0] = value;
184 mBuilder.delete(0, mBuilder.length());
185 mFmt.format("%02d", mArgs);
186 return mFmt.toString();
187 }
188
189 private static char getZeroDigit(Locale locale) {
190 return LocaleData.get(locale).zeroDigit;
191 }
192
193 private java.util.Formatter createFormatter(Locale locale) {
194 return new java.util.Formatter(mBuilder, locale);
195 }
196 }
197
198 private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter();
199
200 /**
201 * @hide
202 */
203 public static final Formatter getTwoDigitFormatter() {
204 return sTwoDigitFormatter;
205 }
206
207 /**
208 * The increment button.
209 */
210 private final ImageButton mIncrementButton;
211
212 /**
213 * The decrement button.
214 */
215 private final ImageButton mDecrementButton;
216
217 /**
218 * The text for showing the current value.
219 */
220 private final EditText mInputText;
221
222 /**
223 * The distance between the two selection dividers.
224 */
225 private final int mSelectionDividersDistance;
226
227 /**
228 * The min height of this widget.
229 */
230 private final int mMinHeight;
231
232 /**
233 * The max height of this widget.
234 */
235 private final int mMaxHeight;
236
237 /**
238 * The max width of this widget.
239 */
240 private final int mMinWidth;
241
242 /**
243 * The max width of this widget.
244 */
245 private int mMaxWidth;
246
247 /**
248 * Flag whether to compute the max width.
249 */
250 private final boolean mComputeMaxWidth;
251
252 /**
253 * The height of the text.
254 */
255 private final int mTextSize;
256
257 /**
258 * The height of the gap between text elements if the selector wheel.
259 */
260 private int mSelectorTextGapHeight;
261
262 /**
263 * The values to be displayed instead the indices.
264 */
265 private String[] mDisplayedValues;
266
267 /**
268 * Lower value of the range of numbers allowed for the NumberPicker
269 */
270 private int mMinValue;
271
272 /**
273 * Upper value of the range of numbers allowed for the NumberPicker
274 */
275 private int mMaxValue;
276
277 /**
278 * Current value of this NumberPicker
279 */
280 private int mValue;
281
282 /**
283 * Listener to be notified upon current value change.
284 */
285 private OnValueChangeListener mOnValueChangeListener;
286
287 /**
288 * Listener to be notified upon scroll state change.
289 */
290 private OnScrollListener mOnScrollListener;
291
292 /**
293 * Formatter for for displaying the current value.
294 */
295 private Formatter mFormatter;
296
297 /**
298 * The speed for updating the value form long press.
299 */
300 private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
301
302 /**
303 * Cache for the string representation of selector indices.
304 */
305 private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
306
307 /**
308 * The selector indices whose value are show by the selector.
309 */
310 private final int[] mSelectorIndices;
311
312 /**
313 * The {@link android.graphics.Paint} for drawing the selector.
314 */
315 private final Paint mSelectorWheelPaint;
316
317 /**
318 * The {@link android.graphics.drawable.Drawable} for pressed virtual (increment/decrement) buttons.
319 */
320 private final Drawable mVirtualButtonPressedDrawable;
321
322 /**
323 * The height of a selector element (text + gap).
324 */
325 private int mSelectorElementHeight;
326
327 /**
328 * The initial offset of the scroll selector.
329 */
330 private int mInitialScrollOffset = Integer.MIN_VALUE;
331
332 /**
333 * The current offset of the scroll selector.
334 */
335 private int mCurrentScrollOffset;
336
337 /**
338 * The {@link android.widget.Scroller} responsible for flinging the selector.
339 */
340 private final Scroller mFlingScroller;
341
342 /**
343 * The {@link android.widget.Scroller} responsible for adjusting the selector.
344 */
345 private final Scroller mAdjustScroller;
346
347 /**
348 * The previous Y coordinate while scrolling the selector.
349 */
350 private int mPreviousScrollerY;
351
352 /**
353 * Handle to the reusable command for setting the input text selection.
354 */
355 private SetSelectionCommand mSetSelectionCommand;
356
357 /**
358 * Handle to the reusable command for changing the current value from long
359 * press by one.
360 */
361 private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
362
363 /**
364 * Command for beginning an edit of the current value via IME on long press.
365 */
366 private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
367
368 /**
369 * The Y position of the last down event.
370 */
371 private float mLastDownEventY;
372
373 /**
374 * The time of the last down event.
375 */
376 private long mLastDownEventTime;
377
378 /**
379 * The Y position of the last down or move event.
380 */
381 private float mLastDownOrMoveEventY;
382
383 /**
384 * Determines speed during touch scrolling.
385 */
386 private VelocityTracker mVelocityTracker;
387
388 /**
389 * @see android.view.ViewConfiguration#getScaledTouchSlop()
390 */
391 private int mTouchSlop;
392
393 /**
394 * @see android.view.ViewConfiguration#getScaledMinimumFlingVelocity()
395 */
396 private int mMinimumFlingVelocity;
397
398 /**
399 * @see android.view.ViewConfiguration#getScaledMaximumFlingVelocity()
400 */
401 private int mMaximumFlingVelocity;
402
403 /**
404 * Flag whether the selector should wrap around.
405 */
406 private boolean mWrapSelectorWheel;
407
408 /**
409 * The back ground color used to optimize scroller fading.
410 */
411 private final int mSolidColor;
412
413 /**
414 * Flag whether this widget has a selector wheel.
415 */
416 private final boolean mHasSelectorWheel;
417
418 /**
419 * Divider for showing item to be selected while scrolling
420 */
421 private final Drawable mSelectionDivider;
422
423 /**
424 * The height of the selection divider.
425 */
426 private final int mSelectionDividerHeight;
427
428 /**
429 * The current scroll state of the number picker.
430 */
431 private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
432
433 /**
434 * Flag whether to ignore move events - we ignore such when we show in IME
435 * to prevent the content from scrolling.
436 */
437 private boolean mIngonreMoveEvents;
438
439 /**
440 * Flag whether to show soft input on tap.
441 */
442 private boolean mShowSoftInputOnTap;
443
444 /**
445 * The top of the top selection divider.
446 */
447 private int mTopSelectionDividerTop;
448
449 /**
450 * The bottom of the bottom selection divider.
451 */
452 private int mBottomSelectionDividerBottom;
453
454 /**
455 * The virtual id of the last hovered child.
456 */
457 private int mLastHoveredChildVirtualViewId;
458
459 /**
460 * Whether the increment virtual button is pressed.
461 */
462 private boolean mIncrementVirtualButtonPressed;
463
464 /**
465 * Whether the decrement virtual button is pressed.
466 */
467 private boolean mDecrementVirtualButtonPressed;
468
469 /**
470 * Provider to report to clients the semantic structure of this widget.
471 */
472 private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
473
474 /**
475 * Helper class for managing pressed state of the virtual buttons.
476 */
477 private final PressedStateHelper mPressedStateHelper;
478
479 /**
480 * The keycode of the last handled DPAD down event.
481 */
482 private int mLastHandledDownDpadKeyCode = -1;
483
484 /**
485 * Interface to listen for changes of the current value.
486 */
487 public interface OnValueChangeListener {
488
489 /**
490 * Called upon a change of the current value.
491 *
492 * @param picker The NumberPicker associated with this listener.
493 * @param oldVal The previous value.
494 * @param newVal The new value.
495 */
496 void onValueChange(LocalePicker picker, int oldVal, int newVal);
497 }
498
499 /**
500 * Interface to listen for the picker scroll state.
501 */
502 public interface OnScrollListener {
503
504 /**
505 * The view is not scrolling.
506 */
507 public static int SCROLL_STATE_IDLE = 0;
508
509 /**
510 * The user is scrolling using touch, and his finger is still on the screen.
511 */
512 public static int SCROLL_STATE_TOUCH_SCROLL = 1;
513
514 /**
515 * The user had previously been scrolling using touch and performed a fling.
516 */
517 public static int SCROLL_STATE_FLING = 2;
518
519 /**
520 * Callback invoked while the number picker scroll state has changed.
521 *
522 * @param view The view whose scroll state is being reported.
523 * @param scrollState The current scroll state. One of
524 * {@link #SCROLL_STATE_IDLE},
525 * {@link #SCROLL_STATE_TOUCH_SCROLL} or
526 * {@link #SCROLL_STATE_IDLE}.
527 */
528 public void onScrollStateChange(LocalePicker view, int scrollState);
529 }
530
531 /**
532 * Interface used to format current value into a string for presentation.
533 */
534 public interface Formatter {
535
536 /**
537 * Formats a string representation of the current value.
538 *
539 * @param value The currently selected value.
540 * @return A formatted string representation.
541 */
542 public String format(int value);
543 }
544
545 /**
546 * Create a new number picker.
547 *
548 * @param context The application environment.
549 */
550 public LocalePicker(Context context) {
551 this(context, null);
552 }
553
554 /**
555 * Create a new number picker.
556 *
557 * @param context The application environment.
558 * @param attrs A collection of attributes.
559 */
560 public LocalePicker(Context context, AttributeSet attrs) {
cretin45b64d5e72015-10-27 12:44:11 -0700561 this(context, attrs, R.attr.localePickerStyle);
cretin450328b872015-01-15 16:04:44 -0800562 }
563
564 /**
565 * Create a new number picker
566 *
567 * @param context the application environment.
568 * @param attrs a collection of attributes.
569 * @param defStyle The default style to apply to this view.
570 */
571 public LocalePicker(Context context, AttributeSet attrs, int defStyle) {
572 super(context, attrs, defStyle);
cretin45b64d5e72015-10-27 12:44:11 -0700573 sSelectorWheelItemCount = context.getResources().getInteger(R.integer.local_picker_items);
574 sSelectorMiddleItemIndex = context.getResources().getInteger(R.integer.local_picker_items)/2;
575 mSelectorIndices= new int[sSelectorWheelItemCount];
cretin450328b872015-01-15 16:04:44 -0800576 // process style attributes
577 TypedArray attributesArray = context.obtainStyledAttributes(
cretin45b64d5e72015-10-27 12:44:11 -0700578 attrs, R.styleable.LocalePicker, defStyle, 0);
cretin450328b872015-01-15 16:04:44 -0800579 final int layoutResId = attributesArray.getResourceId(
cretin45b64d5e72015-10-27 12:44:11 -0700580 R.styleable.LocalePicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID);
cretin450328b872015-01-15 16:04:44 -0800581
cretin45b64d5e72015-10-27 12:44:11 -0700582 mHasSelectorWheel = true;
cretin450328b872015-01-15 16:04:44 -0800583
cretin45b64d5e72015-10-27 12:44:11 -0700584 mSolidColor = attributesArray.getColor(R.styleable.LocalePicker_solidColor, 0);
cretin450328b872015-01-15 16:04:44 -0800585
cretin45b64d5e72015-10-27 12:44:11 -0700586 mSelectionDivider = attributesArray.getDrawable(R.styleable.LocalePicker_selectionDivider);
cretin450328b872015-01-15 16:04:44 -0800587
cretin45b64d5e72015-10-27 12:44:11 -0700588 final int defSelectionDividerHeight = (int) TypedValue.applyDimension(
589 TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
590 getResources().getDisplayMetrics());
591 mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
592 R.styleable.LocalePicker_selectionDividerHeight, defSelectionDividerHeight);
cretin450328b872015-01-15 16:04:44 -0800593
594 final int defSelectionDividerDistance = (int) TypedValue.applyDimension(
595 TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
596 getResources().getDisplayMetrics());
597 mSelectionDividersDistance = attributesArray.getDimensionPixelSize(
cretin45b64d5e72015-10-27 12:44:11 -0700598 R.styleable.LocalePicker_selectionDividersDistance, defSelectionDividerDistance);
cretin450328b872015-01-15 16:04:44 -0800599
600 mMinHeight = attributesArray.getDimensionPixelSize(
cretin45b64d5e72015-10-27 12:44:11 -0700601 R.styleable.LocalePicker_internalMinHeight, SIZE_UNSPECIFIED);
cretin450328b872015-01-15 16:04:44 -0800602
603 mMaxHeight = attributesArray.getDimensionPixelSize(
cretin45b64d5e72015-10-27 12:44:11 -0700604 R.styleable.LocalePicker_internalMaxHeight, SIZE_UNSPECIFIED);
cretin450328b872015-01-15 16:04:44 -0800605 if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
606 && mMinHeight > mMaxHeight) {
607 throw new IllegalArgumentException("minHeight > maxHeight");
608 }
609
610 mMinWidth = attributesArray.getDimensionPixelSize(
cretin45b64d5e72015-10-27 12:44:11 -0700611 R.styleable.LocalePicker_internalMinWidth, SIZE_UNSPECIFIED);
cretin450328b872015-01-15 16:04:44 -0800612
613 mMaxWidth = attributesArray.getDimensionPixelSize(
cretin45b64d5e72015-10-27 12:44:11 -0700614 R.styleable.LocalePicker_internalMaxWidth, SIZE_UNSPECIFIED);
cretin450328b872015-01-15 16:04:44 -0800615 if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
616 && mMinWidth > mMaxWidth) {
617 throw new IllegalArgumentException("minWidth > maxWidth");
618 }
619
620 mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
621
622 mVirtualButtonPressedDrawable = attributesArray.getDrawable(
cretin45b64d5e72015-10-27 12:44:11 -0700623 R.styleable.LocalePicker_virtualButtonPressedDrawable);
cretin450328b872015-01-15 16:04:44 -0800624
625 attributesArray.recycle();
626
627 mPressedStateHelper = new PressedStateHelper();
628
629 // By default Linearlayout that we extend is not drawn. This is
630 // its draw() method is not called but dispatchDraw() is called
631 // directly (see ViewGroup.drawChild()). However, this class uses
632 // the fading edge effect implemented by View and we need our
633 // draw() method to be called. Therefore, we declare we will draw.
634 setWillNotDraw(!mHasSelectorWheel);
635
636 LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
637 Context.LAYOUT_INFLATER_SERVICE);
638 inflater.inflate(layoutResId, this, true);
639
640 OnClickListener onClickListener = new OnClickListener() {
641 public void onClick(View v) {
642 hideSoftInput();
643 mInputText.clearFocus();
cretin45b64d5e72015-10-27 12:44:11 -0700644 if (v.getId() == R.id.lp__increment) {
cretin450328b872015-01-15 16:04:44 -0800645 changeValueByOne(true);
646 } else {
647 changeValueByOne(false);
648 }
649 }
650 };
651
652 OnLongClickListener onLongClickListener = new OnLongClickListener() {
653 public boolean onLongClick(View v) {
654 hideSoftInput();
655 mInputText.clearFocus();
cretin45b64d5e72015-10-27 12:44:11 -0700656 if (v.getId() == R.id.lp__increment) {
cretin450328b872015-01-15 16:04:44 -0800657 postChangeCurrentByOneFromLongPress(true, 0);
658 } else {
659 postChangeCurrentByOneFromLongPress(false, 0);
660 }
661 return true;
662 }
663 };
664
665 // increment button
666 if (!mHasSelectorWheel) {
cretin45b64d5e72015-10-27 12:44:11 -0700667 mIncrementButton = (ImageButton) findViewById(R.id.lp__increment);
cretin450328b872015-01-15 16:04:44 -0800668 mIncrementButton.setOnClickListener(onClickListener);
669 mIncrementButton.setOnLongClickListener(onLongClickListener);
670 } else {
671 mIncrementButton = null;
672 }
673
674 // decrement button
675 if (!mHasSelectorWheel) {
cretin45b64d5e72015-10-27 12:44:11 -0700676 mDecrementButton = (ImageButton) findViewById(R.id.lp__decrement);
cretin450328b872015-01-15 16:04:44 -0800677 mDecrementButton.setOnClickListener(onClickListener);
678 mDecrementButton.setOnLongClickListener(onLongClickListener);
679 } else {
680 mDecrementButton = null;
681 }
682
683 // input text
cretin45b64d5e72015-10-27 12:44:11 -0700684 mInputText = (EditText) findViewById(R.id.localepicker_input);
cretin450328b872015-01-15 16:04:44 -0800685 mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
686 public void onFocusChange(View v, boolean hasFocus) {
687 if (hasFocus) {
688 mInputText.selectAll();
689 } else {
690 mInputText.setSelection(0, 0);
691 validateInputTextView(v);
692 }
693 }
694 });
695 mInputText.setFilters(new InputFilter[] {
696 new InputTextFilter()
697 });
698
699 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
700 mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
701
702 // initialize constants
703 ViewConfiguration configuration = ViewConfiguration.get(context);
704 mTouchSlop = configuration.getScaledTouchSlop();
705 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
706 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
707 / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
708 mTextSize = (int) mInputText.getTextSize();
709
710 // create the selector wheel paint
711 Paint paint = new Paint();
712 paint.setAntiAlias(true);
713 paint.setTextAlign(Align.CENTER);
714 paint.setTextSize(mTextSize);
715 paint.setTypeface(mInputText.getTypeface());
716 ColorStateList colors = mInputText.getTextColors();
717 int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
718 paint.setColor(color);
719 mSelectorWheelPaint = paint;
720
721 // create the fling and adjust scrollers
722 mFlingScroller = new Scroller(getContext(), null, true);
723 mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
724
725 updateInputTextView();
726
727 // If not explicitly specified this view is important for accessibility.
728 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
729 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
730 }
731 }
732
733 @Override
734 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
735 if (!mHasSelectorWheel) {
736 super.onLayout(changed, left, top, right, bottom);
737 return;
738 }
739 final int msrdWdth = getMeasuredWidth();
740 final int msrdHght = getMeasuredHeight();
741
742 // Input text centered horizontally.
743 final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
744 final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
745 final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
746 final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
747 final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
748 final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
749 mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
750
751 if (changed) {
752 // need to do all this when we know our size
753 initializeSelectorWheel();
754 initializeFadingEdges();
755 mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
756 - mSelectionDividerHeight;
757 mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
758 + mSelectionDividersDistance;
759 }
760 }
761
762 @Override
763 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
764 if (!mHasSelectorWheel) {
765 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
766 return;
767 }
768 // Try greedily to fit the max width and height.
769 final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
770 final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
771 super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
772 // Flag if we are measured with width or height less than the respective min.
773 final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
774 widthMeasureSpec);
775 final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
776 heightMeasureSpec);
777 setMeasuredDimension(widthSize, heightSize);
778 }
779
780 /**
781 * Move to the final position of a scroller. Ensures to force finish the scroller
782 * and if it is not at its final position a scroll of the selector wheel is
783 * performed to fast forward to the final position.
784 *
785 * @param scroller The scroller to whose final position to get.
786 * @return True of the a move was performed, i.e. the scroller was not in final position.
787 */
788 private boolean moveToFinalScrollerPosition(Scroller scroller) {
789 scroller.forceFinished(true);
790 int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
791 int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
792 int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
793 if (overshootAdjustment != 0) {
794 if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
795 if (overshootAdjustment > 0) {
796 overshootAdjustment -= mSelectorElementHeight;
797 } else {
798 overshootAdjustment += mSelectorElementHeight;
799 }
800 }
801 amountToScroll += overshootAdjustment;
802 scrollBy(0, amountToScroll);
803 return true;
804 }
805 return false;
806 }
807
808 @Override
809 public boolean onInterceptTouchEvent(MotionEvent event) {
810 if (!mHasSelectorWheel || !isEnabled()) {
811 return false;
812 }
813 final int action = event.getActionMasked();
814 switch (action) {
815 case MotionEvent.ACTION_DOWN: {
816 removeAllCallbacks();
817 mInputText.setVisibility(View.INVISIBLE);
818 mLastDownOrMoveEventY = mLastDownEventY = event.getY();
819 mLastDownEventTime = event.getEventTime();
820 mIngonreMoveEvents = false;
821 mShowSoftInputOnTap = false;
822 // Handle pressed state before any state change.
823 if (mLastDownEventY < mTopSelectionDividerTop) {
824 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
825 mPressedStateHelper.buttonPressDelayed(
826 PressedStateHelper.BUTTON_DECREMENT);
827 }
828 } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
829 if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
830 mPressedStateHelper.buttonPressDelayed(
831 PressedStateHelper.BUTTON_INCREMENT);
832 }
833 }
834 // Make sure we support flinging inside scrollables.
835 getParent().requestDisallowInterceptTouchEvent(true);
836 if (!mFlingScroller.isFinished()) {
837 mFlingScroller.forceFinished(true);
838 mAdjustScroller.forceFinished(true);
839 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
840 } else if (!mAdjustScroller.isFinished()) {
841 mFlingScroller.forceFinished(true);
842 mAdjustScroller.forceFinished(true);
843 } else if (mLastDownEventY < mTopSelectionDividerTop) {
844 hideSoftInput();
845 postChangeCurrentByOneFromLongPress(
846 false, ViewConfiguration.getLongPressTimeout());
847 } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
848 hideSoftInput();
849 postChangeCurrentByOneFromLongPress(
850 true, ViewConfiguration.getLongPressTimeout());
851 } else {
852 mShowSoftInputOnTap = true;
853 postBeginSoftInputOnLongPressCommand();
854 }
855 return true;
856 }
857 }
858 return false;
859 }
860
861 @Override
862 public boolean onTouchEvent(MotionEvent event) {
863 if (!isEnabled() || !mHasSelectorWheel) {
864 return false;
865 }
866 if (mVelocityTracker == null) {
867 mVelocityTracker = VelocityTracker.obtain();
868 }
869 mVelocityTracker.addMovement(event);
870 int action = event.getActionMasked();
871 switch (action) {
872 case MotionEvent.ACTION_MOVE: {
873 if (mIngonreMoveEvents) {
874 break;
875 }
876 float currentMoveY = event.getY();
877 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
878 int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
879 if (deltaDownY > mTouchSlop) {
880 removeAllCallbacks();
881 onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
882 }
883 } else {
884 int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
885 scrollBy(0, deltaMoveY);
886 invalidate();
887 }
888 mLastDownOrMoveEventY = currentMoveY;
889 } break;
890 case MotionEvent.ACTION_UP: {
891 removeBeginSoftInputCommand();
892 removeChangeCurrentByOneFromLongPress();
893 mPressedStateHelper.cancel();
894 VelocityTracker velocityTracker = mVelocityTracker;
895 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
896 int initialVelocity = (int) velocityTracker.getYVelocity();
897 if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
898 fling(initialVelocity);
899 onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
900 } else {
901 int eventY = (int) event.getY();
902 int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
903 long deltaTime = event.getEventTime() - mLastDownEventTime;
904 if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
905 if (mShowSoftInputOnTap) {
906 mShowSoftInputOnTap = false;
907 showSoftInput();
908 } else {
909 int selectorIndexOffset = (eventY / mSelectorElementHeight)
cretin45b64d5e72015-10-27 12:44:11 -0700910 - sSelectorMiddleItemIndex;
cretin450328b872015-01-15 16:04:44 -0800911 if (selectorIndexOffset > 0) {
912 changeValueByOne(true);
913 mPressedStateHelper.buttonTapped(
914 PressedStateHelper.BUTTON_INCREMENT);
915 } else if (selectorIndexOffset < 0) {
916 changeValueByOne(false);
917 mPressedStateHelper.buttonTapped(
918 PressedStateHelper.BUTTON_DECREMENT);
919 }
920 }
921 } else {
922 ensureScrollWheelAdjusted();
923 }
924 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
925 }
926 mVelocityTracker.recycle();
927 mVelocityTracker = null;
928 } break;
929 }
930 return true;
931 }
932
933 @Override
934 public boolean dispatchTouchEvent(MotionEvent event) {
935 final int action = event.getActionMasked();
936 switch (action) {
937 case MotionEvent.ACTION_CANCEL:
938 case MotionEvent.ACTION_UP:
939 removeAllCallbacks();
940 break;
941 }
942 return super.dispatchTouchEvent(event);
943 }
944
945 @Override
946 public boolean dispatchKeyEvent(KeyEvent event) {
947 final int keyCode = event.getKeyCode();
948 switch (keyCode) {
949 case KeyEvent.KEYCODE_DPAD_CENTER:
950 case KeyEvent.KEYCODE_ENTER:
951 removeAllCallbacks();
952 break;
953 case KeyEvent.KEYCODE_DPAD_DOWN:
954 case KeyEvent.KEYCODE_DPAD_UP:
955 if (!mHasSelectorWheel) {
956 break;
957 }
958 switch (event.getAction()) {
959 case KeyEvent.ACTION_DOWN:
960 if (mWrapSelectorWheel || (keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
961 ? getValue() < getMaxValue() : getValue() > getMinValue()) {
962 requestFocus();
963 mLastHandledDownDpadKeyCode = keyCode;
964 removeAllCallbacks();
965 if (mFlingScroller.isFinished()) {
966 changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
967 }
968 return true;
969 }
970 break;
971 case KeyEvent.ACTION_UP:
972 if (mLastHandledDownDpadKeyCode == keyCode) {
973 mLastHandledDownDpadKeyCode = -1;
974 return true;
975 }
976 break;
977 }
978 }
979 return super.dispatchKeyEvent(event);
980 }
981
982 @Override
983 public boolean dispatchTrackballEvent(MotionEvent event) {
984 final int action = event.getActionMasked();
985 switch (action) {
986 case MotionEvent.ACTION_CANCEL:
987 case MotionEvent.ACTION_UP:
988 removeAllCallbacks();
989 break;
990 }
991 return super.dispatchTrackballEvent(event);
992 }
993
994 @Override
995 protected boolean dispatchHoverEvent(MotionEvent event) {
996 if (!mHasSelectorWheel) {
997 return super.dispatchHoverEvent(event);
998 }
999 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
1000 final int eventY = (int) event.getY();
1001 final int hoveredVirtualViewId;
1002 if (eventY < mTopSelectionDividerTop) {
1003 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
1004 } else if (eventY > mBottomSelectionDividerBottom) {
1005 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
1006 } else {
1007 hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
1008 }
1009 final int action = event.getActionMasked();
1010 AccessibilityNodeProviderImpl provider =
1011 (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
1012 switch (action) {
1013 case MotionEvent.ACTION_HOVER_ENTER: {
1014 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1015 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1016 mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
1017 provider.performAction(hoveredVirtualViewId,
1018 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
1019 } break;
1020 case MotionEvent.ACTION_HOVER_MOVE: {
1021 if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
1022 && mLastHoveredChildVirtualViewId != View.NO_ID) {
1023 provider.sendAccessibilityEventForVirtualView(
1024 mLastHoveredChildVirtualViewId,
1025 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1026 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1027 AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1028 mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
1029 provider.performAction(hoveredVirtualViewId,
1030 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
1031 }
1032 } break;
1033 case MotionEvent.ACTION_HOVER_EXIT: {
1034 provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
1035 AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1036 mLastHoveredChildVirtualViewId = View.NO_ID;
1037 } break;
1038 }
1039 }
1040 return false;
1041 }
1042
1043 @Override
1044 public void computeScroll() {
1045 Scroller scroller = mFlingScroller;
1046 if (scroller.isFinished()) {
1047 scroller = mAdjustScroller;
1048 if (scroller.isFinished()) {
1049 return;
1050 }
1051 }
1052 scroller.computeScrollOffset();
1053 int currentScrollerY = scroller.getCurrY();
1054 if (mPreviousScrollerY == 0) {
1055 mPreviousScrollerY = scroller.getStartY();
1056 }
1057 scrollBy(0, currentScrollerY - mPreviousScrollerY);
1058 mPreviousScrollerY = currentScrollerY;
1059 if (scroller.isFinished()) {
1060 onScrollerFinished(scroller);
1061 } else {
1062 invalidate();
1063 }
1064 }
1065
1066 @Override
1067 public void setEnabled(boolean enabled) {
1068 super.setEnabled(enabled);
1069 if (!mHasSelectorWheel) {
1070 mIncrementButton.setEnabled(enabled);
1071 }
1072 if (!mHasSelectorWheel) {
1073 mDecrementButton.setEnabled(enabled);
1074 }
1075 mInputText.setEnabled(enabled);
1076 }
1077
1078 @Override
1079 public void scrollBy(int x, int y) {
1080 int[] selectorIndices = mSelectorIndices;
1081 if (!mWrapSelectorWheel && y > 0
cretin45b64d5e72015-10-27 12:44:11 -07001082 && selectorIndices[sSelectorMiddleItemIndex] <= mMinValue) {
cretin450328b872015-01-15 16:04:44 -08001083 mCurrentScrollOffset = mInitialScrollOffset;
1084 return;
1085 }
1086 if (!mWrapSelectorWheel && y < 0
cretin45b64d5e72015-10-27 12:44:11 -07001087 && selectorIndices[sSelectorMiddleItemIndex] >= mMaxValue) {
cretin450328b872015-01-15 16:04:44 -08001088 mCurrentScrollOffset = mInitialScrollOffset;
1089 return;
1090 }
1091 mCurrentScrollOffset += y;
1092 while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
1093 mCurrentScrollOffset -= mSelectorElementHeight;
1094 decrementSelectorIndices(selectorIndices);
cretin45b64d5e72015-10-27 12:44:11 -07001095 setValueInternal(selectorIndices[sSelectorMiddleItemIndex], true);
1096 if (!mWrapSelectorWheel && selectorIndices[sSelectorMiddleItemIndex] <= mMinValue) {
cretin450328b872015-01-15 16:04:44 -08001097 mCurrentScrollOffset = mInitialScrollOffset;
1098 }
1099 }
1100 while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
1101 mCurrentScrollOffset += mSelectorElementHeight;
1102 incrementSelectorIndices(selectorIndices);
cretin45b64d5e72015-10-27 12:44:11 -07001103 setValueInternal(selectorIndices[sSelectorMiddleItemIndex], true);
1104 if (!mWrapSelectorWheel && selectorIndices[sSelectorMiddleItemIndex] >= mMaxValue) {
cretin450328b872015-01-15 16:04:44 -08001105 mCurrentScrollOffset = mInitialScrollOffset;
1106 }
1107 }
1108 }
1109
1110 @Override
1111 public int getSolidColor() {
1112 return mSolidColor;
1113 }
1114
1115 /**
1116 * Sets the listener to be notified on change of the current value.
1117 *
1118 * @param onValueChangedListener The listener.
1119 */
1120 public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
1121 mOnValueChangeListener = onValueChangedListener;
1122 }
1123
1124 /**
1125 * Set listener to be notified for scroll state changes.
1126 *
1127 * @param onScrollListener The listener.
1128 */
1129 public void setOnScrollListener(OnScrollListener onScrollListener) {
1130 mOnScrollListener = onScrollListener;
1131 }
1132
1133 /**
1134 * Set the formatter to be used for formatting the current value.
1135 * <p>
1136 * Note: If you have provided alternative values for the values this
1137 * formatter is never invoked.
1138 * </p>
1139 *
1140 * @param formatter The formatter object. If formatter is <code>null</code>,
1141 * {@link String#valueOf(int)} will be used.
1142 *@see #setDisplayedValues(String[])
1143 */
1144 public void setFormatter(Formatter formatter) {
1145 if (formatter == mFormatter) {
1146 return;
1147 }
1148 mFormatter = formatter;
1149 initializeSelectorWheelIndices();
1150 updateInputTextView();
1151 }
1152
1153 /**
1154 * Set the current value for the number picker.
1155 * <p>
1156 * If the argument is less than the {@link LocalePicker#getMinValue()} and
1157 * {@link LocalePicker#getWrapSelectorWheel()} is <code>false</code> the
1158 * current value is set to the {@link LocalePicker#getMinValue()} value.
1159 * </p>
1160 * <p>
1161 * If the argument is less than the {@link LocalePicker#getMinValue()} and
1162 * {@link LocalePicker#getWrapSelectorWheel()} is <code>true</code> the
1163 * current value is set to the {@link LocalePicker#getMaxValue()} value.
1164 * </p>
1165 * <p>
1166 * If the argument is less than the {@link LocalePicker#getMaxValue()} and
1167 * {@link LocalePicker#getWrapSelectorWheel()} is <code>false</code> the
1168 * current value is set to the {@link LocalePicker#getMaxValue()} value.
1169 * </p>
1170 * <p>
1171 * If the argument is less than the {@link LocalePicker#getMaxValue()} and
1172 * {@link LocalePicker#getWrapSelectorWheel()} is <code>true</code> the
1173 * current value is set to the {@link LocalePicker#getMinValue()} value.
1174 * </p>
1175 *
1176 * @param value The current value.
1177 * @see #setWrapSelectorWheel(boolean)
1178 * @see #setMinValue(int)
1179 * @see #setMaxValue(int)
1180 */
1181 public void setValue(int value) {
1182 setValueInternal(value, false);
1183 }
1184
1185 /**
1186 * Shows the soft input for its input text.
1187 */
1188 private void showSoftInput() {
1189 InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
1190 if (inputMethodManager != null) {
1191 if (mHasSelectorWheel) {
1192 mInputText.setVisibility(View.VISIBLE);
1193 }
1194 mInputText.requestFocus();
1195 inputMethodManager.showSoftInput(mInputText, 0);
1196 }
1197 }
1198
1199 /**
1200 * Hides the soft input if it is active for the input text.
1201 */
1202 private void hideSoftInput() {
1203 InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
1204 if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
1205 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
1206 if (mHasSelectorWheel) {
1207 mInputText.setVisibility(View.INVISIBLE);
1208 }
1209 }
1210 }
1211
1212 /**
1213 * Computes the max width if no such specified as an attribute.
1214 */
1215 private void tryComputeMaxWidth() {
1216 if (!mComputeMaxWidth) {
1217 return;
1218 }
1219 int maxTextWidth = 0;
1220 if (mDisplayedValues == null) {
1221 float maxDigitWidth = 0;
1222 for (int i = 0; i <= 9; i++) {
1223 final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
1224 if (digitWidth > maxDigitWidth) {
1225 maxDigitWidth = digitWidth;
1226 }
1227 }
1228 int numberOfDigits = 0;
1229 int current = mMaxValue;
1230 while (current > 0) {
1231 numberOfDigits++;
1232 current = current / 10;
1233 }
1234 maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
1235 } else {
1236 final int valueCount = mDisplayedValues.length;
1237 for (int i = 0; i < valueCount; i++) {
1238 final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
1239 if (textWidth > maxTextWidth) {
1240 maxTextWidth = (int) textWidth;
1241 }
1242 }
1243 }
1244 maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
1245 if (mMaxWidth != maxTextWidth) {
1246 if (maxTextWidth > mMinWidth) {
1247 mMaxWidth = maxTextWidth;
1248 } else {
1249 mMaxWidth = mMinWidth;
1250 }
1251 invalidate();
1252 }
1253 }
1254
1255 /**
1256 * Gets whether the selector wheel wraps when reaching the min/max value.
1257 *
1258 * @return True if the selector wheel wraps.
1259 *
1260 * @see #getMinValue()
1261 * @see #getMaxValue()
1262 */
1263 public boolean getWrapSelectorWheel() {
1264 return mWrapSelectorWheel;
1265 }
1266
1267 /**
1268 * Sets whether the selector wheel shown during flinging/scrolling should
1269 * wrap around the {@link LocalePicker#getMinValue()} and
1270 * {@link LocalePicker#getMaxValue()} values.
1271 * <p>
1272 * By default if the range (max - min) is more than the number of items shown
1273 * on the selector wheel the selector wheel wrapping is enabled.
1274 * </p>
1275 * <p>
1276 * <strong>Note:</strong> If the number of items, i.e. the range (
1277 * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
1278 * the number of items shown on the selector wheel, the selector wheel will
1279 * not wrap. Hence, in such a case calling this method is a NOP.
1280 * </p>
1281 *
1282 * @param wrapSelectorWheel Whether to wrap.
1283 */
1284 public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
1285 final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
1286 if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) {
1287 mWrapSelectorWheel = wrapSelectorWheel;
1288 }
1289 }
1290
1291 /**
1292 * Sets the speed at which the numbers be incremented and decremented when
1293 * the up and down buttons are long pressed respectively.
1294 * <p>
1295 * The default value is 300 ms.
1296 * </p>
1297 *
1298 * @param intervalMillis The speed (in milliseconds) at which the numbers
1299 * will be incremented and decremented.
1300 */
1301 public void setOnLongPressUpdateInterval(long intervalMillis) {
1302 mLongPressUpdateInterval = intervalMillis;
1303 }
1304
1305 /**
1306 * Returns the value of the picker.
1307 *
1308 * @return The value.
1309 */
1310 public int getValue() {
1311 return mValue;
1312 }
1313
1314 /**
1315 * Returns the min value of the picker.
1316 *
1317 * @return The min value
1318 */
1319 public int getMinValue() {
1320 return mMinValue;
1321 }
1322
1323 /**
1324 * Sets the min value of the picker.
1325 *
1326 * @param minValue The min value inclusive.
1327 *
1328 * <strong>Note:</strong> The length of the displayed values array
1329 * set via {@link #setDisplayedValues(String[])} must be equal to the
1330 * range of selectable numbers which is equal to
1331 * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1332 */
1333 public void setMinValue(int minValue) {
1334 if (mMinValue == minValue) {
1335 return;
1336 }
1337 if (minValue < 0) {
1338 throw new IllegalArgumentException("minValue must be >= 0");
1339 }
1340 mMinValue = minValue;
1341 if (mMinValue > mValue) {
1342 mValue = mMinValue;
1343 }
1344 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1345 setWrapSelectorWheel(wrapSelectorWheel);
1346 initializeSelectorWheelIndices();
1347 updateInputTextView();
1348 tryComputeMaxWidth();
1349 invalidate();
1350 }
1351
1352 /**
1353 * Returns the max value of the picker.
1354 *
1355 * @return The max value.
1356 */
1357 public int getMaxValue() {
1358 return mMaxValue;
1359 }
1360
1361 /**
1362 * Sets the max value of the picker.
1363 *
1364 * @param maxValue The max value inclusive.
1365 *
1366 * <strong>Note:</strong> The length of the displayed values array
1367 * set via {@link #setDisplayedValues(String[])} must be equal to the
1368 * range of selectable numbers which is equal to
1369 * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1370 */
1371 public void setMaxValue(int maxValue) {
1372 if (mMaxValue == maxValue) {
1373 return;
1374 }
1375 if (maxValue < 0) {
1376 throw new IllegalArgumentException("maxValue must be >= 0");
1377 }
1378 mMaxValue = maxValue;
1379 if (mMaxValue < mValue) {
1380 mValue = mMaxValue;
1381 }
1382 boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
1383 setWrapSelectorWheel(wrapSelectorWheel);
1384 initializeSelectorWheelIndices();
1385 updateInputTextView();
1386 tryComputeMaxWidth();
1387 invalidate();
1388 }
1389
1390 /**
1391 * Gets the values to be displayed instead of string values.
1392 *
1393 * @return The displayed values.
1394 */
1395 public String[] getDisplayedValues() {
1396 return mDisplayedValues;
1397 }
1398
1399 /**
1400 * Sets the values to be displayed.
1401 *
1402 * @param displayedValues The displayed values.
1403 *
1404 * <strong>Note:</strong> The length of the displayed values array
1405 * must be equal to the range of selectable numbers which is equal to
1406 * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
1407 */
1408 public void setDisplayedValues(String[] displayedValues) {
1409 if (mDisplayedValues == displayedValues) {
1410 return;
1411 }
1412 mDisplayedValues = displayedValues;
1413 if (mDisplayedValues != null) {
1414 // Allow text entry rather than strictly numeric entry.
1415 mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
1416 | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
1417 } else {
1418 mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
1419 }
1420 updateInputTextView();
1421 initializeSelectorWheelIndices();
1422 tryComputeMaxWidth();
1423 }
1424
1425 @Override
1426 protected float getTopFadingEdgeStrength() {
1427 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1428 }
1429
1430 @Override
1431 protected float getBottomFadingEdgeStrength() {
1432 return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
1433 }
1434
1435 @Override
1436 protected void onDetachedFromWindow() {
1437 removeAllCallbacks();
1438 }
1439
1440 @Override
1441 protected void onDraw(Canvas canvas) {
1442 if (!mHasSelectorWheel) {
1443 super.onDraw(canvas);
1444 return;
1445 }
1446 float x = (mRight - mLeft) / 2;
1447 float y = mCurrentScrollOffset;
1448
1449 // draw the virtual buttons pressed state if needed
1450 if (mVirtualButtonPressedDrawable != null
1451 && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
1452 if (mDecrementVirtualButtonPressed) {
1453 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1454 mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
1455 mVirtualButtonPressedDrawable.draw(canvas);
1456 }
1457 if (mIncrementVirtualButtonPressed) {
1458 mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
1459 mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
1460 mBottom);
1461 mVirtualButtonPressedDrawable.draw(canvas);
1462 }
1463 }
1464
1465 // draw the selector wheel
1466 int[] selectorIndices = mSelectorIndices;
1467 for (int i = 0; i < selectorIndices.length; i++) {
1468 int selectorIndex = selectorIndices[i];
1469 String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
1470 // Do not draw the middle item if input is visible since the input
1471 // is shown only if the wheel is static and it covers the middle
1472 // item. Otherwise, if the user starts editing the text via the
1473 // IME he may see a dimmed version of the old value intermixed
1474 // with the new one.
cretin45b64d5e72015-10-27 12:44:11 -07001475 if (i != sSelectorMiddleItemIndex || mInputText.getVisibility() != VISIBLE) {
cretin450328b872015-01-15 16:04:44 -08001476 canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
1477 }
1478 y += mSelectorElementHeight;
1479 }
1480
1481 // draw the selection dividers
1482 if (mSelectionDivider != null) {
1483 // draw the top divider
1484 int topOfTopDivider = mTopSelectionDividerTop;
1485 int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
1486 mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
1487 mSelectionDivider.draw(canvas);
1488
1489 // draw the bottom divider
1490 int bottomOfBottomDivider = mBottomSelectionDividerBottom;
1491 int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
1492 mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
1493 mSelectionDivider.draw(canvas);
1494 }
1495 }
1496
1497 @Override
1498 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1499 super.onInitializeAccessibilityEvent(event);
1500 event.setClassName(LocalePicker.class.getName());
1501 event.setScrollable(true);
1502 event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
1503 event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
1504 }
1505
1506 @Override
1507 public AccessibilityNodeProvider getAccessibilityNodeProvider() {
1508 if (!mHasSelectorWheel) {
1509 return super.getAccessibilityNodeProvider();
1510 }
1511 if (mAccessibilityNodeProvider == null) {
1512 mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
1513 }
1514 return mAccessibilityNodeProvider;
1515 }
1516
1517 /**
1518 * Makes a measure spec that tries greedily to use the max value.
1519 *
1520 * @param measureSpec The measure spec.
1521 * @param maxSize The max value for the size.
1522 * @return A measure spec greedily imposing the max size.
1523 */
1524 private int makeMeasureSpec(int measureSpec, int maxSize) {
1525 if (maxSize == SIZE_UNSPECIFIED) {
1526 return measureSpec;
1527 }
1528 final int size = MeasureSpec.getSize(measureSpec);
1529 final int mode = MeasureSpec.getMode(measureSpec);
1530 switch (mode) {
1531 case MeasureSpec.EXACTLY:
1532 return measureSpec;
1533 case MeasureSpec.AT_MOST:
1534 return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
1535 case MeasureSpec.UNSPECIFIED:
1536 return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
1537 default:
1538 throw new IllegalArgumentException("Unknown measure mode: " + mode);
1539 }
1540 }
1541
1542 /**
1543 * Utility to reconcile a desired size and state, with constraints imposed
1544 * by a MeasureSpec. Tries to respect the min size, unless a different size
1545 * is imposed by the constraints.
1546 *
1547 * @param minSize The minimal desired size.
1548 * @param measuredSize The currently measured size.
1549 * @param measureSpec The current measure spec.
1550 * @return The resolved size and state.
1551 */
1552 private int resolveSizeAndStateRespectingMinSize(
1553 int minSize, int measuredSize, int measureSpec) {
1554 if (minSize != SIZE_UNSPECIFIED) {
1555 final int desiredWidth = Math.max(minSize, measuredSize);
1556 return resolveSizeAndState(desiredWidth, measureSpec, 0);
1557 } else {
1558 return measuredSize;
1559 }
1560 }
1561
1562 /**
1563 * Resets the selector indices and clear the cached string representation of
1564 * these indices.
1565 */
1566 private void initializeSelectorWheelIndices() {
1567 mSelectorIndexToStringCache.clear();
1568 int[] selectorIndices = mSelectorIndices;
1569 int current = getValue();
1570 for (int i = 0; i < mSelectorIndices.length; i++) {
cretin45b64d5e72015-10-27 12:44:11 -07001571 int selectorIndex = current + (i - sSelectorMiddleItemIndex);
cretin450328b872015-01-15 16:04:44 -08001572 if (mWrapSelectorWheel) {
1573 selectorIndex = getWrappedSelectorIndex(selectorIndex);
1574 }
1575 selectorIndices[i] = selectorIndex;
1576 ensureCachedScrollSelectorValue(selectorIndices[i]);
1577 }
1578 }
1579
1580 /**
1581 * Sets the current value of this NumberPicker.
1582 *
1583 * @param current The new value of the NumberPicker.
1584 * @param notifyChange Whether to notify if the current value changed.
1585 */
1586 private void setValueInternal(int current, boolean notifyChange) {
1587 if (mValue == current) {
1588 return;
1589 }
1590 // Wrap around the values if we go past the start or end
1591 if (mWrapSelectorWheel) {
1592 current = getWrappedSelectorIndex(current);
1593 } else {
1594 current = Math.max(current, mMinValue);
1595 current = Math.min(current, mMaxValue);
1596 }
1597 int previous = mValue;
1598 mValue = current;
1599 updateInputTextView();
1600 if (notifyChange) {
1601 notifyChange(previous, current);
1602 }
1603 initializeSelectorWheelIndices();
1604 invalidate();
1605 }
1606
1607 /**
1608 * Changes the current value by one which is increment or
1609 * decrement based on the passes argument.
1610 * decrement the current value.
1611 *
1612 * @param increment True to increment, false to decrement.
1613 */
1614 private void changeValueByOne(boolean increment) {
1615 if (mHasSelectorWheel) {
1616 mInputText.setVisibility(View.INVISIBLE);
1617 if (!moveToFinalScrollerPosition(mFlingScroller)) {
1618 moveToFinalScrollerPosition(mAdjustScroller);
1619 }
1620 mPreviousScrollerY = 0;
1621 if (increment) {
1622 mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
1623 } else {
1624 mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
1625 }
1626 invalidate();
1627 } else {
1628 if (increment) {
1629 setValueInternal(mValue + 1, true);
1630 } else {
1631 setValueInternal(mValue - 1, true);
1632 }
1633 }
1634 }
1635
1636 private void initializeSelectorWheel() {
1637 initializeSelectorWheelIndices();
1638 int[] selectorIndices = mSelectorIndices;
1639 int totalTextHeight = selectorIndices.length * mTextSize;
1640 float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
1641 float textGapCount = selectorIndices.length;
1642 mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
1643 mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
1644 // Ensure that the middle item is positioned the same as the text in
1645 // mInputText
1646 int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
1647 mInitialScrollOffset = editTextTextPosition
cretin45b64d5e72015-10-27 12:44:11 -07001648 - (mSelectorElementHeight * sSelectorMiddleItemIndex);
cretin450328b872015-01-15 16:04:44 -08001649 mCurrentScrollOffset = mInitialScrollOffset;
1650 updateInputTextView();
1651 }
1652
1653 private void initializeFadingEdges() {
1654 setVerticalFadingEdgeEnabled(true);
1655 setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
1656 }
1657
1658 /**
1659 * Callback invoked upon completion of a given <code>scroller</code>.
1660 */
1661 private void onScrollerFinished(Scroller scroller) {
1662 if (scroller == mFlingScroller) {
1663 if (!ensureScrollWheelAdjusted()) {
1664 updateInputTextView();
1665 }
1666 onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1667 } else {
1668 if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
1669 updateInputTextView();
1670 }
1671 }
1672 }
1673
1674 /**
1675 * Handles transition to a given <code>scrollState</code>
1676 */
1677 private void onScrollStateChange(int scrollState) {
1678 if (mScrollState == scrollState) {
1679 return;
1680 }
1681 mScrollState = scrollState;
1682 if (mOnScrollListener != null) {
1683 mOnScrollListener.onScrollStateChange(this, scrollState);
1684 }
1685 }
1686
1687 /**
1688 * Flings the selector with the given <code>velocityY</code>.
1689 */
1690 private void fling(int velocityY) {
1691 mPreviousScrollerY = 0;
1692
1693 if (velocityY > 0) {
1694 mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1695 } else {
1696 mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
1697 }
1698
1699 invalidate();
1700 }
1701
1702 /**
1703 * @return The wrapped index <code>selectorIndex</code> value.
1704 */
1705 private int getWrappedSelectorIndex(int selectorIndex) {
1706 if (selectorIndex > mMaxValue) {
1707 return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
1708 } else if (selectorIndex < mMinValue) {
1709 return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
1710 }
1711 return selectorIndex;
1712 }
1713
1714 /**
1715 * Increments the <code>selectorIndices</code> whose string representations
1716 * will be displayed in the selector.
1717 */
1718 private void incrementSelectorIndices(int[] selectorIndices) {
1719 for (int i = 0; i < selectorIndices.length - 1; i++) {
1720 selectorIndices[i] = selectorIndices[i + 1];
1721 }
1722 int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
1723 if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
1724 nextScrollSelectorIndex = mMinValue;
1725 }
1726 selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
1727 ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1728 }
1729
1730 /**
1731 * Decrements the <code>selectorIndices</code> whose string representations
1732 * will be displayed in the selector.
1733 */
1734 private void decrementSelectorIndices(int[] selectorIndices) {
1735 for (int i = selectorIndices.length - 1; i > 0; i--) {
1736 selectorIndices[i] = selectorIndices[i - 1];
1737 }
1738 int nextScrollSelectorIndex = selectorIndices[1] - 1;
1739 if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
1740 nextScrollSelectorIndex = mMaxValue;
1741 }
1742 selectorIndices[0] = nextScrollSelectorIndex;
1743 ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
1744 }
1745
1746 /**
1747 * Ensures we have a cached string representation of the given <code>
1748 * selectorIndex</code> to avoid multiple instantiations of the same string.
1749 */
1750 private void ensureCachedScrollSelectorValue(int selectorIndex) {
1751 SparseArray<String> cache = mSelectorIndexToStringCache;
1752 String scrollSelectorValue = cache.get(selectorIndex);
1753 if (scrollSelectorValue != null) {
1754 return;
1755 }
1756 if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
1757 scrollSelectorValue = "";
1758 } else {
1759 if (mDisplayedValues != null) {
1760 int displayedValueIndex = selectorIndex - mMinValue;
1761 scrollSelectorValue = mDisplayedValues[displayedValueIndex];
1762 } else {
1763 scrollSelectorValue = formatNumber(selectorIndex);
1764 }
1765 }
1766 cache.put(selectorIndex, scrollSelectorValue);
1767 }
1768
1769 private String formatNumber(int value) {
1770 return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
1771 }
1772
1773 private void validateInputTextView(View v) {
1774 String str = String.valueOf(((TextView) v).getText());
1775 if (TextUtils.isEmpty(str)) {
1776 // Restore to the old value as we don't allow empty values
1777 updateInputTextView();
1778 } else {
1779 // Check the new value and ensure it's in range
1780 int current = getSelectedPos(str.toString());
1781 setValueInternal(current, true);
1782 }
1783 }
1784
1785 /**
1786 * Updates the view of this NumberPicker. If displayValues were specified in
1787 * the string corresponding to the index specified by the current value will
1788 * be returned. Otherwise, the formatter specified in {@link #setFormatter}
1789 * will be used to format the number.
1790 *
1791 * @return Whether the text was updated.
1792 */
1793 private boolean updateInputTextView() {
1794 /*
1795 * If we don't have displayed values then use the current number else
1796 * find the correct value in the displayed values for the current
1797 * number.
1798 */
1799 String text = (mDisplayedValues == null) ? formatNumber(mValue)
1800 : mDisplayedValues[mValue - mMinValue];
1801 if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) {
1802 mInputText.setText(text);
1803 return true;
1804 }
1805
1806 return false;
1807 }
1808
1809 /**
1810 * Notifies the listener, if registered, of a change of the value of this
1811 * NumberPicker.
1812 */
1813 private void notifyChange(int previous, int current) {
1814 if (mOnValueChangeListener != null) {
1815 mOnValueChangeListener.onValueChange(this, previous, mValue);
1816 }
1817 }
1818
1819 /**
1820 * Posts a command for changing the current value by one.
1821 *
1822 * @param increment Whether to increment or decrement the value.
1823 */
1824 private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
1825 if (mChangeCurrentByOneFromLongPressCommand == null) {
1826 mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
1827 } else {
1828 removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1829 }
1830 mChangeCurrentByOneFromLongPressCommand.setStep(increment);
1831 postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
1832 }
1833
1834 /**
1835 * Removes the command for changing the current value by one.
1836 */
1837 private void removeChangeCurrentByOneFromLongPress() {
1838 if (mChangeCurrentByOneFromLongPressCommand != null) {
1839 removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1840 }
1841 }
1842
1843 /**
1844 * Posts a command for beginning an edit of the current value via IME on
1845 * long press.
1846 */
1847 private void postBeginSoftInputOnLongPressCommand() {
1848 if (mBeginSoftInputOnLongPressCommand == null) {
1849 mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
1850 } else {
1851 removeCallbacks(mBeginSoftInputOnLongPressCommand);
1852 }
1853 postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
1854 }
1855
1856 /**
1857 * Removes the command for beginning an edit of the current value via IME.
1858 */
1859 private void removeBeginSoftInputCommand() {
1860 if (mBeginSoftInputOnLongPressCommand != null) {
1861 removeCallbacks(mBeginSoftInputOnLongPressCommand);
1862 }
1863 }
1864
1865 /**
1866 * Removes all pending callback from the message queue.
1867 */
1868 private void removeAllCallbacks() {
1869 if (mChangeCurrentByOneFromLongPressCommand != null) {
1870 removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
1871 }
1872 if (mSetSelectionCommand != null) {
1873 removeCallbacks(mSetSelectionCommand);
1874 }
1875 if (mBeginSoftInputOnLongPressCommand != null) {
1876 removeCallbacks(mBeginSoftInputOnLongPressCommand);
1877 }
1878 mPressedStateHelper.cancel();
1879 }
1880
1881 /**
1882 * @return The selected index given its displayed <code>value</code>.
1883 */
1884 private int getSelectedPos(String value) {
1885 if (mDisplayedValues == null) {
1886 try {
1887 return Integer.parseInt(value);
1888 } catch (NumberFormatException e) {
1889 // Ignore as if it's not a number we don't care
1890 }
1891 } else {
1892 for (int i = 0; i < mDisplayedValues.length; i++) {
1893 // Don't force the user to type in jan when ja will do
1894 value = value.toLowerCase();
1895 if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
1896 return mMinValue + i;
1897 }
1898 }
1899
1900 /*
1901 * The user might have typed in a number into the month field i.e.
1902 * 10 instead of OCT so support that too.
1903 */
1904 try {
1905 return Integer.parseInt(value);
1906 } catch (NumberFormatException e) {
1907
1908 // Ignore as if it's not a number we don't care
1909 }
1910 }
1911 return mMinValue;
1912 }
1913
1914 /**
1915 * Posts an {@link SetSelectionCommand} from the given <code>selectionStart
1916 * </code> to <code>selectionEnd</code>.
1917 */
1918 private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
1919 if (mSetSelectionCommand == null) {
1920 mSetSelectionCommand = new SetSelectionCommand();
1921 } else {
1922 removeCallbacks(mSetSelectionCommand);
1923 }
1924 mSetSelectionCommand.mSelectionStart = selectionStart;
1925 mSetSelectionCommand.mSelectionEnd = selectionEnd;
1926 post(mSetSelectionCommand);
1927 }
1928
1929 /**
1930 * The numbers accepted by the input text's {@link android.view.LayoutInflater.Filter}
1931 */
1932 private static final char[] DIGIT_CHARACTERS = new char[] {
1933 // Latin digits are the common case
1934 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
1935 // Arabic-Indic
1936 '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
1937 , '\u0669',
1938 // Extended Arabic-Indic
1939 '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
1940 , '\u06f9'
1941 };
1942
1943 /**
1944 * Filter for accepting only valid indices or prefixes of the string
1945 * representation of valid indices.
1946 */
1947 class InputTextFilter extends NumberKeyListener {
1948
1949 // XXX This doesn't allow for range limits when controlled by a
1950 // soft input method!
1951 public int getInputType() {
1952 return InputType.TYPE_CLASS_TEXT;
1953 }
1954
1955 @Override
1956 protected char[] getAcceptedChars() {
1957 return DIGIT_CHARACTERS;
1958 }
1959
1960 @Override
1961 public CharSequence filter(
1962 CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
1963 if (mDisplayedValues == null) {
1964 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
1965 if (filtered == null) {
1966 filtered = source.subSequence(start, end);
1967 }
1968
1969 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1970 + dest.subSequence(dend, dest.length());
1971
1972 if ("".equals(result)) {
1973 return result;
1974 }
1975 int val = getSelectedPos(result);
1976
1977 /*
1978 * Ensure the user can't type in a value greater than the max
1979 * allowed. We have to allow less than min as the user might
1980 * want to delete some numbers and then type a new number.
1981 * And prevent multiple-"0" that exceeds the length of upper
1982 * bound number.
1983 */
1984 if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
1985 return "";
1986 } else {
1987 return filtered;
1988 }
1989 } else {
1990 CharSequence filtered = String.valueOf(source.subSequence(start, end));
1991 if (TextUtils.isEmpty(filtered)) {
1992 return "";
1993 }
1994 String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
1995 + dest.subSequence(dend, dest.length());
1996 String str = String.valueOf(result).toLowerCase();
1997 for (String val : mDisplayedValues) {
1998 String valLowerCase = val.toLowerCase();
1999 if (valLowerCase.startsWith(str)) {
2000 postSetSelectionCommand(result.length(), val.length());
2001 return val.subSequence(dstart, val.length());
2002 }
2003 }
2004 return "";
2005 }
2006 }
2007 }
2008
2009 /**
2010 * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
2011 * middle element is in the middle of the widget.
2012 *
2013 * @return Whether an adjustment has been made.
2014 */
2015 private boolean ensureScrollWheelAdjusted() {
2016 // adjust to the closest value
2017 int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
2018 if (deltaY != 0) {
2019 mPreviousScrollerY = 0;
2020 if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
2021 deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
2022 }
2023 mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
2024 invalidate();
2025 return true;
2026 }
2027 return false;
2028 }
2029
2030 class PressedStateHelper implements Runnable {
2031 public static final int BUTTON_INCREMENT = 1;
2032 public static final int BUTTON_DECREMENT = 2;
2033
2034 private final int MODE_PRESS = 1;
2035 private final int MODE_TAPPED = 2;
2036
2037 private int mManagedButton;
2038 private int mMode;
2039
2040 public void cancel() {
2041 mMode = 0;
2042 mManagedButton = 0;
2043 LocalePicker.this.removeCallbacks(this);
2044 if (mIncrementVirtualButtonPressed) {
2045 mIncrementVirtualButtonPressed = false;
2046 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2047 }
2048 mDecrementVirtualButtonPressed = false;
2049 if (mDecrementVirtualButtonPressed) {
2050 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2051 }
2052 }
2053
2054 public void buttonPressDelayed(int button) {
2055 cancel();
2056 mMode = MODE_PRESS;
2057 mManagedButton = button;
2058 LocalePicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
2059 }
2060
2061 public void buttonTapped(int button) {
2062 cancel();
2063 mMode = MODE_TAPPED;
2064 mManagedButton = button;
2065 LocalePicker.this.post(this);
2066 }
2067
2068 @Override
2069 public void run() {
2070 switch (mMode) {
2071 case MODE_PRESS: {
2072 switch (mManagedButton) {
2073 case BUTTON_INCREMENT: {
2074 mIncrementVirtualButtonPressed = true;
2075 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2076 } break;
2077 case BUTTON_DECREMENT: {
2078 mDecrementVirtualButtonPressed = true;
2079 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2080 }
2081 }
2082 } break;
2083 case MODE_TAPPED: {
2084 switch (mManagedButton) {
2085 case BUTTON_INCREMENT: {
2086 if (!mIncrementVirtualButtonPressed) {
2087 LocalePicker.this.postDelayed(this,
2088 ViewConfiguration.getPressedStateDuration());
2089 }
2090 mIncrementVirtualButtonPressed ^= true;
2091 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2092 } break;
2093 case BUTTON_DECREMENT: {
2094 if (!mDecrementVirtualButtonPressed) {
2095 LocalePicker.this.postDelayed(this,
2096 ViewConfiguration.getPressedStateDuration());
2097 }
2098 mDecrementVirtualButtonPressed ^= true;
2099 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2100 }
2101 }
2102 } break;
2103 }
2104 }
2105 }
2106
2107 /**
2108 * Command for setting the input text selection.
2109 */
2110 class SetSelectionCommand implements Runnable {
2111 private int mSelectionStart;
2112
2113 private int mSelectionEnd;
2114
2115 public void run() {
2116 mInputText.setSelection(mSelectionStart, mSelectionEnd);
2117 }
2118 }
2119
2120 /**
2121 * Command for changing the current value from a long press by one.
2122 */
2123 class ChangeCurrentByOneFromLongPressCommand implements Runnable {
2124 private boolean mIncrement;
2125
2126 private void setStep(boolean increment) {
2127 mIncrement = increment;
2128 }
2129
2130 @Override
2131 public void run() {
2132 changeValueByOne(mIncrement);
2133 postDelayed(this, mLongPressUpdateInterval);
2134 }
2135 }
2136
2137 /**
2138 * @hide
2139 */
2140 public static class CustomEditText extends EditText {
2141
2142 public CustomEditText(Context context, AttributeSet attrs) {
2143 super(context, attrs);
2144 }
2145
2146 @Override
2147 public void onEditorAction(int actionCode) {
2148 super.onEditorAction(actionCode);
2149 if (actionCode == EditorInfo.IME_ACTION_DONE) {
2150 clearFocus();
2151 }
2152 }
2153 }
2154
2155 /**
2156 * Command for beginning soft input on long press.
2157 */
2158 class BeginSoftInputOnLongPressCommand implements Runnable {
2159
2160 @Override
2161 public void run() {
2162 showSoftInput();
2163 mIngonreMoveEvents = true;
2164 }
2165 }
2166
2167 /**
2168 * Class for managing virtual view tree rooted at this picker.
2169 */
2170 class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
2171 private static final int UNDEFINED = Integer.MIN_VALUE;
2172
2173 private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
2174
2175 private static final int VIRTUAL_VIEW_ID_INPUT = 2;
2176
2177 private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
2178
2179 private final Rect mTempRect = new Rect();
2180
2181 private final int[] mTempArray = new int[2];
2182
2183 private int mAccessibilityFocusedView = UNDEFINED;
2184
2185 @Override
2186 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
2187 switch (virtualViewId) {
2188 case View.NO_ID:
2189 return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
2190 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2191 case VIRTUAL_VIEW_ID_DECREMENT:
2192 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
2193 getVirtualDecrementButtonText(), mScrollX, mScrollY,
2194 mScrollX + (mRight - mLeft),
2195 mTopSelectionDividerTop + mSelectionDividerHeight);
2196 case VIRTUAL_VIEW_ID_INPUT:
2197 return createAccessibiltyNodeInfoForInputText(mScrollX,
2198 mTopSelectionDividerTop + mSelectionDividerHeight,
2199 mScrollX + (mRight - mLeft),
2200 mBottomSelectionDividerBottom - mSelectionDividerHeight);
2201 case VIRTUAL_VIEW_ID_INCREMENT:
2202 return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
2203 getVirtualIncrementButtonText(), mScrollX,
2204 mBottomSelectionDividerBottom - mSelectionDividerHeight,
2205 mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
2206 }
2207 return super.createAccessibilityNodeInfo(virtualViewId);
2208 }
2209
2210 @Override
2211 public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
2212 int virtualViewId) {
2213 if (TextUtils.isEmpty(searched)) {
2214 return Collections.emptyList();
2215 }
2216 String searchedLowerCase = searched.toLowerCase();
2217 List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
2218 switch (virtualViewId) {
2219 case View.NO_ID: {
2220 findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2221 VIRTUAL_VIEW_ID_DECREMENT, result);
2222 findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2223 VIRTUAL_VIEW_ID_INPUT, result);
2224 findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
2225 VIRTUAL_VIEW_ID_INCREMENT, result);
2226 return result;
2227 }
2228 case VIRTUAL_VIEW_ID_DECREMENT:
2229 case VIRTUAL_VIEW_ID_INCREMENT:
2230 case VIRTUAL_VIEW_ID_INPUT: {
2231 findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
2232 result);
2233 return result;
2234 }
2235 }
2236 return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
2237 }
2238
2239 @Override
2240 public boolean performAction(int virtualViewId, int action, Bundle arguments) {
2241 switch (virtualViewId) {
2242 case View.NO_ID: {
2243 switch (action) {
2244 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2245 if (mAccessibilityFocusedView != virtualViewId) {
2246 mAccessibilityFocusedView = virtualViewId;
2247 requestAccessibilityFocus();
2248 return true;
2249 }
2250 } return false;
2251 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2252 if (mAccessibilityFocusedView == virtualViewId) {
2253 mAccessibilityFocusedView = UNDEFINED;
2254 clearAccessibilityFocus();
2255 return true;
2256 }
2257 return false;
2258 }
2259 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
2260 if (LocalePicker.this.isEnabled()
2261 && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
2262 changeValueByOne(true);
2263 return true;
2264 }
2265 } return false;
2266 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
2267 if (LocalePicker.this.isEnabled()
2268 && (getWrapSelectorWheel() || getValue() > getMinValue())) {
2269 changeValueByOne(false);
2270 return true;
2271 }
2272 } return false;
2273 }
2274 } break;
2275 case VIRTUAL_VIEW_ID_INPUT: {
2276 switch (action) {
2277 case AccessibilityNodeInfo.ACTION_FOCUS: {
2278 if (LocalePicker.this.isEnabled() && !mInputText.isFocused()) {
2279 return mInputText.requestFocus();
2280 }
2281 } break;
2282 case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
2283 if (LocalePicker.this.isEnabled() && mInputText.isFocused()) {
2284 mInputText.clearFocus();
2285 return true;
2286 }
2287 return false;
2288 }
2289 case AccessibilityNodeInfo.ACTION_CLICK: {
2290 if (LocalePicker.this.isEnabled()) {
2291 showSoftInput();
2292 return true;
2293 }
2294 return false;
2295 }
2296 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2297 if (mAccessibilityFocusedView != virtualViewId) {
2298 mAccessibilityFocusedView = virtualViewId;
2299 sendAccessibilityEventForVirtualView(virtualViewId,
2300 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2301 mInputText.invalidate();
2302 return true;
2303 }
2304 } return false;
2305 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2306 if (mAccessibilityFocusedView == virtualViewId) {
2307 mAccessibilityFocusedView = UNDEFINED;
2308 sendAccessibilityEventForVirtualView(virtualViewId,
2309 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2310 mInputText.invalidate();
2311 return true;
2312 }
2313 } return false;
2314 default: {
2315 return mInputText.performAccessibilityAction(action, arguments);
2316 }
2317 }
2318 } return false;
2319 case VIRTUAL_VIEW_ID_INCREMENT: {
2320 switch (action) {
2321 case AccessibilityNodeInfo.ACTION_CLICK: {
2322 if (LocalePicker.this.isEnabled()) {
2323 LocalePicker.this.changeValueByOne(true);
2324 sendAccessibilityEventForVirtualView(virtualViewId,
2325 AccessibilityEvent.TYPE_VIEW_CLICKED);
2326 return true;
2327 }
2328 } return false;
2329 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2330 if (mAccessibilityFocusedView != virtualViewId) {
2331 mAccessibilityFocusedView = virtualViewId;
2332 sendAccessibilityEventForVirtualView(virtualViewId,
2333 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2334 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2335 return true;
2336 }
2337 } return false;
2338 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2339 if (mAccessibilityFocusedView == virtualViewId) {
2340 mAccessibilityFocusedView = UNDEFINED;
2341 sendAccessibilityEventForVirtualView(virtualViewId,
2342 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2343 invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
2344 return true;
2345 }
2346 } return false;
2347 }
2348 } return false;
2349 case VIRTUAL_VIEW_ID_DECREMENT: {
2350 switch (action) {
2351 case AccessibilityNodeInfo.ACTION_CLICK: {
2352 if (LocalePicker.this.isEnabled()) {
2353 final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
2354 LocalePicker.this.changeValueByOne(increment);
2355 sendAccessibilityEventForVirtualView(virtualViewId,
2356 AccessibilityEvent.TYPE_VIEW_CLICKED);
2357 return true;
2358 }
2359 } return false;
2360 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
2361 if (mAccessibilityFocusedView != virtualViewId) {
2362 mAccessibilityFocusedView = virtualViewId;
2363 sendAccessibilityEventForVirtualView(virtualViewId,
2364 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
2365 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2366 return true;
2367 }
2368 } return false;
2369 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
2370 if (mAccessibilityFocusedView == virtualViewId) {
2371 mAccessibilityFocusedView = UNDEFINED;
2372 sendAccessibilityEventForVirtualView(virtualViewId,
2373 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2374 invalidate(0, 0, mRight, mTopSelectionDividerTop);
2375 return true;
2376 }
2377 } return false;
2378 }
2379 } return false;
2380 }
2381 return super.performAction(virtualViewId, action, arguments);
2382 }
2383
2384 public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
2385 switch (virtualViewId) {
2386 case VIRTUAL_VIEW_ID_DECREMENT: {
2387 if (hasVirtualDecrementButton()) {
2388 sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2389 getVirtualDecrementButtonText());
2390 }
2391 } break;
2392 case VIRTUAL_VIEW_ID_INPUT: {
2393 sendAccessibilityEventForVirtualText(eventType);
2394 } break;
2395 case VIRTUAL_VIEW_ID_INCREMENT: {
2396 if (hasVirtualIncrementButton()) {
2397 sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
2398 getVirtualIncrementButtonText());
2399 }
2400 } break;
2401 }
2402 }
2403
2404 private void sendAccessibilityEventForVirtualText(int eventType) {
2405 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2406 AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2407 mInputText.onInitializeAccessibilityEvent(event);
2408 mInputText.onPopulateAccessibilityEvent(event);
2409 event.setSource(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT);
2410 requestSendAccessibilityEvent(LocalePicker.this, event);
2411 }
2412 }
2413
2414 private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
2415 String text) {
2416 if (AccessibilityManager.getInstance(mContext).isEnabled()) {
2417 AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
2418 event.setClassName(Button.class.getName());
2419 event.setPackageName(mContext.getPackageName());
2420 event.getText().add(text);
2421 event.setEnabled(LocalePicker.this.isEnabled());
2422 event.setSource(LocalePicker.this, virtualViewId);
2423 requestSendAccessibilityEvent(LocalePicker.this, event);
2424 }
2425 }
2426
2427 private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
2428 int virtualViewId, List<AccessibilityNodeInfo> outResult) {
2429 switch (virtualViewId) {
2430 case VIRTUAL_VIEW_ID_DECREMENT: {
2431 String text = getVirtualDecrementButtonText();
2432 if (!TextUtils.isEmpty(text)
2433 && text.toString().toLowerCase().contains(searchedLowerCase)) {
2434 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
2435 }
2436 } return;
2437 case VIRTUAL_VIEW_ID_INPUT: {
2438 CharSequence text = mInputText.getText();
2439 if (!TextUtils.isEmpty(text) &&
2440 text.toString().toLowerCase().contains(searchedLowerCase)) {
2441 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2442 return;
2443 }
2444 CharSequence contentDesc = mInputText.getText();
2445 if (!TextUtils.isEmpty(contentDesc) &&
2446 contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
2447 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
2448 return;
2449 }
2450 } break;
2451 case VIRTUAL_VIEW_ID_INCREMENT: {
2452 String text = getVirtualIncrementButtonText();
2453 if (!TextUtils.isEmpty(text)
2454 && text.toString().toLowerCase().contains(searchedLowerCase)) {
2455 outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
2456 }
2457 } return;
2458 }
2459 }
2460
2461 private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
2462 int left, int top, int right, int bottom) {
2463 AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
2464 info.setSource(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT);
2465 if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
2466 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2467 }
2468 if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
2469 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2470 }
2471 Rect boundsInParent = mTempRect;
2472 boundsInParent.set(left, top, right, bottom);
2473 info.setVisibleToUser(isVisibleToUser(boundsInParent));
2474 info.setBoundsInParent(boundsInParent);
2475 Rect boundsInScreen = boundsInParent;
2476 int[] locationOnScreen = mTempArray;
2477 getLocationOnScreen(locationOnScreen);
2478 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2479 info.setBoundsInScreen(boundsInScreen);
2480 return info;
2481 }
2482
2483 private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
2484 String text, int left, int top, int right, int bottom) {
2485 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2486 info.setClassName(Button.class.getName());
2487 info.setPackageName(mContext.getPackageName());
2488 info.setSource(LocalePicker.this, virtualViewId);
2489 info.setParent(LocalePicker.this);
2490 info.setText(text);
2491 info.setClickable(true);
2492 info.setLongClickable(true);
2493 info.setEnabled(LocalePicker.this.isEnabled());
2494 Rect boundsInParent = mTempRect;
2495 boundsInParent.set(left, top, right, bottom);
2496 info.setVisibleToUser(isVisibleToUser(boundsInParent));
2497 info.setBoundsInParent(boundsInParent);
2498 Rect boundsInScreen = boundsInParent;
2499 int[] locationOnScreen = mTempArray;
2500 getLocationOnScreen(locationOnScreen);
2501 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2502 info.setBoundsInScreen(boundsInScreen);
2503
2504 if (mAccessibilityFocusedView != virtualViewId) {
2505 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2506 }
2507 if (mAccessibilityFocusedView == virtualViewId) {
2508 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2509 }
2510 if (LocalePicker.this.isEnabled()) {
2511 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
2512 }
2513
2514 return info;
2515 }
2516
2517 private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
2518 int right, int bottom) {
2519 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
2520 info.setClassName(LocalePicker.class.getName());
2521 info.setPackageName(mContext.getPackageName());
2522 info.setSource(LocalePicker.this);
2523
2524 if (hasVirtualDecrementButton()) {
2525 info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_DECREMENT);
2526 }
2527 info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_INPUT);
2528 if (hasVirtualIncrementButton()) {
2529 info.addChild(LocalePicker.this, VIRTUAL_VIEW_ID_INCREMENT);
2530 }
2531
2532 info.setParent((View) getParentForAccessibility());
2533 info.setEnabled(LocalePicker.this.isEnabled());
2534 info.setScrollable(true);
2535
2536 final float applicationScale =
2537 getContext().getResources().getCompatibilityInfo().applicationScale;
2538
2539 Rect boundsInParent = mTempRect;
2540 boundsInParent.set(left, top, right, bottom);
2541 boundsInParent.scale(applicationScale);
2542 info.setBoundsInParent(boundsInParent);
2543
2544 info.setVisibleToUser(isVisibleToUser());
2545
2546 Rect boundsInScreen = boundsInParent;
2547 int[] locationOnScreen = mTempArray;
2548 getLocationOnScreen(locationOnScreen);
2549 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
2550 boundsInScreen.scale(applicationScale);
2551 info.setBoundsInScreen(boundsInScreen);
2552
2553 if (mAccessibilityFocusedView != View.NO_ID) {
2554 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
2555 }
2556 if (mAccessibilityFocusedView == View.NO_ID) {
2557 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
2558 }
2559 if (LocalePicker.this.isEnabled()) {
2560 if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
2561 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
2562 }
2563 if (getWrapSelectorWheel() || getValue() > getMinValue()) {
2564 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
2565 }
2566 }
2567
2568 return info;
2569 }
2570
2571 private boolean hasVirtualDecrementButton() {
2572 return getWrapSelectorWheel() || getValue() > getMinValue();
2573 }
2574
2575 private boolean hasVirtualIncrementButton() {
2576 return getWrapSelectorWheel() || getValue() < getMaxValue();
2577 }
2578
2579 private String getVirtualDecrementButtonText() {
2580 int value = mValue - 1;
2581 if (mWrapSelectorWheel) {
2582 value = getWrappedSelectorIndex(value);
2583 }
2584 if (value >= mMinValue) {
2585 return (mDisplayedValues == null) ? formatNumber(value)
2586 : mDisplayedValues[value - mMinValue];
2587 }
2588 return null;
2589 }
2590
2591 private String getVirtualIncrementButtonText() {
2592 int value = mValue + 1;
2593 if (mWrapSelectorWheel) {
2594 value = getWrappedSelectorIndex(value);
2595 }
2596 if (value <= mMaxValue) {
2597 return (mDisplayedValues == null) ? formatNumber(value)
2598 : mDisplayedValues[value - mMinValue];
2599 }
2600 return null;
2601 }
2602 }
2603
2604 static private String formatNumberWithLocale(int value) {
2605 return String.format(Locale.getDefault(), "%d", value);
2606 }
2607}