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

Posted carbs_wang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android平台一款UI体验好于NumberPicker的自定义控件NumberPickerView相关的知识,希望对你有一定的参考价值。

NumberPickerView

another NumberPicker with more flexible attributes on android platform

项目地址

https://github.com/Carbs0126/NumberPickerView

前言

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

控件截图



静态截图以及渐变效果



应用NumberPickerView的一个实例,一款可以选择公历/农历日期的View,且公农历自由切换,项目地址 GregorianLunarCalendar



应用NumberPickerView的一个实例,一款可以选择公历/农历日期的View,且公农历自由切换

gif大小超过限制了,具体效果可见:
https://github.com/Carbs0126/Screenshot/blob/master/gregorian.gif

截屏有些问题,使得看上去有点卡顿且divider颜色不一致,实际效果很流畅。具体项目地址可见:
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()等方法的代码
            
        );
    

5.各项自定义属性的说明

    <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并且又不想让控件"跳动",那么就可以设置所有改变的内容的最大宽度
        <!--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-->
        <attr name="npv_AlternativeTextArrayWithMeasureHint" format="reference" />//可能达到的最大宽度,包括说明文字在内,最大宽度只可能比此String的宽度更大
        <attr name="npv_AlternativeTextArrayWithoutMeasureHint" format="reference" />//可能达到的最大宽度,不包括说明文字在内,最大宽度只可能比此String的宽度+说明文字+说明文字marginstart +说明文字marginend 更大
        <!--the max length of hint content-->
        <attr name="npv_AlternativeHint" format="reference|string" />//说明文字的最大宽度
    </declare-styleable>

主要原理

一般情况下,我们只使用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效果。这两者均是通过override onTouchEvent(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即可。

天若有情天亦老,Star一下好不好?

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

以上是关于Android平台一款UI体验好于NumberPicker的自定义控件NumberPickerView的主要内容,如果未能解决你的问题,请参考以下文章

Docker系列七: 使用Humpback管理工具管理容器(一款UI管理工具)

鸿蒙初体验-五子棋

最接近原生APP体验的高性能前端框架——MUI

小程序开发前准备

个人总结

谷歌的开源跨平台移动 UI 框架Flutter