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
方法
initializeSelectorWheelIndices
方法可以理解为reset选择器中要显示字符串的数组,数值中的元素为字符串对应的index,并缓存index对应的string
/**
* 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
ensureCachedScrollSelectorValue
方法
ensureCachedScrollSelectorValue
方法用来缓存上面计算的selectorIndex
对应的string,将其存储在SparseArray<String>
中
/**
* Ensures we have a cached string representation of the given <code>
* selectorIndex</code> to avoid multiple instantiations of the same string.
*/
private void ensureCachedScrollSelectorValue(int selectorIndex) {
SparseArray<String> cache = mSelectorIndexToStringCache;
String scrollSelectorValue = cache.get(selectorIndex);
if (scrollSelectorValue != null) {
return;
}
if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
scrollSelectorValue = "";
} else {
if (mDisplayedValues != null) {
int displayedValueIndex = selectorIndex - mMinValue;
scrollSelectorValue = mDisplayedValues[displayedValueIndex];
} else {
scrollSelectorValue = formatNumber(selectorIndex);
}
}
cache.put(selectorIndex, scrollSelectorValue);
}
tryComputeMaxWidth
方法
tryComputeMaxWidth
方法用来计算字符串的最大宽度
/**
* Computes the max width if no such specified as an attribute.
*/
private void tryComputeMaxWidth() {
if (!mComputeMaxWidth) {
return;
}
int maxTextWidth = 0;
if (mDisplayedValues == null) {
float maxDigitWidth = 0;
//找出0-9字符中最大的宽度
for (int i = 0; i <= 9; i++) {
final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
if (digitWidth > maxDigitWidth) {
maxDigitWidth = digitWidth;
}
}
int numberOfDigits = 0;
int current = mMaxValue;
//数字有多少位
while (current > 0) {
numberOfDigits++;
current = current / 10;
}
maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
} else {
final int valueCount = mDisplayedValues.length;
for (int i = 0; i < valueCount; i++) {
//计算字符串宽度
final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
if (textWidth > maxTextWidth) {
maxTextWidth = (int) textWidth;
}
}
}
maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
if (mMaxWidth != maxTextWidth) {
if (maxTextWidth > mMinWidth) {
mMaxWidth = maxTextWidth;
} else {
mMaxWidth = mMinWidth;
}
invalidate();
}
}