Android 自定义控件实现 (多行选择条)

一、实现思路(如何实现?)

二、读源码:TabLayout

三、自定义实现主要过程

四、该页面其他一些相关问题(Fragment销毁后RadioGroup恢复)

五、小结


虽然Android提供了一套GUI库,里面有很多控件,但是很多时候我们并不满足于系统的控件,通过自定义view,我们可以实现各种五花八门的效果,但是自定义控件有一定难度,尤其是复杂的自定义控件。

 

在云阅读新版本(5.4.3)中,交互需要实现多行的选择条,平常我们看到的基本上都是网易新闻这种单行的TabLayout。 那我们改如何实现各种定制化的控件呢? 先上图:

     1.网易新闻单行选择条

 

     2.网易云阅读多行选择条

  

    

         3.网易云阅读界面预览

一、实现思路(如何实现)

a、首先… … 、没错、、、第一个思路就是网上去搜搜看,有没有类似控件,去网上找了一圈没有发现类似的多行的选择器。

发现了两个单行的开源指示器还不错,有兴趣可以看看             

https://github.com/hackware1993/MagicIndicator http://blog.csdn.net/analyzesystem/article/details/51426473

b、既然网上没有,就只能自己实现了。实现自定义View一般有4种思路。

1.        继承特定的View(比如TextView)。例如定制的TextView可以继承TextView再添加或修改一些特定的方法。

2.        继承View重写onDraw的方法。(例如实现圆形view

3.        继承特定的ViewGroup。比如LinearLayout,几种常见View组合在一起的时候,可以采用此方法。不需要自己处理ViewGroup的测量和布局的两个过程。

4.        继承ViewGroup,需要自己处理ViewGroup的测量和布局的两个过程。

    思路23都不适合当前方法,剩下思路1、和2 一个是继承Tablayout重写onMeasureonLayout对子view重写测量和排列,另一种是自己继承viewGroup ,自己处理ViewGroup的测量和布局的两个过程。第一种好像更简单,但是我们可以先看看TabLayout源码。

二、读源码:TabLayout

1、内部类及分析其关系:

l   Tab类和TabView类和SlidingTabStrip类为TabLayout提供了三个基本的元素。 

l   TabLayoutOnPageChangeListenerViewPagerOnTabSelectedListener 实现了ViewPager类的两个接口,作用是监听ViewPager页面改变和Tab选中状态。

l   PagerAdapterObserver为观察者监控PagerAdapter数据变化。

2、TabLayout常用的方法如下:

3、源码中选择tab的关键实现:

    Tab是每个itemmode类。Tab内部类定义了item的成员变量,并setget方法,然后又封装了Tab的属性设置方法。

 

TabView是每个itemView,继承自LinearLayout。它的作用是让Tab同时显示文字和图片,也可以通过mCustomview设置自定义item

 

    TabLayout前两个个构造函数都是调用了自己的第三个构造函数,第三个构造函数里面添加了一个私有内部类SlidingTabStrip作为子view

这个自定义view SlidingTabStrip是私有内部类,它继承自LinearLayout,作用为获取tab的最宽宽度,设置SlidingTabStrip的宽度,并设置一个动画,随着tab的改变绘制SlidingTabStrip

    在将选项添加到选项卡的addTabView方法中,看到每个item其实是加到SlidingTabStrip中。

三、自定义实现主要过程

    通过读TabLayout源码,我们发现思路1,直接继承Tablayout重写onLayoutonMeasure对子view重写排列和测量不行,因为TabLayout里是加了一个私有的SlidingTabStrip作为子类,Itemview都是加在SlidingTabStrip中,没有办法通过重写私有 SlidingTabStriponLayoutonMeasure

所以现在只能继承ViewGroup,需要自己处理ViewGroup的测量和布局的两个过程。我们可以通过仿写TabLayout实现MyTabLayout,最简单的办法就是先把TabLayout拷贝出来到MyTabLayout,再进行定制化修改。

实现:

1.   先把TabLayout拷贝出来到MyTabLayout,先跑通。

2.   TabLayout包裹了一个SlidingTabStrip,我们只需要重写SlidingTabStriponLayoutonMeasure, 再重写TabLayoutonMeasure,让他的宽高为唯一子类SlidingTabStrip的宽高。

ViewGroup 常用的生命周期回调:initView(构造方法)、onFinishInflate(当布局加载完成调用)、onMeasure(当测量时调用)、onSizeChanged(当尺寸改变调用)、onLayout(当布局时调用)、onDrawdispatchDraw (绘制背景以及绘制子 View )。

初始化 ViewGroup 的流程大致为:构造方法创建对象->从布局加载(xml中定义时)->第一遍测量->开始改变尺寸->第一遍布局->第二遍测量->第二遍布局

 重载onMeasure()方法

为什么要重载onMeasure()方法这里就不赘述了。测算间距space 如果间距小于3dp 按顺序排列,否则每行N个(item4个字N=5 2个字N=7)。setMeasuredDimension(resolveSize(mScreenWidth, widthMeasureSpec), Hight);设置自身宽高(宽为屏幕宽,高为子View的排数)。



   由于ViewGroup的定位就是一个容器,用来盛放子控件的,所以就必须定义要以什么的方式来盛放,比如LinearLayout就是以横向或者纵向顺序存放,而RelativeLayout则以相对位置来摆放子控件,同样,我们的自定义ViewGroup也必须给出我们期望的布局方式,而这个定义就通过onLayout()函数来实现。

  childView.layout(mPainterPosX + space, mPainterPosY, mPainterPosX + width + space, mPainterPosY + height);   执行ChildView的绘制。

最后重写,TabLayoutonMeasure方法。

 

到目前为止,已经基本实现了,多层的选择器。

另外:

1、可以看到原来的TabLayout继承自HorizontalScrollView,所以需要禁止滑动功能,可以在构造函数添加如下代码:

2、细节的处理,还有很多间距什么需要处理,通过View的自定义参数的方式实现,这里不详叙述了。





3、如果每个item可以实现自定义view,处理好如下方法即可。

 

四、该页面其他一些相关问题(Fragment销毁后RadioGroup恢复

由一个个单独的Fragment改成了TabLayout的形式,往后翻几个之前的Fragment容易被回收,这里就需要恢复Fragment了,通onSaveInstanceState()方法实现。

RadioGroup的选择项恢复有很多坑。发现RadioGroup恢复后不会按设定存的选项进行设置,主要就是需要注意以下的两个问题:

1、一组RadioGroup设置选中时候,要以RadioGroup为单位设置check(),不要给单个RadioButton设置button.setChecked(true),否则恢复很容易出问题。应该以组为单位使用check()。



2、RadioButton设置id的时候应该设置一个独一无二的id,否则恢复也会出现巨坑。改成随机数后恢复正常、不再随便乱选中。

五、小结:

1.        一种自定义View可能有多种实现方式、我们要找到代价最小、最高效的方式。

2.        通过实践有了更深的体会。

本文来自网易实践者社区,经作者吴思博授权发布。