自定义View之时间选择控件

8,889 阅读8分钟

       自定义view是一个很神奇的技术,可以简单到只需要组合控件,也可以复杂到各种动画、矩阵操作的组合。有人看到视觉稿里从未见过的新控件而恐慌,也有人觉得自定义控件很有成就感。作为一名合格的Android高级工程师,自定义view乃是必经之路。嗯哼,自定义view的大佬可以按返回了,因为这篇帖子对你来说可能会太简单。废话少说,今天给大家带来本人自定义的时间选择器。

需求思想

       需求很简单,点击时间选择后弹出背景半透明选择弹框。弹框上小时和分钟选项可以上下滑动,滑动时会自动吸附到和目前滑动点最近的选项,也就是说要确保被选中的项一定是在正中间的。也可以点击来选择数字,数字是一个数组,可以动态配置。选中的数字标蓝色,当点击确定以后关闭当前弹框,获取到当前选中的时间。

       这个弹框可以用dialog来实现,这个就不多说大家都知道,重点是弹框里面的选择数字控件。这里两边的功能是一模一样的,都是可滑动可点击选择,最后都是获取到一个数字,最重要的是样式都一模一样,所以我当机立断,选择先封装一个上下滑动的数字选择器,然后在这个dialog中使用2次该数字选择器即可。这样的话万一以后要调整两边样式的话,方便统一调整,提高维护性。再者说,万一以后出现只需要选择一个数字的情况下,这个数字选择器就可以直接用了,而没必要重新自定义一个只选择一个数字的了,这样就大大提高了自定义view的灵活性,方便插拔。所以接下来只讲解选择一个数字的控件,该控件做好了后面double就好。

技术实现

1.如何实现半透明弹框效果

       这个其实很常用,继承Dialog然后调用addContentView方法传入自己写好的布局,我这里布局文件当然是包括上面的title文本,2个选择数字控件,一条线,以及下面的取消和确定按钮了。


2.如何做到让该控件正好显示5个选项

       由于我们是在做控件化,所有控件的大小都是固定的,这里我就写死了该控件的总高度,以及每个选项的高度为48dp,这里如果需要修改大小的,可以做成动态设置。

3.滑动到最顶部或者是最底部的时候,那半屏空白该如何处理

       我在初始化每个选项时有判断,如果是最顶部的或者是最底部那个TextView,就给它设置了一个相当于2个TextView高度,也就是96dp的margin。


4.如何支持可滑动效果

为了让它滑动起来,这里需要用到ScrollView容器,这里又想吐槽一下的是,ScrollView并没有提供监听滑动位置的public回调。这种常用的回调居然不给提供,真是毁三观呐!这个让我想起了EditText监听剪贴板,也是如出一辙。吐槽完了事情还是得自己干,为了解决这个问题,需要自定义一个VIew来继承ScrollView,复写onScrollChanged方法,然后自定义一个接口用来回调数据。


        有了可监听滑动位置的ScrollView以后就简单了,只需要往里面添加控件就可以了,首先在ScrollView里面放一个LinearLayout,然后在LinearLayout中根据可选择数组的大小动态添加TextView。


5.如何在初始化的时候自动滑动相应的默认位置

       如果在ScrollView显示出来前让它滑动,是没有效果的,所以不能在初始化的时候直接用scrollTo来进行滑动,而是需要调用ScrollView的post方法来延迟自动滑动,滑动到的具体位置是根据默认数据,也就是点击控件时传入的


       这里需要注意的是,这一次的默认滑动也会被监听到。因为我们选择了自己来控制滑动,所以为了避免再次重复滑动,需要对本次初始化的滑动进行过滤,同时也为了记录最初始的位置,这里定义一个布尔型变量来控制就可以了。


6.在滑动后,怎么确保被选中的数字每次都在正中间

       这里不需要记录多滑动了多少,所以其实很简单,只需要取本次滑动距离除以单个项高度,然后四舍五入,就可以获取到最近的位置了。为了实现这个功能,我们需要拦截系统ScrollView的滑动,从而自己来处理。具体方式是重写onTouchEvent方法,计算本次应该滑动的距离为四舍五入以后的位置,然后再返回true告知父View本次操作已处理。


7.滑动到上下边界的时候应该做什么处理

       我这里是做了双重的保障,首先在滑动的监听处理时,保证一次滑动的距离不会超过整个项高度,也不会滑动到小于0的边界。


       但是单单这么做是不够的,要考虑到单次虽然没有滑动到越出边界,但是滑了很多次以后越界的情况。所以我在监听的时候还做了第二层的处理,如果滑动到最上方以后,就当本次只滑动到了第0个View的位置,如果滑动到了最下方以后,继续滑也只算滑动到最后一个view的位置。


8.设置选中项的样式时应该注意什么

       因为被选中的TextView需要设置不同的字体、颜色,我在最初的时候,是用一个for循环专门来设置样式,每个子项都给设置了一遍,其中有很多项其实都样式没变。当时做的时候有考虑到这种情况,但是怕麻烦然后自己手机自测的时候也没出现问题,就没有修改方案直接提测了。果然测试小姐姐又找上门了,说是一个低端手机(华为7)上选择数字会很卡顿,看到这个bug我第一时间就知道是这里的问题,于是做了一次优化,代码如下图。保存当前的位置作为下次滑动以后的上次位置,这样每次只设置2个TextView的样式,避免多做无用功,导致app不必要的卡顿。所以平时开发时还是要多注重这种细节,尽量在编码阶段就做好这种优化。


9.设置背景和滑动事件冲突如何处理

        由于被选中项在长按的时候,背景需要置灰,为了实现该效果,需要给每个项设置一个selector的背景,并且设置clickable为true。只要设置了clickable,那么问题就来了,系统就会将TextView的事件作为最优先事件,所以ScrollView在onTouchEvent中将监听不到被按住这个事件,因为被它的子项TextView给抢夺走了。

textView.setClickable(true);
textView.setBackgroundResource(R.drawable.selector_itemcolor);

        在这里由于子View设置了clickable为true,系统优先将手指按下的事件让子view去消费了,所以在ScrollView中该事件在onInterceptTouchEvent中就被系统给拦截,转而给子View了,所以根本走不到onTouchEvent方法了。这里还好只是需要获取一下初始位置而已,不需要修改冲突,做一下兼容就可以了,我们不在ouTouchEvent中获取初始位置,改为在onInterceptTouchEvent中来获取即可。


总结:本文借实际项目中的一个自定义时间选择弹框来分享了一个自定义组合型控件,这种也是项目中经常用到的,这次分享的控件也很简单,希望能对大家有所帮助。其实自定义View千万种,但是万变不离其宗,我个人总结一下当UI让画新控件的时候,可以分为这几步走:

1.这个控件是做成View好还是ViewGroup好

2.view是否需要自己画,能否组合一下就可以

3.按照视觉稿来实现控件效果,处理一下可能的事件冲突

4.考虑一下低端机上是否可能有性能问题或者兼容性问题

5.有空的话考虑一下封装,方便以后维护,也提升灵活性

希望看完以后的新同学再也不害怕UI出新的控件了,无论她出什么交互视觉,只要不是特别复杂全都满足她。省的老板对界面视觉不满意的时候,UI还说因为你技术不行,人在家中趟,锅从天上来。只要做的和视觉稿一样,看她还能怎么说?另外之前分享了一个需要自己画的刻度尺,对自定义感兴趣的同学可以去翻我之前的文章,有其他问题的同学也欢迎一起讨论哦。

demo地址:github.com/dongrong-fu…