Android 自定义控件实现 (多行选择条)
by 吴思博
一、实现思路(如何实现?)
二、读源码: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的测量和布局的两个过程。
思路2、3都不适合当前方法,剩下思路1、和2。一个是继承Tablayout重写onMeasure和onLayout对子view重写测量和排列,另一种是自己继承viewGroup,自己处理ViewGroup的测量和布局的两个过程。第一种好像更简单,但是我们可以先看看TabLayout源码。
二、读源码:TabLayout
1、内部类及分析其关系:
lTab类和TabView类和SlidingTabStrip类为TabLayout提供了三个基本的元素。
lTabLayoutOnPageChangeListener和ViewPagerOnTabSelectedListener实现了ViewPager类的两个接口,作用是监听ViewPager页面改变和Tab选中状态。
lPagerAdapterObserver为观察者监控PagerAdapter数据变化。
2、TabLayout常用的方法如下:
3、源码中选择tab的关键实现:
Tab是每个item的mode类。Tab内部类定义了item的成员变量,并set和get方法,然后又封装了Tab的属性设置方法。
TabView是每个item的View,继承自LinearLayout。它的作用是让Tab同时显示文字和图片,也可以通过mCustomview设置自定义item。
TabLayout前两个个构造函数都是调用了自己的第三个构造函数,第三个构造函数里面添加了一个私有内部类SlidingTabStrip作为子view。
这个自定义view SlidingTabStrip是私有内部类,它继承自LinearLayout,作用为获取tab的最宽宽度,设置SlidingTabStrip的宽度,并设置一个动画,随着tab的改变绘制SlidingTabStrip。
在将选项添加到选项卡的addTabView方法中,看到每个item其实是加到SlidingTabStrip中。
三、自定义实现主要过程
通过读TabLayout源码,我们发现思路1,直接继承Tablayout重写onLayout和onMeasure对子view重写排列和测量不行,因为TabLayout里是加了一个私有的SlidingTabStrip作为子类,Item的view都是加在SlidingTabStrip中,没有办法通过重写私有SlidingTabStrip的onLayout和onMeasure。
所以现在只能继承ViewGroup,需要自己处理ViewGroup的测量和布局的两个过程。我们可以通过仿写TabLayout实现MyTabLayout,最简单的办法就是先把TabLayout拷贝出来到MyTabLayout,再进行定制化修改。
实现:
1.先把TabLayout拷贝出来到MyTabLayout,先跑通。
2.TabLayout包裹了一个SlidingTabStrip,我们只需要重写SlidingTabStrip的onLayout和onMeasure, 再重写TabLayout的onMeasure,让他的宽高为唯一子类SlidingTabStrip的宽高。
ViewGroup常用的生命周期回调:initView(构造方法)、onFinishInflate(当布局加载完成调用)、onMeasure(当测量时调用)、onSizeChanged(当尺寸改变调用)、onLayout(当布局时调用)、onDraw和dispatchDraw(绘制背景以及绘制子View)。
初始化ViewGroup的流程大致为:构造方法创建对象->从布局加载(xml中定义时)->第一遍测量->开始改变尺寸->第一遍布局->第二遍测量->第二遍布局
重载onMeasure()方法
为什么要重载onMeasure()方法这里就不赘述了。测算间距space如果间距小于3dp按顺序排列,否则每行N个(item为4个字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的绘制。
最后重写,TabLayout的onMeasure方法。
到目前为止,已经基本实现了,多层的选择器。
另外:
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.通过实践有了更深的体会。