NumberPicker分析(一)
NumberPicker可实现连续滚动的字符串选择,其实现方式很有借鉴的意义
以最基本的使用方式为例,在layout中布局:
<NumberPicker
android:id="@+id/number_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
/>
然后设置minValue和maxValue。(当然也可以设置DisplayedValues,这里以最简单的使用方式为例)
mNumberPicker.setMaxValue(4);
mNumberPicker.setMinValue(0);
其显示效果如下:

分析下NumberPicker构造方法(源码可参考NumberPicker.java)
mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
mHasSelectorWheel表示 - 是否具有选择轮
如果在源码处添加一个debug的断点,会发现mHasSelectorWheel结果是true,即表示layoutResId不是DEFAULT_LAYOUT_RESOURCE_ID(R.layout.number_picker)
通过官方文档对NumberPicker的介绍,可发现其style与主题有关:
- 如果当前主题是从
R.style.Theme派生的,则小部件将当前值显示为可编辑的输入字段,上面有一个递增按钮,下面有一个递减按钮。 长按按钮可以快速更改当前值。 点击输入字段允许输入所需的值。- 如果当前主题是从
R.style.Theme_Holo或R.style.Theme_Holo_Light派生的,则小部件将当前值显示为可编辑的输入字段,上面的值较小,下面的值较大。 点击较小或较大的值,通过向上或向下动画数字轴来选择它,使所选值成为当前值。 向上或向下滑动允许当前值的多个增量或减量。 长按较小和较大的值也可以快速更改当前值。 点击当前值可以输入所需的值。- 如果当前主题是从
R.style.Theme_Material派生的,则小部件将当前值显示为滚动的垂直选择器,所选值位于中心,前后数字由分隔符分隔。 通过垂直滑动来更改值。 可以使用R.attr.selectionDividerHeight属性更改分隔线的厚度,可以使用R.attr.colorControlNormal属性更改分隔线的颜色。
在android源码中搜索下number_picker.xml相关的布局(frameworks/base/core/res/res/layout/number_picker.xml)

Holo主题中NumberPicker定义如下(frameworks/base/core/res/res/values/styles_holo.xml):
<style name="Widget.Holo.NumberPicker" parent="Widget.NumberPicker">
<item name="internalLayout">@layout/number_picker_with_selector_wheel</item>
<item name="solidColor">@color/transparent</item>
<item name="selectionDivider">@drawable/numberpicker_selection_divider</item>
<item name="selectionDividerHeight">2dip</item>
<item name="selectionDividersDistance">48dip</item>
<item name="internalMinWidth">64dip</item>
<item name="internalMaxHeight">180dip</item>
<item name="virtualButtonPressedDrawable">?attr/selectableItemBackground</item>
</style>
其internalLayout对应的布局为number_picker_with_selector_wheel
number_picker_with_selector_wheel.xml布局文件内容如下:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<view class="android.widget.NumberPicker$CustomEditText"
android:textAppearance="?android:attr/textAppearanceMedium"
android:id="@+id/numberpicker_input"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:background="@null" />
</merge>
可见其中的view,是一个EditText,类型是NumberPicker的内部类CustomEditText
如果点击下上面创建的NumberPicker的中间部分,会弹出键盘编辑值

setMinValue 和 setMaxValue
setMinValue 方法 和 setMaxValue 方法中逻辑有共通之处
/**
* Sets the min value of the picker.
*
* @param minValue The min value inclusive.
*
* <strong>Note:</strong> The length of the displayed values array
* set via {@link #setDisplayedValues(String[])} must be equal to the
* range of selectable numbers which is equal to
* {@link #getMaxValue()} - {@link #getMinValue()} + 1.
*/
public void setMinValue(int minValue) {
if (mMinValue == minValue) {
return;
}
if (minValue < 0) {
throw new IllegalArgumentException("minValue must be >= 0");
}
mMinValue = minValue;
if (mMinValue > mValue) {
//设置了当前值
mValue = mMinValue;
}
updateWrapSelectorWheel();
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
invalidate();
}
/**
* Sets the max value of the picker.
*
* @param maxValue The max value inclusive.
*
* <strong>Note:</strong> The length of the displayed values array
* set via {@link #setDisplayedValues(String[])} must be equal to the
* range of selectable numbers which is equal to
* {@link #getMaxValue()} - {@link #getMinValue()} + 1.
*/
public void setMaxValue(int maxValue) {
if (mMaxValue == maxValue) {
return;
}
if (maxValue < 0) {
throw new IllegalArgumentException("maxValue must be >= 0");
}
mMaxValue = maxValue;
if (mMaxValue < mValue) {
mValue = mMaxValue;
}
updateWrapSelectorWheel();
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
invalidate();
}
如都调用量initializeSelectorWheelIndices方法
/**
* Resets the selector indices and clear the cached string representation of
* these indices.
*/
@UnsupportedAppUsage
private void initializeSelectorWheelIndices() {
mSelectorIndexToStringCache.clear();
int[] selectorIndices = mSelectorIndices;
int current = getValue();
for (int i = 0; i < mSelectorIndices.length; i++) {
/**
* 1.SELECTOR_MIDDLE_ITEM_INDEX表示中间行,总共3行,中间行index就为1
* 2.current表示当前值,在上面的初始设置中,即为mMinValue
*/
int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
if (mWrapSelectorWheel) {
//处理selectorIndex大于最大值和小于最小值的情况
selectorIndex = getWrappedSelectorIndex(selectorIndex);
}
selectorIndices[i] = selectorIndex;
ensureCachedScrollSelectorValue(selectorIndices[i]);
}
}
/**
* @return The wrapped index <code>selectorIndex</code> value.
*/
private int getWrappedSelectorIndex(int selectorIndex) {
if (selectorIndex > mMaxValue) {
return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
} else if (selectorIndex < mMinValue) {
return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
}
return selectorIndex;
}
如何理解上面的代码?
mSelectorIndices是一个int数组,我自己的理解,其保存的是页面上从上到下显示的字符串,在数组中对应的索引index
例如在上面设置minValue为0,maxValue为4,可理解要显示的字符串数组为[0, 1, 2, 3, 4]
所以第一次要展示的字符串为[4, 0, 1],其index也对应为[4, 0, 1]
由于是循环滚动,所以如果计算的selectorIndex小于了最小值0, 即表示要从数组[0, 1, 2, 3, 4]逆序寻找,即从maxValue往前去获取
如第一次遍历中,selectorIndex = -1,即从往前maxValue找一个,即为maxValue本身

即getWrappedSelectorIndex方法中的
mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1
如果current == 4, selectorIndex = 5时,此时selectorIndex超过了最大值4,即表示要从数组[0, 1, 2, 3, 4]正序寻找,从minValue开始寻找

即getWrappedSelectorIndex方法中的
mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1



















