Android一款UI体验好于NumberPicker的自定义控件NumberPickerView

carbs / 文 发表于2016-06-29 21:57 次阅读 自定义控件,view,android

NumberPickerView

another NumberPicker with more flexible attributes on Android platform


项目地址

https://github.com/Carbs0126/NumberPickerView


文章地址:

http://www.jianshu.com/p/ce33faec2a29

前言

在平时开发中会用到NumberPicker组件,但是默认风格的NumberPicker具有一些不灵活的属性,且定制起来比较麻烦,且缺少一些过渡动效,因此在应用开发时,一般采用自定义的控件来完成选择功能。


控件截图

NumberPickerView的动态图:(截图处理有点卡,实际效果很流畅)

numberpickerview_refine2.gif


NumberPickerView的截图:

numberpickerviewall.jpg


使用NumberPickerView开发的一款公历/农历选择View:

gregorian.jpg


具体项目地址可见:

https://github.com/Carbs0126/GregorianLunarCalendar


说明

NumberPickerView是一款与android原生NumberPicker具有类似界面以及类似功能的View。主要功能同样是从多个候选项中通过上下滚动的方式选择需要的选项,但是与NumberPicker相比较,有几个主要不同点,下面是两者的不同之处。


原始控件特性-NumberPicker

1. 显示窗口只能显示3个备选选项;

2. 在fling时阻力较大,无法快速滑动;

3. 在选中与非选中状态切换比较生硬;

4. 批量改变选项中的内容时,没有动画效果;

5. 动态设置wrap模式时(setWrapSelectorWheel()方法),会有“暂时显示不出部分选项”的问题;

6. 选中位置没有文字说明;

7. 代码中不能控制选项滑动滚动到某一item;


自定义控件特性-NumberPickerView

1. 显示窗口可以显示多个备选选项;

2. fling时滑动速度较快,且可以设置摩擦力;

3. 在选中与非选中的状态滑动时,具有渐变的动画效果,包括文字放大缩小以及颜色的渐变;

4. 在批量改变选项中的内容时,可以选择是否采用友好的滑动效果;

5. 可以动态的设置是否wrap,即,是否循环滚动;

6. 选中位置可以添加文字说明,可控制文字字体大小颜色等;

7. 具有在代码中动态的滑动到某一位置的功能;

8. 支持wrap_content,支持item的padding;

9. 提供多种属性,优化UI效果;

10. 在滑动过程中不响应onValueChanged()

11. 点击上下单元格,可以自动滑动到对应的点击对象。

12. 兼容NumberPicker的重要方法和接口:


    //兼容的方法有:


    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.android</groupId>   

    <artifactId>NumberPickerView</artifactId> 

    <version>1.0.2</version> 

    <type>pom</type>

</dependency>


2.通过布局声明NumberPickerView

<cn.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()等方法的代码

        }

    });

将NumberPicker改为NumberPickerView

要替代项目中使用的NumberPicker,只需要将涉及NumberPicker的代码(如回调中传入了NumberPicker、使用了NumberPicker的内部接口)改为NumberPickerView即可。


各项自定义属性的说明

