another NumberPicker with more flexible attributes on Android platform
https://github.com/Carbs0126/NumberPickerView
在平时开发中会用到NumberPicker组件,但是默认风格的NumberPicker
具有一些不灵活的属性,且定制起来比较麻烦,且缺少一些过渡动效,因此在应用开发时,一般采用自定义的控件来完成选择功能。
gif大小超过限制了,具体效果可见:
https://github.com/Carbs0126/Screenshot/blob/master/gregorian.gif
截屏有些问题,使得看上去有点卡顿且divider颜色不一致,实际效果很流畅。具体项目地址可见:
https://github.com/Carbs0126/GregorianLunarCalendar
NumberPickerView
是一款与android原生NumberPicker
具有类似界面以及类似功能的View
。
主要功能同样是从多个候选项中通过上下滚动的方式选择需要的选项,但是与NumberPicker
相比较,有几个主要不同点,下面是两者的不同之处。
setWrapSelectorWheel()
方法),会有“暂时显示不出部分选项”的问题;wrap_content
,支持item的paddingonValueChanged()
兼容的方法有:
setOnValueChangedListener()
setOnScrollListener()
setDisplayedValues()/getDisplayedValues()
setWrapSelectorWheel()/getWrapSelectorWheel()
setMinValue()/getMinValue()
setMaxValue()/getMaxValue()
setValue()/getValue()
兼容的内部接口有:
OnValueChangeListener
OnScrollListener
1.导入至工程
compile 'cn.carbswang.android:NumberPickerView:1.0.2'
或者
<dependency>
<groupId>cn.carbswang.androidgroupId>
<artifactId>NumberPickerViewartifactId>
<version>1.0.2version>
<type>pomtype>
dependency>
2.通过布局声明NumberPickerView
.carbswang.android.numberpickerview.library.NumberPickerView
android:id="@+id/picker"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:background="#11333333"
android:contentDescription="test_number_picker_view"
app:npv_ItemPaddingHorizental="5dp"
app:npv_ItemPaddingVertical="5dp"
app:npv_ShowCount="5"
app:npv_TextSizeNormal="16sp"
app:npv_TextSizeSelected="20sp"
app:npv_WrapSelectorWheel="true"/>
3.Java代码中使用:
1)若设置的数据(String[] mDisplayedValues)不会再次改变,可以使用如下方式进行设置:(与NumberPicker的设置方式一致)
picker.setMinValue(minValue);
picker.setMaxValue(maxValue);
picker.setValue(value);
2)若设置的数据(String[] mDisplayedValues)会改变,可以使用如下组合方式进行设置:(与NumberPicker的更改数据方式一致)
int minValue = getMinValue();
int oldMaxValue = getMaxValue();
int oldSpan = oldMaxValue - minValue + 1;
int newMaxValue = display.length - 1;
int newSpan = newMaxValue - minValue + 1;
if (newSpan > oldSpan) {
setDisplayedValues(display);
setMaxValue(newMaxValue);
} else {
setMaxValue(newMaxValue);
setDisplayedValues(display);
}
或者直接使用NumberPickerView提供的方法:
refreshByNewDisplayedValues(String[] display)
使用此方法时需要注意保证数据改变前后的minValue值不变。
4.另外,NumberPickerView提供了平滑滚动的方法:
public void smoothScrollToValue(int fromValue, int toValue, boolean needRespond)
此方法与setValue(int)
方法相同之处是可以动态设置当前显示的item,不同之处在于此方法可以使NumberPickerView
平滑的从滚动,即从fromValue
值挑选最近路径滚动到toValue
,第三个参数needRespond
用来标识在滑动过程中是否响应onValueChanged
回调函数。因为多个NumberPickerView
在联动时,很可能不同的NumberPickerView
的停止时间不同,如果在此时响应了onValueChanged
回调,就可能再次联动,造成数据不准确,将needRespond
置为false
,可避免在滑动中响应回调函数。
另外,在使用此方法或者间接调用此方法时,需要注意最好不要在onCreate(Bundle savedInstanceState)
方法中调用,因为scroll动画需要一定时间,如需确要在onCreate(Bundle savedInstanceState)
中调用,请使用如下方式:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//代码省略
mNumberPickerView.post(new Runnable() {
@Override
public void run() {
//调用smoothScrollToValue()等方法的代码
}
});
}
5.各项自定义属性的说明
"NumberPickerView">
"npv_ShowCount" format="reference|integer" />//显示的条目个数,默认3个
"npv_ShowDivider" format="reference|boolean" />//是否显示两条divider,默认显示
"npv_DividerColor" format="reference|color" />//两条divider的颜色
"npv_DividerMarginLeft" format="reference|dimension" />//divider距左侧的距离
"npv_DividerMarginRight" format="reference|dimension" />//divider距右侧的距离
"npv_DividerHeight" format="reference|dimension" />//divider的高度
"npv_TextColorNormal" format="reference|color" />//未选中文字的颜色
"npv_TextColorSelected" format="reference|color" />//选中文字的颜色
"npv_TextColorHint" format="reference|color" />//中间偏右侧说明文字的颜色
"npv_TextSizeNormal" format="reference|dimension" />//未选中文字的大小
"npv_TextSizeSelected" format="reference|dimension" />//选中文字的大小
"npv_TextSizeHint" format="reference|dimension" />//说明文字的大小
"npv_TextArray" format="reference" />//文字内容,stringarray类型
"npv_MinValue" format="reference|integer" />//最小值,同setMinValue()
"npv_MaxValue" format="reference|integer" />//最大值,同setMaxValue()
"npv_WrapSelectorWheel" format="reference|boolean" />//设置是否wrap,同setWrapSelectorWheel
"npv_HintText" format="reference|string" />//设置说明文字
"npv_EmptyItemHint" format="reference|string" />//空行的显示文字,默认不显示任何文字。只在WrapSelectorWheel==false是起作用
"npv_MarginStartOfHint" format="reference|dimension" />//说明文字距离左侧的距离,"左侧"是指文字array最宽item的右侧
"npv_MarginEndOfHint" format="reference|dimension" />//说明文字距离右侧的距离
"npv_ItemPaddingHorizental" format="reference|dimension" />//item的水平padding,用于wrap_content模式
"npv_ItemPaddingVertical" format="reference|dimension" />//item的竖直padding,用于wrap_content模式
//以下属性用于在wrap_content模式下,改变内容array并且又不想让控件"跳动",那么就可以设置所有改变的内容的最大宽度
--just used to measure maxWidth for wrap_content without hint,
the string array will never be displayed.
you can set this attr if you want to keep the wraped numberpickerview
width unchanged when alter the content list-->
"npv_AlternativeTextArrayWithMeasureHint" format="reference" />//可能达到的最大宽度,包括说明文字在内,最大宽度只可能比此String的宽度更大
"npv_AlternativeTextArrayWithoutMeasureHint" format="reference" />//可能达到的最大宽度,不包括说明文字在内,最大宽度只可能比此String的宽度+说明文字+说明文字marginstart +说明文字marginend 更大
--the max length of hint content-->
"npv_AlternativeHint" format="reference|string" />//说明文字的最大宽度
一般情况下,我们只使用NumberPicker对文字进行选择,很少涉及到添加不同的View甚至是图片,因此,NumberPickerView只针对传入的String[] 类型的内容通过onDraw(Canvas canvas)函数进行显示。这里主要涉及三个知识点:
Android的framework中带有一个工具类Scroller,此类的功能是根据输入值以及当前持续的时间,配合插值计算器,计算出当前的输出值。输入值一般是速度和坐标值,输出值是坐标值,举例来说,可以将其想像成高中物理根据初始速度、加速度和运行时间求取当前路程。Scroller的实现比较巧妙,它不会在startScroll()
后去计算当前“路程”,而是只记录“出发”时的各种状态,只有当外部需要知道此时滑动到的位置时,才会去根据时间以及插值来进行计算,这种设计避免了另起线程实时计算等问题。
那么问题来了,什么时候去获取scroller中当前的坐标值呢?阅读View类的源码,我们可以看到在View的boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
函数中,有如下代码:
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
即在绘制view时,先调用computeScroll()
,因此我们可以在computeScroll()
函数重新设置或刷新当前的坐标,并在onDraw(Canvas canvas)
函数中根据当前的状态重新绘制此view。
computeScroll()
的使用方法通常为:
@Override
public void computeScroll() {
//如果Scroller仍然在滚动
if (mScroller.computeScrollOffset()) {
//获取mScroller中的值并计算和记录,如 mGlobalY = mScroller.getCurrY();
//刷新
postInvalidate();
}
}
在NumberPickerView中,我使用一个globalY值来记录当前滚动到的坐标,并通过与Item的高度相除,计算当前应该显示的item的偏移值,从而画出当前只在显示区域的item。
绘制的思路明确了,再来看滑动跟随手指移动以及fling效果。这两者均是通过override onTouchEvent(MotionEvent event)
函数来实现的,不过实现的具体过程稍有不同:
(1). 跟随手指滑动是复写了MotionEvent.ACTION_MOVE
的情况,根据当前手指移动的坐标值,计算globalY值,并刷新显示。
(2). fling效果是通过VelocityTracker
工具类实现的,此工具类可以根据接收的两个连续发出的MotionEvent
来获取当前的滑动速度,若速度大于阈值,则将初始坐标值、VelocityTracker
计算获取的速度传递给Scroller.fling(...)
函数,此时再次刷新,即可通过computeScroll()
函数,获取当前globalY值,进而更新绘图。
为了实现自动校准,我在每次手指Up或者Scroller开始Fling时,均通过handler发送refresh消息,在接受到此消息时,计算当前位置是否处于被校准的位置,如果否,则计算需要滑动的位移,并将值传递给Scroller,进而进行校准滑动。当确保已经校准后,暂停发送refresh消息。
渐变UI效果同样是通过计算当前滑动的坐标以及某个item与中间显示位置的差值比例,来确定此item中的字体大小以及颜色。
要替代项目中使用的NumberPicker,只需要将涉及NumberPicker的代码(如回调中传入了NumberPicker、使用了NumberPicker的内部接口)改为NumberPickerView即可。
天若有情天亦老,Star一下好不好?
欢迎大家不吝指教。
email: [email protected]