<declare-styleable name="NumberPickerView">

    <attr name="npv_ShowCount"format="reference|integer"/>//显示的条目个数,默认3个

    <attr name="npv_ShowDivider"format="reference|boolean"/>//是否显示两条divider,默认显示

    <attr name="npv_DividerColor"format="reference|color"/>//两条divider的颜色

    <attr name="npv_DividerMarginLeft"format="reference|dimension"/>//divider距左侧的距离

    <attr name="npv_DividerMarginRight"format="reference|dimension"/>//divider距右侧的距离

    <attr name="npv_DividerHeight"format="reference|dimension"/>//divider的高度

    <attr name="npv_TextColorNormal"format="reference|color"/>//未选中文字的颜色

    <attr name="npv_TextColorSelected"format="reference|color"/>//选中文字的颜色

    <attr name="npv_TextColorHint"format="reference|color"/>//中间偏右侧说明文字的颜色

    <attr name="npv_TextSizeNormal"format="reference|dimension"/>//未选中文字的大小

    <attr name="npv_TextSizeSelected"format="reference|dimension"/>//选中文字的大小

    <attr name="npv_TextSizeHint"format="reference|dimension"/>//说明文字的大小

    <attr name="npv_TextArray"format="reference"/>//文字内容,stringarray类型

    <attr name="npv_MinValue"format="reference|integer"/>//最小值,同setMinValue()

    <attr name="npv_MaxValue"format="reference|integer"/>//最大值,同setMaxValue()

    <attr name="npv_WrapSelectorWheel"format="reference|boolean"/>//设置是否wrap,同setWrapSelectorWheel

    <attr name="npv_HintText"format="reference|string"/>//设置说明文字

    <attr name="npv_EmptyItemHint"format="reference|string"/>//空行的显示文字,默认不显示任何文字。只在WrapSelectorWheel==false是起作用

    <attr name="npv_MarginStartOfHint"format="reference|dimension"/>//说明文字距离左侧的距离,"左侧"是指文字array最宽item的右侧

    <attr name="npv_MarginEndOfHint"format="reference|dimension"/>//说明文字距离右侧的距离

    <attr name="npv_ItemPaddingHorizental"format="reference|dimension"/>//item的水平padding,用于wrap_content模式

    <attr name="npv_ItemPaddingVertical"format="reference|dimension"/>//item的竖直padding,用于wrap_content模式

//以下属性用于在wrap_content模式下,改变内容array并且又不想让控件"跳动",那么就可以设置所有改变的内容的最大宽度

    <attr name="npv_AlternativeTextArrayWithMeasureHint"format="reference"/>//可能达到的最大宽度,包括说明文字在内,最大宽度只可能比此String的宽度更大

    <attr name="npv_AlternativeTextArrayWithoutMeasureHint"format="reference"/>//可能达到的最大宽度,不包括说明文字在内,最大宽度只可能比此String的宽度+说明文字+说明文字marginstart +说明文字marginend 更大

    <attr name="npv_AlternativeHint"format="reference|string"/>//说明文字的最大宽度


主要原理

一般情况下,我们只使用NumberPicker对文字进行选择,很少涉及到添加不同的View甚至是图片,因此,NumberPickerView只针对传入的String[] 类型的内容通过onDraw(Canvas canvas)函数进行显示。

这里主要涉及三个知识点:

1.滚动效果的产生:

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效果。这两者均是通过overrideonTouchEvent(MotionEvent event)函数来实现的,不过实现的具体过程稍有不同:

(1). 跟随手指滑动是复写了MotionEvent.ACTION_MOVE的情况,根据当前手指移动的坐标值,计算globalY值,并刷新显示。

(2). fling效果是通过VelocityTracker工具类实现的,此工具类可以根据接收的两个连续发出的MotionEvent来获取当前的滑动速度,若速度大于阈值,则将初始坐标值、VelocityTracker计算获取的速度传递给Scroller.fling(...)函数,此时再次刷新,即可通过computeScroll()函数,获取当前globalY值,进而更新绘图。


2.自动校准位置

为了实现自动校准,我在每次手指Up或者Scroller开始Fling时,均通过handler发送refresh消息,在接受到此消息时,计算当前位置是否处于被校准的位置,如果否,则计算需要滑动的位移,并将值传递给Scroller,进而进行校准滑动。当确保已经校准后,暂停发送refresh消息。


3.渐变的UI效果

渐变UI效果同样是通过计算当前滑动的坐标以及某个item与中间显示位置的差值比例,来确定此item中的字体大小以及颜色。


将NumberPicker改为NumberPickerView

要替代项目中使用的NumberPicker,只需要将涉及NumberPicker的代码(如回调中传入了NumberPicker、使用了NumberPicker的内部接口)改为NumberPickerView即可。


项目地址

https://github.com/Carbs0126/NumberPickerView

欢迎大家指教。email:yeah0126@yeah.net


收藏 赞 (10) 踩 (0)